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.
Opening the iframed site in a new tab we can see a small description about the challenge:
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 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:
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:
default-src 'none'; script-src 'unsafe-eval' 'strict-dynamic' 'nonce-6755725ecdd42d7e89cd72168608ad0f'; style-src 'nonce-4da53a0dad78dcf5a115126f5c4fe970'
The interesting one for us is
Looking at the HTML we can see a script that seems to do some interesting stuff:
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
xss parameter get prepended by
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
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.
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:
<!-- 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.
<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:
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:
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.
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
<'aa>) makes the tag not valid anymore because it cannot start with the
' character. It can however end with a
;) again to end this line, and continue with the string. A tag like
<c;'> would thus be valid, and would make
?html=</h1></div><z id=intigriti><z><c;'>&xss=;alert(document.domain) with 69 characters (link)