Adventures in XSS
Yesterday I submitted my first bug bounty which felt just as good as I thought it would, great success. The exploit is on www.ziggo.tv, it's only a basic reflected XSS exploit but it was fairly hard won as they have extensive protection to deal with user input. It turns out that despite a bug bounty description that offers compensation in exchange for bugs, they don't think XSS is dangerous enough to warrant paying out. Normally I'd be more discreet with security issues but I thought that was a bit of a dick move on their part so I've decided to publish instead and at least get an interesting blog post out of it.
In this case the user submitted content was a search feature on their media site, if you enter a search term you'll get back a very ugly looking URL including an array of page parameters, one of which is the search term. This is the result of searching for "test".
You can see the search_by_keyword parameter at the end, it is set to a value of test, the basic attack methodology of reflected XSS is to craft a malicious link and then send that link to a victim(s), or simply post it on another site and encourage people to click on it. Only those people following the specially crafted URL will be affected by the vulnerability, everyone else arriving at the search page naturally will never be affected.
Trial and errorFinding XSS vulnerabilities usually involves a lot of trial and error, you're blind as to how the server processes input so you need to present specific inputs and test to see how the server behaves and what it returns, this helps you build up a mental image of the method by which they processes data and return it to the user, it almost always has some kind of sanitization that removes potentially harmful code.
The first thing to do is just throw in an attempt to open up a script tag and see the behaviour, let's try the parameter
This does several things, first of all having the word test in there helps me find the result in the source code of the page that is returned, that just makes iterative testing much faster. The " /> closes out the input box we're injecting this into. In the HTML on the result page you'll find the search box pre-populated with the old search result which is the source of the injection, this is where your search term is being returned to the page. It looks something like this normally
<input type="text" name="search_by_keyword" placeholder="Zoeken" value="" />
You can see that by using the input test" /><script> we can prematurely close the value property and then close the input tag itself, leaving us able to write valid HTML such as <script> tags.
However the result in this case was that <script> was replaced with [removed], they're specifically looking for script tags and replacing them with something harmless, this is the first piece of the puzzle that is their user input sanitization.
search_by_keyboard=test" onerror="" />
<input type="text" name="search_by_keyword" placeholder="Zoeken" value="test" />
It looks like they've removed the onerror="" but they've kept test, they obviously have some kind of blacklist of denied strings. There is however a benefit to this, unlike the detection of <script> which is replaced with [removed], these attributes are just deleted, that has the extremely helpful property of collapsing together text that exists either side. For example
There is the potential for us to use this to help beat other parts of the filter, if we can work out the order the filters are applied then we can pass the first one by splitting up our key words and have the other filter reassemble them. Let's try breaking up a script tag:
The result is
<input type="text" name="search_by_keyword" placeholder="Zoeken" value="test /><script>"
Damn, so close. <script> now evades being replaced with [Removed], however they've safely HTML encoded the angle brackets into < and >, and oddly the close quote ends up at the end rather than closing the value attribute like it should.
If there was only a method to stop the angle brackets from being HTML encoded, unfortunately you c cannot use the same trick of splitting up text in order to obfuscate it when you're only dealing with a single character...or can you?
Enter URL encoding to save the day, all text can be URL encoded, it just so happens the site is taking URL encoded input and it's decoding it for us before usage, that's handy. So we can represent < and > as their URL encoded counterparts which is %3C and %3E. These on their own are correctly URL decoded by the server and are treated like regular brackets, but there's nothing to stop us from breaking up the URL encoded variants with an event tag. Let's try.
You'll also note I've added a 2nd quote after test, this is to try and keep the HTML mark up well formatted by adding in another quote as one alone is removed. It's a success, our output is now
<input type="text" name="search_by_keyword" placeholder="Zoeken" value="test " /><script>
var new_audio = document.createElement("audio");
Let's add that in, wrap the end in a close script tag </script>, we'll need to use the same trick with onerror="" to obfuscate it. Let's also clean up after ourselves to make sure we're not leaving behind any malformed HTML, when we prematurely closed the value attribute and then closed the input tag we left behind characters that were designed to do this. So let's open up an extra html element ourselves and not close this tag, it will be closed by the characters left behind. For this purpose a half open anchor tag is fine - this keeps the page looking the same as before and masks the attack. Our final URL is
http://ziggo.tv/on-demand/series/top-10/?type=serie&genre=&category=Top+10+Serie&in_package=&package=&view_type=cover&filter_genre=&filter_package=&search_by_keyword=test"" />%3onerror=""Cscript>var new_audio = document.createElement("audio");new_audio.src="http://www.singing-bell.com/wp-content/uploads/2013/09/Jingle-Bells-Singing-Bell.mp3";new_audio.play();%3onerror=""C/script><a "
Merry Christmas everyone!
Note: Because I submit the bug to their security team it's not likely to remain active for very long, also if you have a slow connection it might take a few seconds to load the audio.