Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Intigriti's October 2021 XSS challenge writeup.

TL;DR

This month's challenge consists of the exploitiation of a custom js code hosted on a document with a Halloween style. In overview we'll be injecting JS inside a <script> tag (thanks to an interesting detail in the CSP) that was previously injected into a document's div's innerHTML.

The solution of this challenge has been found in collaboration with @carlospolopm.

Initial approach

imagen

Although the style is very much polished we won't be seeing it much during the exploitation ^_^.

Pressing super secret NSA-powered exploitation tool (a.k.a F12) we'll be able to see that there's some ways to provide input to the hosted JS and play with it.

<script nonce="NONCE">
      window.addEventListener("DOMContentLoaded", function () {
        e = `)]}'` + new URL(location.href).searchParams.get("xss");
        c = document.getElementById("body").lastElementChild;
        if (c.id === "intigriti") {
          l = c.lastElementChild;
          i = l.innerHTML.trim();
          f = i.substr(i.length - 4);
          e = f + e;
        }
        let s = document.createElement("script");
        s.type = "text/javascript";
        s.appendChild(document.createTextNode(e));
        document.body.appendChild(s);
      });
    </script>

What happens with our input?

In the code above, our GET parameter xss (URL(location.href).searchParams.get("xss")) is taken, and also html, as the explanation in the initial approach image shows.

?html=

First of all, the content inside html gets injected this way (with no relation to the script shown above):

imagen

?xss=

Let's analyse the main script of the document.

e = `)]}'` + new URL(location.href).searchParams.get("xss");

At first, the e variable holds )]}' + XSS-PARAM.

c = document.getElementById("body").lastElementChild;

Then, c holds the last element of the document's body. (Currently <div id="container">, but we may be able to control that with ?html=)

if (c.id === "intigriti") {
          l = c.lastElementChild;
          i = l.innerHTML.trim();
          f = i.substr(i.length - 4);
          e = f + e;
        }

After that there's a check. If the previous element's id equals to intigriti, e will be appended in the beginning with the last 4 characters of the lastElementChild of the current element (the one whose id equals intigriti).

let s = document.createElement("script");
s.type = "text/javascript";
s.appendChild(document.createTextNode(e));
document.body.appendChild(s);

Even if that check doesn't succeed, the document's html will be appended a script with the contents of e.

CSP Detail

Perhaps you have noticed already, but there's a detail we are missing. At first, checking the document's CSP (<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-eval' 'strict-dynamic' 'nonce-'; style-src 'nonce-'">) made me think that, even if we managed to get the script working, that script won't hold the current nonce, so it won't be executed. However, there's a specified keyword in the CSP's script-src part called strict-dynamic that will be in charge of letting us insert a script to the document through an already nonced script.

Exploitation

Now we know how everything works, let's start by trying to append alert(document.domain) with the xss param.

imagen

Although it gets inserted, there's a SyntaxError happening on it. Why? As it points out, Unexpected token ')'. The script is not recognising the text as valid JS code. For that we'll have to play with html.

For us to be able to control the first part of the script, we'll have to enter the if shown before, so let's create a div to specify it's id to intigriti and get it matched.

imagen

But according to it, our div must be the last one in the document, and it isn't currently (it'd be <div id="html" class="text">). Because of that, just adding </div> in the beginning to close the former div works like a charm.

imagen

Hmm, it is not the last element yet... Apparently, we'll be needing a tag that kinda collects more html tags (one that is not aimed to contain an strict type or tag inside). This way, Chrome will make the tag to be the last in the document.

For example, <pre> works:

imagen

We are done! The last thing we have to do is controlling the last part of that <pre>'s lastElementChild. Currently it is a div.

imagen

For that we'll just have to create another element and add a quote after it:

imagen

We now do control the first part of the script. From here there's two ways to proceed:

String+

imagen

Form a valid string (adding junk after the quote to make the quote the first character inside the script) and append the function +alert(document.domain)

Valid regex

imagen

Form a valid regex using the closing tag (/), adding a ( for the regex not to throw an error and add then the function.

This one is my favourite, so the final payload would be the following:

imagen

?xss=/;alert(document.domain)&html=</div><b/id=intigriti><a><a(>

(Notice that there are no quotes for intigriti and pre is now b)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment