XSSGame by Google at #HITB2017AMS – Writeup

CTF’s homepage

CTF’s homepage

During the last edition of HITB in Amsterdam we partecipated in the XSSGame by Google: 8 XSS challenges to win a Nexus 5X. The various levels exposed common vulnerabilities present in modern web apps.

Introduction

Each level required to trigger the JavaScript’s alert function by creating an URL with a Cross-Site Scripting (XSS) payload inside, which should be executed without any user interaction: once it is executed, the server replies with the link to the following challenge.

Level 1

A search bar is available, on submit the input query is printed into the page HTML code. All we have to do is search for <script>alert(1)</script> and an alert will popup on page load.

Level 1

Level 1

Level 2

A form containing a timeout input value is available, on submit the page will wait for the seconds entered and popup an alert. Looking at the HTML source code it is possible to guess that the timeout value entered (timer GET parameter) is directly inserted into the startTimer() JS function of the onload HTML attribute in

1
<img id="loading" src="/static/img/loading.gif" style="width: 50%" onload="startTimer('user_input');" >

potentially leading to a JavaScript code injection.

Requesting <challenge url>/?timer='-alert(1)-' leads to code injection and Level 3.

Level 2

Level 2

Level 3

XSS library is a gallery of cat pictures, allowing us to navigate through the pictures. Only the fragment identifier is changed on picture change.

Looking at the source code we can see the function chooseTab, called on url change, appends the fragment identifier (user_input) to the src attribute of an img:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function chooseTab(user_input) {
 	var html = "Cat " + parseInt(user_input) + "<br>";
 	html += "<img src='/static/img/cat" + user_input + ".jpg' />";
 	 
 	document.getElementById('tabContent').innerHTML = html;
 	 
 	// Select the current tab
 	var tabs = document.querySelectorAll('.tab');
 	for (var i = 0; i < tabs.length; i++) {
 	    if (tabs[i].id == "tab" + parseInt(user_input)) {
 	        tabs[i].className = "tab active";
 	    } else {
 	        tabs[i].className = "tab";
 	    }
 	}
 	 
 	window.location.hash = user_input;
 	 
 	// Tell parent we've changed the tab
 	top.postMessage({'url': self.location.toString()}, "*");
}

Playing with the fragment identifier allows us to exploit the DOM based XSS injecting JavaScript code into an arbitrary HTML attribute, for example “onerror”: <challenge url>/#1'onerror=alert(1)> (It’s important to note that this payload would not work in the latest versions of Firefox since it url encodes the fragment identifier)

Level 3

Level 3

Level 4

A Google Reader 2.0 homepage is presented to us, with registration available.

No reflection seems to be available during the procedure, but we observe that just after the registration a redirect is executed, bouncing us back to the homepage. The parameter next in <challenge url>/confirm?next=welcome lead us to think welcome is some sort of page identifier, looking at the source code the vulnerability is evident:

1
2
3
4
5
<script>
  setTimeout(function() { 
    window.location = user_input; 
  }, 1000);
</script>

Requesting <challenge url>/confirm?next=javascript:alert(1) triggers the alert.

Level 4

Level 4

Level 5

A Google-ish search bar, again. Now AngularJS-flavoured, the web app ships with a vulnerable version of the famous JavaScript framework, 1.5.8. The form inputs utm_term and utm_campaign are set from the request parameters, if any.

The first things that come to mind when looking for vulnerabilities in an AngularJS-powered web app are template injections and AngularJS sandbox escapes (which from version 1.6 have been removed).

It is easy to find the correct payload in order to escape the sandbox of the JavaScript framework and insert it in one of them:

<challenge url>/?utm_term=&utm_campaign={{x={'y':''.constructor.prototype};x['y'].charAt=[].join;$eval('x=alert(`1`)');}}
Level 5

Level 5

Level 6

Like level 5, a search bar and a vulnerable version of AngularJS (1.2.0) are available. Again the payload is not directly usable through the search bar because the query string entered is used inside of a ng-non-bindable div element, which as the documentation reports it is not interpreted by Angular at runtime.

After some dumb-fuzzing we notice that:

  • the URL is used as the form action attribute;
  • UTF-8 characters as £ in the URL make the page throw a 500 error (later it’d be marked as a bug, ahah);
  • the { character is deleted from the URL on page load.

Looking into the first and third notes we try to use the HTML entity version &lcub;, which works and it’s been interpreted – from here we can encode all the braces in the payload and spawn our alert: <challenge url>/?query=&lcub;&lcub;a='constructor';b=&lcub;};a.sub.call.call(b[a].getOwnPropertyDescriptor(b[a].getPrototypeOf(a.sub),a).value,0,'alert(1)')()}}

Level 6

Level 6

Level 7

A blog page loads content using the menu GET parameter and JSONP requests, the OK response contains title and pictures attributes (which will be used to populate a h1 and few img HTML elements), while an ERROR response contains only the title attribute. In the description this challenge is described as a “common CSP bypass”; unfortunately we haven`t the screenshot for this one.

XSS is possible inside of the base64-encoded menu parameter, but contrary to previous challenges (where it wasn’t present), CSP is defined as default-src https://hitb.xssgame.com/static/ <challenge url>.

The callback parameter in the JSONP request can be used to inject valid Javascript code into the response, which will be interpreted client side, for example:

1
2
GET /jsonp?callback=alert(1);// HTTP/1.1
[...]

returns

1
2
3
[...]

alert(1);//[...]

We can use the JSONP endpoint as src of a script element in order to bypass CSP and execute the alert: <challenge url>?menu=base64_encode(<script src="jsonp?callback=alert(1)%3b%2f%2f"></script>) (like previously, base64_encode is only used for better readability, final payload is already base64 encoded)

Level 8

The last one mixes the previous challenges into one: “the exploit must work for any user, logged in or not, and CSRF, self-XSS and CSP should be exploited in order to win”, the introduction says.

It is possible to execute bank transfers logging into an account by username (optional) and sending a transfer with name and amount values. After the login a username cookie is set containing the username entered, name and amount of the transfer are sent as GET parameters, alongside a random 16 bytes CSRF token saved as cookie. CSP is defined like level 7.

Looking into the transfer procedure it is clear the amount field is vulnerable to reflected XSS and the CSP is not defined, however it is just the self-XSS part of the challenge and it works only with a csrf_token parameter matching the homonym cookie.

During the login procedure we notice a request to <challenge url>/set?name=username&value=<username entered>&redirect=index, which would set our username cookie and send us back to the homepage.

Using this “feature” we can set an arbitrary cookie with an arbitrary value and redirect the user to an arbitrary page. In our case we can set the csrf_token cookie and redirect the user to /transfer where the transfer will be executed because the cookie and the csrf_token GET parameter will then match: <challenge url>/set?name=csrf_token&value=arbitrary&redirect=url_encode(/transfer?name=attacker&amount=3"><script>alert(1)</script>&csrf_token=arbitrary) (using url_encode for better readability, the argument should be url-encoded in the URL.)

Level 8

Level 8

Conclusion

Thanks HITB for the the great conference and Google for the Nexus 5X! 🤟🏻

The prize

The prize

5 min

Date

26 April 2017

Author

polict

I’m a vulnerability researcher and exploit developer at Shielder. In my free time I enjoy backpacking 🧭