Node.js原型链污染
Node.js原型链污染
一般审计原型链污染的思路可以从可以执行命令(或能达到目的)的危险函数入手,往上推,尝试找到可控的参数,来污染到危险函数,从而执行命令达到命令。或者,先找到可控的地方再去寻找能够污染的危险函数,来达到目的等。
引入
可以在浏览器的控制台执行下面这行代码:
1 |
|
明明object2没有hey属性,为什么也可以输出Hello World呢?因为我们对object1的原型对象设置了一个hey属性,而object2和object1一样,都是继承了Object.prototype。在获取object2.hey时,由于object2本身不存在hey属性,就会往父类Object.prototype中去寻找。这就造成了一个原型链污染,所以原型链污染简单来说就是如果能够控制并修改一个对象的原型,就可以影响到所有和这个对象同一个原型的对象。
可能到你这里还是不能清楚的理解,没关系,这只是简单的引入原型链污染这个概念,下面会尽可能的详细介绍和理解概念(~ ̄(OO) ̄)ブ
prototype和_proto_
在前面的代码中
1 |
|
代码访问了object1的原型链上的__proto__属性,然后在原型对象上添加了一个名为”hey”的属性,并将其值设置为”Hello World”,然后就污染了Object.prototype,为什么会这样呢?
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性
所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
搬一下p牛的解释:
JavaScript中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义:
1 |
|
Foo
函数的内容,就是Foo
类的构造函数,而this.bar
就是Foo
类的一个属性。
一个类必然有一些方法,类似属性this.bar
,我们也可以将方法定义在构造函数内部:
1 |
|
但这样写有一个问题,就是每当我们新建一个Foo对象时,this.show = function...
就会执行一次,这个show
方法实际上是绑定在对象上的,而不是绑定在“类”中。
我希望在创建类的时候只创建一次show
方法,这时候就则需要使用原型(prototype)了:
1 |
|
我们可以认为原型prototype
是类Foo
的一个属性,而所有用Foo
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的foo
对象,其天生就具有foo.show()
方法。
我们可以通过Foo.prototype
来访问Foo
类的原型,但Foo
实例化出来的对象,是不能通过prototype访问原型的。这时候,就该__proto__
登场了。
一个Foo类实例化出来的foo对象,可以通过foo.__proto__
属性来访问Foo类的原型,也就是说:
1 |
|
现在回到之前的例子
1 |
|
这里将hey属性和值”Hello World”赋给object1的父类的prototype也就是object.prototype
console.log(object2.hey);就可以输出”Hello World”
原型链污染的条件
我们已经知道了可以通过设置__proto__
的值来使原型链被污染,但是进一步的看,该如何才能设置__proto__
,(毕竟一个正常的程序肯定不会出现任由我们设置__proto__
来造成污染的)
其实一些操作能够达到控制数组(对象)的“键名”的效果:
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
- copy等
以最常见的对象merge为例:
1 |
|
需要注意的是:
在JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历object2的时候会存在这个键。
运行结果:
1 |
|
可以看到object3已经被污染了,应为它一开始并没定义属性b
从污染原型链到执行代码
现在已经可以造成原型链污染了,那么怎么利用污染来进一步利用,执行代码呢?︿( ̄︶ ̄)︿
和php等语言一样,node.js等js中也存在着一些可以用来执行代码的危险函数
一些危险函数或方法
eval()、setInteval(some_function, 2000)、setTimeout(some_function, 2000);、Function(“console.log(‘HelloWolrd’)”)()等
- eval()
和php中的eval()一样,它执行其中的的 JavaScript 代码。也就是说如果我们可以控制传入eval的值,就能执行任意js代码
- setInteval(some_function, 2000)
间隔两秒执行函数
- setTimeout(some_function, 2000);
两秒后执行函数:
some_function处就类似于eval函数的参数
- Function(“console.log(‘HelloWolrd’)”)()
输出HelloWorld:
类似于php中的create_function
构造->外部命令
同样的也是和php差不多,eval()等函数只能执行该语言的代码,并不能去执行系统命令,这时候就需要借助一些已有的库或函数来实现执行外部命令了
比较常见的就这两个
- require(‘child_process’).exec(‘tac flag’);
- global.process.mainModule.constructor._load(‘child_process’).exec(‘tac flag’)
例如
1 |
|
执行命令ls
1 |
|
一些例子
以后遇到这个类型比较有意思的题目可能会记录在这里,但现在除了ctfshow上面的也没遇到过该类型的题目,先记一下p神文章中的Code-Breaking 2018的这个经典例子吧
Code-Breaking 2018 Thejs
完整代码在这里
主要看一下出现漏洞的部分,server.js
1 |
|
根据上面的学习,可以看出来lodashs.merge函数这里存在原型链污染漏洞。
条件有了,接下来就需要找出能够利用的函数或功能,
该页面最终会通过lodash.template进行渲染,看lodash/template.js
1 |
|
sourceURL是通过下面的语句赋值的,options默认没有sourceURL属性,所以sourceURL默认也是为空。
1 |
|
如果我们能够给options的原型对象加一个sourceURL属性,那么我们就可以控制sourceURL的值。
继续往下面看,最后sourceURL传递到了Function函数的第二个参数当中,这里的Function函数就是前面说的危险函数
到这里就能构造去执行代码了
payload:
POST传给主页面
1 |
|
还有一个更好的payload
1 |
|
关于为什么要多加一个for循环,p神的解释
一些特性
以后如果遇到什么有趣的特性就记在这里吧
- 有时候可以用大写或小写绕过限制
1 |
|
- 过滤逗号
node.js会把同名参数以数组的形式存储(处理req.query.query的时候,会把这些值都放进一个数组中),而JSON.parse
会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析
参考资料
深入理解 JavaScript Prototype 污染攻击 | 离别歌 (leavesongs.com)
Node.js 常见漏洞学习与总结 - 先知社区 (aliyun.com)
再探 JavaScript 原型链污染到 RCE - 先知社区 (aliyun.com)