Sunday, 6 October 2013

XSS challenges on - Part 1

If you haven't seen it yet, there are 14 challenges that test your XSS filter evasion skills over here:

Go try them before you read further, because spoilers.

Having never found any significant XSS on the web or attempted filter evasion of any sort, I could only manage the first 7 within reasonable time. Here are the solutions + a little explanation to them. Not all of them are the shortest solutions that exist.

Challenge 0

function escape(s) {
  // Warmup.

  return '<script>console.log("'+s+'");</script>';

No escaping or input filtering of any sort, so the answer is pretty simple:


Shortest, 12 characters.

Challenge 1

function escape(s) {
  // Escaping scheme courtesy of Adobe Systems, Inc.
  s = s.replace(/"/g, '\\"');
  return '<script>console.log("' + s + '");</script>';

Oh no, the quotes are being escaped! But nothing else is, so we can escape the escape character so the quote isn't escaped.


Shortest solution is 14 characters. This is 21.

Challenge 2

function escape(s) {
  s = JSON.stringify(s);
  return '<script>console.log(' + s + ');</script>';

Now many characters will be escaped including \. So closing the script tag, and adding a new one seems to work. What I think happens here is that the browser sees this as a closing tag to the script tag in the beginning. Even though it throws a couple of errors,

this solution seems to work.


Shortest solution is 27 characters, this is 34.

Challenge 3

function escape(s) {
  var url = 'javascript:console.log(' + JSON.stringify(s) + ')';

  var a = document.createElement('a');
  a.href = url;

JavaScript in the address bar. So we can no longer use the closing script tag trick. But, because this will be put in the address bar, the URL will be decoded. %22 is equivalent to ", but %22 won't be escaped by JSON.stringify()

So the solution is same as the first challenge, only the quotes are replaced by %22


Shortest solution is 15 characters. Just one extra here.

Challenge 4

function escape(s) {
  var text = s.replace(/</g, '&lt;').replace('"', '&quot;');
  // URLs
  text = text.replace(/(http:\/\/\S+)/g, '<a href="$1">$1</a>');
  // [[img123|Description]]
  text = text.replace(/\[\[(\w+)\|(.+?)\]\]/g, '<img alt="$2" src="$1.gif">');
  return text;

This one took me some time. < is being replaced by &lt; and " is being replaced by &quot. Or is it?

This should be self explanatory. Unless you do a global regexp replace, only the first instance gets replaced. So we use extra quotes!


Shortest, 24 characters.

Challenge 5

function escape(s) {
  // Level 4 had a typo, thanks Alok.
  // If your solution for 4 still works here, you can go back and get more points on level 4 now.

  var text = s.replace(/</g, '&lt;').replace(/"/g, '&quot;');
  // URLs
  text = text.replace(/(http:\/\/\S+)/g, '<a href="$1">$1</a>');
  // [[img123|Description]]
  text = text.replace(/\[\[(\w+)\|(.+?)\]\]/g, '<img alt="$2" src="$1.gif">');
  return text;

Oh noes! the replace issue has been fixed! What now? After much trial and error, I found that this happens to work.


The description parameter has fewer restrictions when it is being matched by regexp, so we use http:// to bring regexp hell and find our way though that. We control some text outside the quotes, and then use single quotes to avoid being escaped.

Shortest, 30 characters.

Challenge 6

function escape(s) {
  // Slightly too lazy to make two input fields.
  // Pass in something like "TextNode#foo"
  var m = s.split(/#/);

  // Only slightly contrived at this point.
  var a = document.createElement('div');
  a.appendChild(document['create'+m[0]].apply(document, m.slice(1)));
  return a.innerHTML;

Reddit may have been of a little help here, but I was on the right track. TextNode will escape everything, so it would be difficult to find a way around it.

But TextNode isn't the only string which when suffixed to `create` gives you a document.<something> function. The JavaScript console in Chrome developer tools is an excellent place for this.

createComment fulfills the purpose. Close the comment, and add a script tag.


Shortest unverified solution is 13 characters. Next one by  is 29 characters. This is 34.

I tried some hex encoded eval('alert(1)') on no. 8 but no luck. 
Challenges after this involve ()[]{}!+, which I have no idea about. 
But I found a good blog post on it:

That will be all.