prompt.ml XSS Summary – Part 2

Hi again 🙂

First of all, if you are not familiar with promt.ml XSS challenge, please stop reading and go try your luck – much fun guaranteed!!

Secondly, if you are familiar with this challenge, and you have solved the first 8 levels – feel free to read my summary to these levels

Now that the prompt.ml challenge is officially over, and the XSS Challenge wiki is out, i took the liberty of posting the solutions for levels 9 – 15.

Most of the descriptions here are from the wiki, because i was too lazy to type my own 😛

Level 9

level_9

Level 9 uses the regular expression “<([a-zA-Z])” which prevents the user from adding any alphabet followed by an opening bracket (<) and hence preventing us from injecting a valid HTML tag. However the problem here is thetoUpperCase() method converts not only English alphabet, but also some Unicode characters, as ECMAScript Language Specification states:

This function behaves in exactly the same way as String.prototype.toLowerCase, except that characters are mapped to their uppercase equivalents as specified in the Unicode Character Database.

The ſ character, when passed to the toUpperCase() function would be converted to the ASCII character “S” hence solving our problem.

level_9_31

Level 10

level_10

Level 10 is one of the easier to solve levels of this challenge. There are two regular expressions to bypass: the first removes all the occurrences of promptkeyword, while the second removes all single quotes '. To bypass the first regular expression is enough a single quote to split prompt keyword to pr'ompt, this clearly is not a valid JavaScript instruction but no panic the second regular expression will remove the intruder character ' giving back a valid attack vector!

level_10_10

Level 11

level_11

Level 11 allows us to inject directly into what will be the body of a script element. However, before doing so, the string we can influence experiences heavy filtering and we cannot inject any operators or other language elements that would allow for easy concatenation and payload injection. The trick here is to use an operator, that is alphanumeric – so an operator that doesn’t require us to use the banned special characters. Well. There is a bunch of these and one we can utilize here. The in operator.

level_11_15

Level 12 – for the lulz 🙂

level_12

This level seems to expect some kind of aaencode solution (at lest the comments suggest that – there are a few short solutions in “plain text”)

but, anyways, the good old String.fromChardCode can always help to get you to the longest vector 🙂

level_12_1055

 

Level 13

level_13

Level 13 requires a couple of interesting tricks, one of which will also be useful for the hidden level. The main goal of this level is to tamper with a JSON object (config) with a special key (source) and bypassing a bunch of limitations. Note: We have to manage to get the payload through JSON.parse(). Which is not easy and prohibits anything active and dangerous.

Analyzing the code, there’s no way to inject any attack vector within source, the only hope is in the __proto__ property of Object.prototype. A deprecatedproperty that is still present in all modern browsers.

The idea is to redefine the source value and use some filters against themselves, yeah mad but awesome! To do this, we must remind some main rules:

  • There must be only one source key
  • The source key must have a valid value otherwise will be removed:
  • // forbit invalid image source
    if (/[^\w:\/.]/.test(config.source)) {
        delete config.source;
    }
    

So, if we provide an object like this:

{"source":"_-_invalid-URL_-_","__proto__":{"source":"my_evil_payload"}}`

we have a valid object with two keys: source and __proto__.

config = {
    "source": "_-_invalid-URL_-_",
    "__proto__": {
        "source": "my_evil_payload"
    }
}

Now the interesting part. We said that the 2nd rule requires a valid image source, but the one provided is not valid (_-_invalid-URL_-_) and thus we triggered thedelete instruction: delete config.source;. Awesome! That’s is what we were looking for. At this point the config object is as follows:

config = {
    "__proto__": {
        "source": "my_evil_payload"
    }
}

This means that we have a new getter for source! In fact, config.source is equal to config.__proto__.source, this because __proto__ is an accessor property (getter/setter function). Now we have a way to inject our attack vector withinsource, but now the problem is this rule:

var source = config.source.replace(/"/g, '');

If we cannot inject a " character we still cannot break the injection point:

<img src="{{source}}">;

We need another trick .. say hello to String.replace()! It’s not commonly known that the replace method accepts some Special replacement patterns.
This is what we need:

 $`   | Inserts the portion of the string that follows the matched substring 

So, injecting the following…

{"source":"_-_invalid-URL_-_","__proto__":{"source":"$`onerror=prompt(1)>"}}

… will give us working payload without even using the double-quote!

level_13_59

Level 14

level_14

Although at first sight, it might seem easy – you can simply close the img tag with “> and then insert your own tags – and let the party begin.
But, very soon you realize that this level is quite nasty – and this is because of a few reasons:

  • You can open <SCRIPT> tags, but all of your code must be UPPER CASE.
  • vbscript is blocked, so you can’t get away with <SCRIPT TYPE=VBSCRIPT>PROMPT 1</SCRIPT> (BTW, the short IE only solutions do hint that a vbs vector exists)
  • You can’t load anything from any uri scheme that isn’t data:, so no <SCRIPT SRC=//XSS.XOM/></SCRIPT>
  • “+” is blocked, so you can’t use methods like jjencode and aaencode (or JSF*ck)

