原型链污染,是 NodeJs 中常见的漏洞,在做 antCTF 时也遇到的原型链污染题目,在此记录自己学习原型链污染的过程。
0x01 问:原型链有什么作用?
用来做继承,也就是基于原有的代码做一定的修改。下面是一个使用原型链实现继承的案例:
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
当「方法」的 prototype 指定对象原型之后,当试图访问该类的对象属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。下面是一个修改 prototype 的案例:
注意:prototype 是「方法特有的」(需要大概了解,后面需要使用到)
- 方法:类似 C++ 中的类。除了有属性
__proto__
, 还有属性 prototype,prototype 指向该方法的原型对象。propotype 指定其他对象之后,会包含所有原型对象的属性和方法- 对象:类似 C++ 中的对象。对象只有属性
__proto__
指向该对象的构造函数的原型对象。对象有constructor
里面包含该类的 prototype。
// 让我们从一个函数里创建一个对象o,它自身拥有属性a和b的:
let f = function () {
this.a = 1;
this.b = 2;
}
/* 这么写也一样
function f() {
this.a = 1;
this.b = 2;
}
*/
let o = new f(); // {a: 1, b: 2}
// 在f函数的原型上定义属性
f.prototype.b = 3;
f.prototype.c = 4;
// 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
// o.[[Prototype]] 有属性 b 和 c
// (其实就是 o.__proto__ 或者 o.constructor.prototype)
// o.[[Prototype]].[[Prototype]] 是 Object.prototype.
// 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
// 这就是原型链的末尾,即 null,
// 根据定义,null 就是没有 [[Prototype]]。
// 综上,整个原型链如下:
// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
console.log(o.a); // 1
// a是o的自身属性吗?是的,该属性的值为 1
console.log(o.b); // 2
// b是o的自身属性吗?是的,该属性的值为 2
// 原型上也有一个'b'属性,但是它不会被访问到。
// 这种情况被称为"属性遮蔽 (property shadowing)"
console.log(o.c); // 4
// c是o的自身属性吗?不是,那看看它的原型上有没有
// c是o.[[Prototype]]的属性吗?是的,该属性的值为 4
console.log(o.d); // undefined
// d 是 o 的自身属性吗?不是,那看看它的原型上有没有
// d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
// o.[[Prototype]].[[Prototype]] 为 null,停止搜索
// 找不到 d 属性,返回 undefined
0x02 问:__proto__
属性有什么作用?
每个「对象」都有 __proto__
属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__
来访问。
简而言之:用 prototype 无法直接访问,需要使用
__proto__
访问。prototype 是一个指针属性。
这里有个需要区分的概念:
__proto__
:指向原型对象的构造器。constructor
:指向当前对象的构造器。
(图片说明:右下角是图片的说明,左图的__proto__
的箭头指向原型对象的构造器)
(图片说明:右下角是图片的说明,左图的constructor
的箭头指向原型对象的构造器)
0x03 问:原型链污染的概念是什么?
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
基本原理:引用类型的属性被所有实例共享。案例:
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
按照 Java 中正常的继承,child2.names 应该和原对象一样,数组中只有 2 个数据。
问:怎么判断是否有原型链污染?
下面是一个原型链污染的简单案例:
function merge(target, source) {
console.log('merge', target, source);
// 遍历 source 中的 key。
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}
let o1 = {} // {} 是一个对象,存在 __proto__ 的 key。
console.log(typeof(o1));
console.log(o1); // object
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
console.log(typeof(o2));
console.log(o2); // object
merge(o1, o2)
console.log('o1 proto:', o1.__proto__);
console.log('o2 proto:', o2.__proto__);
console.log('{} proto:', {}.__proto__);
console.log(o1, o2); // 对象 {} 的原型对象变为 { a: 1 }。
o3 = {}
console.log(o3)
实战1:在 antCTF 中,使用 shvl 库对键值进行操作。
email: async function (req, res) {
let contents = {};
Object.keys(req.body).forEach((key) => {
shvl.set(contents, key, req.body[key]);
});
// 遍历请求参数中所有的 key
// 将键和值赋值为 contents(shvl 库的 set 函数)
contents.from = '"admin" ' ;
try {
await send(contents);
return res.json({
message: "Success."
});
} catch (err) {
return res.status(500).json({
message: err.message
});
}
}
问:常见的原型链污染方法有哪些?
function.__proto__.polluted
。function.prototype.polluted
。obj.__proto__.pollluted
。例如:shvl 只禁用了 __proto__
,传送门Security Fix for Prototype Pollution - huntr.devobj.constructor.polluted
。案例:
function person(fullName) {
this.fullName = fullName;
}
var person1 = new person("Satoshi");
// function:prototype, __prototype
person.prototype.sayHello = 1
console.log(person1.__proto__);
person.prototype.newConstant = 2
console.log(person1.__proto__);
// object: __prototype__, constructor
person1.__proto__.sayHi= 3
console.log(person1.__proto__);
person1.constructor.prototype.oldConstant = 4
console.log(person1.__proto__);
/*
person { sayHello: 1 }
person { sayHello: 1, newConstant: 2 }
person { sayHello: 1, newConstant: 2, sayHi: 3 }
person { sayHello: 1, newConstant: 2, sayHi: 3, oldConstant: 4 }
*/
推荐一个非常 nice 的网站:https://book.hacktricks.xyz/ 。收集 hack 中的 tricks
问:原型链污染链怎么挖掘?
(拓展阅读:反弹Shell表)
问:原型链污染怎么防御?怎么绕过防御呢?
constructor
prototype
__proto__
绕过防御:
参考教程: