The bug bounty platform Intigriti recently posted an XSS challenge that I saw on Twitter (link).
It linked to a website containing the challenge rules and the challenge itself: https://challenge-1021.intigriti.io/
Looking at the HTML, it loads a page challenge/challenge.php in an iframe. So this is probably the actual challenge.

The Challenge

Opening the iframed site in a new tab we can see a small description about the challenge:

Text

ARE YOU SCARED?
ARE YOU STILL SANE?
NOBODY CAN BREAK THIS!
NOBODY CAN SAVE INTIGRITI
I USE ?html= TO CONVEY THESE MESSAGES
I'LL RELEASE INTIGRITI FROM MY WRATH...
... AFTER YOU POP AN XSS
ELSE, INTIGRITI IS MINE!
SIGNED* 1337Witch69

It looks like we can use the GET parameter html to input some HTML code. Inputting something like ?html=123<u>test</u>321 results in our HTML actually being put cleanly on the page. You can see the 'test' is underlined as we told it to in the html parameter:

This html variable is put in the HTML code using PHP, because when the page is loaded this is also instantly loaded with the page (You can also verify this by looking at the raw response in the Network tab). This looks like a very easy XSS, if we just put our payload in this variable we can get it directly on the page.
If we try this though, the script is not executed. It shows up perfectly in the HTML but just does not execute. Looking at the Console tab we can see it is blocked by the Content Security Policy:

Text

Refused to execute inline script because it violates the following Content Security Policy directive: 
"script-src 'unsafe-eval' 'strict-dynamic' 'nonce-6755725ecdd42d7e89cd72168608ad0f'". 
Either the 'unsafe-inline' keyword, a hash ('sha256-n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg='), or a nonce ('nonce-...') is required to enable inline execution.

We can look at the full CSP by looking at the request in the <head> element. Here it has a <meta> tag with the whole Content Security Policy that is followed by the browser. In the content attribute of this element we see 3 rules:

Text

default-src 'none'; 
script-src 'unsafe-eval' 'strict-dynamic' 'nonce-6755725ecdd42d7e89cd72168608ad0f'; 
style-src 'nonce-4da53a0dad78dcf5a115126f5c4fe970'

The interesting one for us is script-src. It allows 'unsafe-eval', 'strict-dynamic', and a specific nonce. This nonce allows their own scripts to execute fine, but not any other script. The 'unsafe-eval' is interesting here because it still allows this trusted script with a nonce, to execute javascript code out of a string. Maybe we can find a way to get our input into a string and execute it through this.

Looking at the HTML we can see a script that seems to do some interesting stuff:

JavaScript

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);
});

Looking at this quickly, it executes a function after the DOM has loaded. It gets a parameter from the URL called xss and prepends some characters to it, and together they are put the variable e. Eventually, it creates a <script> tag that contains the code from that string. Creating an element like this actually executes the code that is put into the script tag. This means we have some sort of user input, that gets executed as JavaScript code! The challenge is not this easy though. The things we input via the xss parameter get prepended by )]}'. These characters obviously do not fit at the start of JavaScript, so they cause a SyntaxError. We can also see this error in the Console tab where it says Uncaught SyntaxError: Unexpected token ')'. We only have input after this error, so our code actually does not get executed because it is stopped by the error before it gets there.
We did not yet look at the code in between though. This code takes the last element in the <body> and checks if its id is set to 'intigriti'. Then it takes the last element of that element and finally takes the last 4 characters of its innerHTML. Then it prepends this to the string that is executed. If we can somehow control this value, we can just do something to make the syntax right again, and then execute our payload with the xss parameter.

If you remember from earlier, we still have control over the html parameter. We might be able to use this to get through this if() check, and fix the syntax to execute our code. It gets the last element of the body tag though, and the place our HTML gets inserted is inside of some elements around the middle of the body. Multiple more elements get placed after our injection, making it look like we can't control the last element.
We can get closer to our goal by closing a few of the parent tags we are in. Using </h1></div> we can close out of those two tags, and place HTML directly in the <body>. For example: The payload ?html=</h1></div><u>test</u> places our element directly inside of the body tag.
One thing I tried was to comment out the end, using an HTML comment. This did not work in this case though, because our input is surrounded by <!-- !!! --> tags that catch this comment and make it not go further.

Solution

Browsers often try to fix your HTML code. For example: If you forget to close a tag, the browser will try to close it for you at a reasonable place. The browser is purely guessing though, so it might make mistakes. In this case, I found that if I start a tag that does not exist, it will try to close that tag at the end of the <body> tag. So a payload like ?html=</h1></div><doesntexist> will create a tag that ends all the way at the end. This means it encapsulates all the other tags that used to be there. This is how it looks in HTML:

HTML

<!-- Before: -->
<body>
    <doesntexist>
    <div class="a">'"</div>
    <div id="container">...</div>
</body>

<!-- After: -->
<body>
    <doesntexist>
        <div class="a">'"</div>
        <div id="container">...</div>
    </doesntexist>
</body>

We also now have control over the last element! Meaning we can start to get through the if() statement from earlier. If we give this non-existent element an id of 'intigriti' we get through the first if statement.

But then the code takes the last element of this again and prepends the last 4 characters from the innerHTML of that element to the JavaScript code. With our payload it just encapsulates the existing elements, so the last element here is now just the last element that already existed before: the <div id="container"> element. We can use the same trick again though, to also control this element. If we add another non-existent element it will also encapsulate the code following it, and become the only and last element. So a payload like ?html=</h1></div><doesntexisst id="intigriti"><alsodoesntexist> will give us control over the element that gets its innerHTML put into the executed code. There is still one problem though, only the last 4 characters of the innerHTML get prepended to the code. As our payload is now, it just takes the ending </div>'s last 4 characters putting div> at the start of the executed code. If we can control these last 4 characters we can easily fix the syntax and execute our code.

We're in the final stretch now. We can once more use the same trick to create a non-existent element that will get closed by the browser at the end. This means we can sort of control the end now. The only catch is, it needs to be a valid end to an HTML tag since the browser needs to close it for us and put the text there. We can fix the SyntaxError from earlier by simply making the characters into a string. By just starting the code with a quote (') we make the code into ')]}' which is a valid string and will allow us to execute the code. Here comes the surprising part though: You can make HTML tags with quotes (') in them! A tag like <abc'def> is totally fine.
I found this out by just trying it. I did not expect it to work, but it's good to always check. This means that we can make the browser create a fitting closing tag also including the quote. If we now use a tag like </doesntexist'aa> the last 4 characters will become 'aa> and together with the SyntaxError we got, will fix it and become a valid string like 'aa>)]}'. Putting all this together in a payload looks like this: ?html=</h1></div><doesntexisst id="intigriti"><alsodoesntexist><doesntexist'aa>.
Now we can finally use the xss parameter to put any XSS payload we want. We now have our injection right after the string, so we can use a semicolon (;) to end the line, and then follow it with our payload of alert(document.domain). This will make the final created script tag look like this:

HTML

<script type="text/javascript">'aa>)]}';alert(document.domain)</script>

Finally we can put all the together and end up with a payload of ?html=</h1></div><doesntexisst id="intigriti"><alsodoesntexist><doesntexist'aa>&xss=;alert(document.domain) (link). This manipulates the HTML to get input before the error, and fixes it so our payload gets executed.

Shorter payload

This was a cool challenge, but I wanted to try and get as short of a payload as possible. The payload from ?html on is 107 characters long.
Obviously we can use shorter non existant tags. For example: the <z> tag. This would make our payload ?html=</h1></div><z id="intigriti"><z><z'aa>&xss=;alert(document.domain) and only have 72 characters.
We can do even better though, because we don't actually need the quotes (") around 'intigriti'. Using id=intigriti will make the browser fix the HTML for us, and add the quotes back automatically. So our payload becomes ?html=</h1></div><z id=intigriti><z><z'aa>&xss=;alert(document.domain) with 70 characters.
Finally, we can do a little bit better in how we fix the syntax with the last tag. Here the z character at the start of the tag does not get used in the JavaScript, but getting rid of it (<'aa>) makes the tag not valid anymore because it cannot start with the ' character. It can however end with a ' character. Now we just need some valid JavaScript to put at the start of the tag. The JavaScript variable c from earlier was used for one of our tags. We can use this to start our HTML tag and be a valid JavaScript term because it was defined earlier. Then we can use the semicolon (;) again to end this line, and continue with the string. A tag like <c;'> would thus be valid, and would make c;'>)]}' valid JavaScript code. This tag is 1 character shorter, so this is better than we did before. The final payload will then be ?html=</h1></div><z id=intigriti><z><c;'>&xss=;alert(document.domain) with 69 characters (link)

Conclusion

I learned a lot during this challenge. About browsers and how they try to fix your HTML code for you, and about tricks in JavaScript syntax. This was a very fun and interesting challenge and I will definitively try more Intigriti challenges in the future.