The replacement of all uri schemes to “data:” gave a lead in that direction, but:

  • \, &, and % are blocked, so you can’t use simply use text/html and hide the lower case xss in hex / decimal encodings (&#00;, \x00, %00, etc..)

The remaining solution seemed clear: create a script tag, and use base64, to hide “prompt(1)” in the data scheme. like this:

<script/src=’data:text/html;base64,cHJvbXB0KDEp’/></script>

I only had one problem: my payload gets upper-cased 🙁

This affected the definition of “base64” – out of the 3 browsers i checked, only FF accepted “BASE64”.

And, of course, the base64 payload itself had to work in upper case (and not contain a plus (+) sign !)

In order to understand what will be lost if the escape function will upper case my base64 payload, i wrote a simple tool that demonstrates this process.

here are the results (first line is the inupt, the second line is base64encode(input), and third is: “base64decode(base64encode(input).toUpperCase() ” ):

level_14_explain_1 level_14_explain_2 level_14_explain_3

As you can see, the base64 contains a decent amount of lower case chars, and converting them to upper case loses their content.

Adding spaces to the input does give different blocks of responses, but still they all contains lower case chars.

The next test i did, was to check what happens if my input is already upper case. the results were much better:

level_14_explain_4

still there are a few lower case chars, but if we pad the area that the are created with spaces, it makes their loss not important.

now, having an upper case code is exactly where we were in the begining, but with once change: now we can <SCRIPT SRC=//XSS.XOM/></SCRIPT>

so.. after much effort, I found a vector that works in pure upper case base64:

level_14_explain_5 level_14_explain6

A few notes about this output:

  • FF and chrome accept http:domain.com and http:/domain.com. (chrome will also get http:/\domain.com and http:\/domain.com)
  • I didn’t find a way to get any of the above pass the base64 uppercase routine with http:, I only managed https: in
  • There were still some glitches (marked in yellow), but newlines and spaces solved most of them, and the rest are acceptible by FF.
  • It was hard!@!@!@!@!@

So, now I have a script that is loaded from https://pmt1.ml, and I can embed it in an iframe using bas64 data uri scheme, without fearing upper case!

<IFRAME/SRC=”data:TEXT/HTML;BASE64,ICA8U0NSSVBUL1MGIIINU3JDICAKICA9SFRUUFM6UE1UMS5NTD4GPC9TQ1JJUFQKPD4=”>

One last note, because the script is running from within an iframe, it needs to call window.parent.promt(1) instead of just promt(1)

So the code in pmt1.ml is:

if(typeof window.parent != “undefined” && typeof window.parent.prompt == “function”) { window.parent.prompt(1); } else { prompt(1); }

level_14_102

Level 15

level_15

Like Level 7, also here the input is split into segments separated by the # char.

Each segment is stripped to a maximum length of 15 chars, and warped in a <p>tag.

The key difference is that unlike Level 7, it’s not possible to use the /* JS comments, and quotes will be cut due to the “data-comment” attribute which is added to each segment.

A Trick we can use here, is to use HTML comments <!-- in a <svg> tag to hide the “junk”

level_15_explain

level_15_57

 

Hidden Level

level_-1

To conclude the challenge @filedescriptor placed a hidden level. At first glance, because of the history API, it seems an HTML5 challenge but it is not. The goal is to break the conditional statement and, of course, call prompt(1). Furthermore, there is a simple but really effective filter to bypass:

  • } and < are denied

Ok this challenge is pure madness, I agree but if you’ve done all levels before, surely you remember the special trick in Level 13 about the replace function. That trick is part of the solution. The next trick is a basic feature of JavaScript, ignored by many but powerful: Hoisting.
Basically, what JavaScript says is:

it does not matter where you put your objects, if I find a declaration I’ll evaluate it first of all.

So, keep in mind that JavaScript hoists declarations (not initializations). At this point, the idea is to inject the declaration of a new object named history with a length as big as 1337. In this way it will be hoisted and will overwrite the existinghistory object with the new one created and will pass the conditional statement.

Now the question is: what’s the right object to use? The only object able to include declaration and initialization in one statement is the Function. In fact, one of the possible methods to define a new function is named Function Declarationand use the following syntax:

function functionDeclaration(a,b,c) {
    alert('Function declared with ' + functionDeclaration.length + ' parameters');
}


functionDeclaration(); //alert > Function declared with 3 parameters

To pass the conditional statement, we’ll need a function with 1338 parameters, but this is still not enough. We need a way to close the declaration because the regex is still there…and here comes the String.replace() trick with is useful pattern: $&. What it does is to insert the matched substring within the string, exactly what we are looking for since the matched substring {{injection}}contains the closing curly bracket!
With the right combination of elements, we can generate something like:

if (history.length > 1337) {                                 
   // you can inject any code here
   // as long as it will be executed
   function history(l,o,r,e,m...1338 times...){{injection}}
   prompt(1)
}   

and the payload would be:

function history(l,o,r,e,m, ....)$&prompt(1)

level_-1_1337

Leave a Comment.