JavaScript n'a pas de format de sérialisation d'objet intégré comme PHP ou Java, donc les bugs de désérialisation Node vivent dans des bibliothèques tierces qui sérialisent et réaniment des fonctions. Le fer de lance est node-serialize : son unserialize() reconstruit une fonction sérialisée et, via une expression de fonction immédiatement invoquée, l'exécute au moment du chargement, ce qui fait du sink une primitive de RCE directe.
node-serialize#
node-serialize encode les fonctions avec un marqueur _$$ND_FUNC$$_. À unserialize(), une valeur étiquetée comme fonction passe par eval. Ajouter () au corps de la fonction la transforme en IIFE qui s'exécute pendant la désérialisation :
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('id',function(e,o){console.log(o)});}()"}
Le () final avant le guillemet fermant est le déclencheur : la fonction réanimée s'invoque elle-même immédiatement. Tout ce qui appelle serialize.unserialize(userInput) là-dessus exécute la commande. require('child_process') est accessible parce que l'eval s'exécute ici dans le scope du module (contrairement à un constructeur Function nu, qui est en scope global).
Livrez le JSON là où l'application le désérialise : cookies, corps, entrées de cache ou messages de file.
Autres bibliothèques#
funcster: reconstruit les fonctions dans un module en sandbox ; des motifs d'évasion atteignentthis.constructor.constructor('return process')()pour regagnerprocessetrequire.serialize-javascript: avant tout un sérialiseur, mais le fait de faire transiter sa sortie par une réanimation basée surevaldans le code applicatif réintroduit le sink d'exécution de fonction.cryo,node-cryo: sérialiseurs de graphe d'objets qui peuvent réanimer des fonctions.
Notes d'exploitation#
- Confirmez la bibliothèque et que votre entrée atteint son appel
unserialize/de réanimation ; un simpleJSON.parsen'est pas vulnérable (il ne réanime jamais de fonctions). - Pour la sortie, le callback de
child_process.execne vous revient pas par HTTP ; préférez un callback OOB (DNS/HTTP) ou un reverse shell. - Si la réanimation utilise le constructeur
Function(scope global) plutôt qu'eval(scope module), atteignez un loader viaprocess.mainModule.require('child_process')au lieu d'unrequirenu.
Outils#
- Payloads
_$$ND_FUNC$$_forgés à la main.
Références#
- PortSwigger Web Security Academy: Insecure deserialization
- node-serialize advisory writeups