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&#40;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>

Closing HTML comments

  • !--> 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?