Flag Remover - KitCTF 2024


tl;dr

  • Dom clobbering to clobber document.body
  • Bypassing Dompurify dom clobbering protection
  • Dom clobbering using form tags

šŸ”Ž Initial analysis

We are given an application where there’s html injection. Looking at the server.js file we can see the main functionality which allows HTML injection.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18

app.post('/chal2', (req, res) => {
    let { flag, html } = req.body;
    // don't xss
    html = DOMPurify.sanitize(html);
    // don't close/open the body tag
    html = html.replace(/<body>|<\/body>/g, '');
    res.setHeader("Content-Security-Policy", "default-src 'none'; script-src 'self';");
    res.send(`
        <head>
            <script async defer src="/removeFlag.js"></script>
        </head>
        <body>
            <div class="flag">${flag}</div>
            ${html}
        </body>
    `);
});

However there is no easy XSS as the input HTML is sanitized using Dompurify and there is also a CSP.

Looking at the adminbot .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
app.post('/admin', async (req, res) => {
    try {
        const { solve } = req.body;
        const browser = await puppeteer.launch({ executablePath: process.env.BROWSER, args: ['--no-sandbox'] });
        // go to localhost:1337, type flag and html, click submit
        const page = await browser.newPage();
        await page.goto('http://localhost:1337/');
        await page.type('input[name="flag"]', flag.trim());
        await page.type('input[name="html"]', solve.trim());
        await page.click('button[type="submit"]');
        await new Promise(resolve => setTimeout(resolve, 5000));
        // make sure js execution isn't blocked
        await page.waitForFunction('true')
        // take a screenshot
        const screenshot = await page.screenshot({ encoding: 'base64' });
        await browser.close();
        res.send(`<img src="data:image/png;base64,${screenshot}" />`);
    } catch(e) {console.error(e); res.send("internal error :( pls report to admins")}
});

The admin bot types the flag in the flag field and our HTML , and takes a screenshot of the rendered page and returns it.

But we can’t see the flag as there is a removeFlag.js which removes the flag from the DOM quickly .

1
2
3
4
5
6
7
8
app.get('/removeFlag.js', (req, res) => {
    res.type('.js');
    res.send(`try {
        let els = document.body.querySelectorAll('.flag');
        if (els.length !== 1) throw "nope";
        els[0].remove();
    } catch(e) { location = 'https://duckduckgo.com/?q=no+cheating+allowed&iax=images&ia=images' }`);
});

šŸ„· Attack plan

So we have to somehow stop the removeFlag.js from removing the flag from th DOM so that the screenshot will contain the flag.

Since i had HTML injection the first idea that came into my mind was Dom Clobbering. We can clobber document’s properties using the embed, form, iframe, image, img,object HTML tags using the name attribute.

For eg if I inject <form name="body">, document.body will return the form tag instead of the documents body contents.

But however there’s still another problem Dompurify has DOM Clobbering protection. Dompurify will remove the id and name attributes from HTML tags if the value of those attributes are existing properties of the document Object.

So we cant give <form name=body> as Dompurify will remove that attribute .

This is the relevant code that does this in Dompurify.

1
2
3
4
5
6
7
8
9
const _isValidAttribute = function (lcTag, lcName, value) {
    /* Make sure attribute cannot clobber */
    if (
      SANITIZE_DOM &&
      (lcName === 'id' || lcName === 'name') &&
      (value in document || value in formElement)
    ) {
      return false;
    }

Bypassing Dompurify’s protection šŸŒŸ

When i looked into how the sanitization is implemented there seems to be changes made to the sanitized HTML by removing the body tags .

1
2
3
html = DOMPurify.sanitize(html);
// don't close/open the body tag
html = html.replace(/<body>|<\/body>/g, '');

This is a bad practise, to modify or to make changes to the HTML after it has been sanitized by Dompurify.

So now we can give <form name="<body>body"> Dompurify wont remove this as there is not property in the Document object called <body>body and after it remove’s the body tags it will become <form name="body"> which gets rendered and will clobber the body successfully .

Now we can give a p tag inside the form tag with class attribute flag which will be returned when let els = document.body.querySelectorAll('.flag') is called and that p tag will get removed instead of the real flag.

šŸš€ Final Payloads

1
2
3
<form name="body<body>">
<div class=flag>
</form>

we could have also gotten a 1-click XSS using

1
<a href="java<body>script:alert(1)">click me</a>

See also