前言
ISCC结束前几天就开始看原型链这一块了,为了填上之前那篇博客的坑还是啃了一会的,因为没有很系统了解过js因此理解起来不是特别顺利。
继承与原型链
我们都知道js是面向对象的编程语言,因此js有很多骚操作是别的编程语言没有的。而js是动态的,本身不提供一个class的实现。即便是在ES2015/ES6中引入了class关键字,但那也只是语法糖,js仍然是基于原型的。
js中只有一种结构:对象(object)。每个obj都有一个私有属性(__proto__
)指向它的构造函数的原型链对象(prototype)。然后这个原型对象也有一个自己的原型对象,这样层层向上知道NULL。那么根据原型链定义,NULL就是原型链中最后一个环节。
接下来我们来分析下什么是prototype和__proto__。
js中我们如果想要定义一个类,需要以定义函数的方式来实现:
1 | function Foo() { |
这样写确实可以实现,但是会产生一个问题,就是每新建一个Foo对象,this.show = function()…
就会执行一次,因为show方法实际上还是绑定在对象上,而并非“类”。
如果我希望创建类时只创建一次show方法,我们就需要用到prototype:
1 | function Foo() { |
我们可以认为原型prototype是类Foo的一个属性,而所有用Foo类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的foo对象,其天生就具有foo.show()方法。
我们可以通过Foo.prototype来访问Foo类的原型,但Foo实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__
登场了:
1 | foo.__proto__ == Foo.prototype |
基于js原型链的继承
js的继承机制利用的是类对象实例化时拥有prototype中的属性与方法,p牛在他的文章中用一个生动形象的例子向我们展示了这个机制:
1 | function Father() { |
很明显可以看出来,Son类继承了Father类中last_name的属性,整个操作过程是这样的:
- 在对象Son中寻找last_name
- 如果找不到,就在
son.__proto__
中寻找last_name - 就这样一层一层往上寻找,直到找到last_name或NULL结束
1 | son.__proto__ |
这个查找机制被称为prototype继承连。
js原型链污染
什么是原型链污染
结合之前所介绍的,我们不禁发出疑问,如果我们修改了foo.__proto__
中的值,那么会不会改变Foo类呢?带着这个疑问,我们来进行测试:
1 | let foo = {mrl64: 1} |
我们发现最后一个sp是一个空对象,但是输出sp.mrl64的结果是2,这正是由于修改了foo的原型foo.proto.mrl64这个obj类,给这个类增加了一个属性mrl64,值为2。而在这之后,我们用obj类创建了一个sp对象,因此sp对象中自然会有一个sp属性。
在应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象,这种攻击方式就是原型链污染。
什么情况下会存在原型链污染
这就涉及到在哪些情况下我们可以设置__proto__
的值了,其实可以控制数组键名的操作就可以做到,例如:
- 对象merge
- 对象clone(将待操作的对象merge到一个空对象中)
merge函数:
1 | function merge(target, source) { |
看到最后一个赋值语句我们不禁就来了想法了,如果这个key的名称就是__proto__
,是不是就可以触发原型链污染了?我们同样试验一下:
1 | let test1 = {} |
可以发现合并成功,但是原型链污染失败。这是因为我们用js创建test2的过程中,__proto__
已经代表test2的原型了,此时遍历test2的键名__protp__
并不是一个key,因此不会修改obj原型。
那么该怎么进行处理呢,我们来康康这个代码:
1 | let test1 = {} |
这次原型链被成功污染了,这是因为JSON解析下,__proto__
被认为是真正的键名而并非原型。marge操作极其容易存在原型链污染,多个常见库中都存在这个问题。
[GYCTF2020]Ez_Express
进入网页是个login页面,提示我们要注册,用户名只支持大写且需要使用ADMIN登录,但是注册页面又啥都没有。
怀疑存在信息搜集,测试发现存在www.zip
,下载后审计源码:
1 | //login部分 |
可以发现使用了toUpperCase(),那就是一个经典trick了,登录账号输入admın
然后点击注册,这次出现页面了。有一个输入框,问我们最喜欢的语言,随便填都会alert一个success,那我们继续审计:
1 | //action部分 |
我们在这里看到了clone函数:
1 | const merge = (a, b) => { |
看到这两个函数相信应该就能反应过来要使用原型链污染了,结合题目的名字,不难想到利用ejs来进行RCE,尤其是代码中显眼的不行的outputFunctionName:
1 | router.get('/info', function (req, res) { |
具体原理可以参考下这篇文章:
Express+lodash+ejs: 从原型链污染到RCE
那么我们构建payload:
1 | Content-Type: application/json |
接着访问/info即可获取flag。