prompt.ml
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#
- Challenge site: https://prompt.ml/
- Solution site: https://github.com/cure53/xsschallengewiki/wiki/prompt.ml
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.
<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
<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
()
<!-- Both of these executes prompt(1) -->
<script>eval.call`${'prompt\x281)'}`</script>
<script>prompt.call`${1}`</script>
- More on
.call
: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call
Closing HTML comments#
!-->
can be used for closing HTML comment, specifically,<!--
. This character sequence raises aParse 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.
<!-- This is valid -->
<!-- This is (somewhat) valid --!>
Attribute separators#
- Aside from the
/
trick to replace spaces (e.gsvg/onerror=alert(1)
), attributes can be separated usingU+000A LINE FEED (LF)
andU+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 theonerror
and=
are on different lines
"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 toid
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
<script src="test.js" async>
<script/async/src=https://google.com>
Uppercase and Lowercase tricks#
toUppercase
andtoLowercase
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
(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 executealert(1)
, and then raise a runtime exceptionVM147: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:
// The string "prompt" is equivalent to 1558153217 in Base 36
parseInt("prompt",36); //1558153217
- We can convert back the number into the original string
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
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:
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#
- We can use JSONP endpoint to perform Reverse clickjacking (also called SOME). More details here: https://public-firing-range.appspot.com/reverseclickjacking/
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:
if (somecondition) {
foo(); // Hello
function foo {
console.log("Hello");
}
}
but not this one:
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?