YesWeHack Dojo Challenge #5 Writeup
on 4/12/2020 by Robin De Baets from Panoply Tech
I recently came across the YesWeHack Dojo Challenge #5. Having never looked at a YesWeHack challenge before, I attempted to solve it. The challenge turned out to be tricky and I managed to learn some new things.
Challenge
The challenge code looked like this:
<script>
const age = $age
const name = "$name"
if (age > 18 && name.length ){
document.write(`Welcome to my adult website ${name} 8=D`)
} else {
document.write(`Go away`)
}
</script>
We can control both the $age
and $name
parameter. There are two restrictions:
- Length of
$age
can not exceed 2 - All characters matching the regular expression
[^a-z$`{}/]
are removed from$name
Our goal is to find XSS and call alert(window.name)
.
As you can see, the restrictions on $name
are quite strict, since we can’t use for example parentheses, spaces, dots or even numbers.
Solution
My first thought was that we could make the goal a bit easier by simply calling alert(name)
instead of alert(window.name)
. Note that we’re not referring to the name
parameter in the original challenge code here.
Any variable that’s declared in the global Javascript scope, can be accessed through the window
object and vice versa.
I then started looking at various ways that we could trigger Javascript execution. Because we can’t use any <
or >
characters, it seems unlikely that the sink here would be the document.write
.
None of the allowed characters for the $name
variable allow us to break out of the context of the double quotes. This means we would have to look at $age
and possible open some new context there. Since backticks are allowed in both $age
and $name
, it seemed like a good idea to start a multiline string. Javascript strings declared with backticks are called template literals and have some interesting properties that could allow us to execute other code.
By setting $age
to `
and $name
to INJECTION`//
, we get the following result:
const age = `
const name = "INJECTION`//"
I used two forward slashes at the end to start a Javascript comment and prevent the double quote from causing a syntax error. It seems like we’re on the right path, since forward slashes are also allowed by the regular expression, so this is probably not a coincidence. As you can see, we now simply have a multiline template literal declaration of which we can control part of the body.
Template literals are interesting because they can evaluate Javascript expressions and execute code.
By using the syntax `${expression}`
, Javascript will execute the expression within the curly brackets.
This is good, since the regular expression also allowed for curly brackets.
Ideally, we would set $name
to ${alert(name)}`//
and be done with it. This is where the hard part starts however.
We can’t use parentheses, which prevents us from calling alert(name)
directly.
Experienced bug hunters and CTF players might have seen payloads like alert`1`
being used in proof of concepts and are aware of the fact that
backticks can also be used to call functions in Javascript. In reality, things are a bit more difficult.
Simply executing alert`name`
will not show the contents of the global name
variable, but rather just the plaintext string name
. This means we’ll have to use the expression syntax to get the contens of the variable.
When executing alert`${name}`
, we get this surprising result.
The alert box shows a single comma. What is happening here?
Let’s have a look at the MDN docs. Apparently, calling a function using backticks is called a tagged template.
Here you can see a quick example of this:
Apparenty, tagged templates call the target function with as first argument the plaintext parts of the template literal as an array. The expressions in our template literal will be filled in as subsequent arguments.
We can validate this ourselves.
Javascript is passing an array with two empty strings as first argument. These are the empty parts before and after our expression. The second argument is our actual expression.
This is quite annoying, since alert
does not render its second argument. In our previous example, it shows a comma because ["", ""].toString()
simply results in a single comma.
We can also validate this behaviour by calling prompt
, which does use its second argument.
Calling prompt`${name}`
shows us the following:
This makes sense, the first argument is once again the array of two empty strings and the second argument is name
. We’re getting closer, but we still need to find a way to call the alert
function.
To do this, we need some kind of gadget function that would allow us to execute arbitrary code through one of its arguments besides the first argument.
Luckily, I found such a gadget in this list.
The Function
class allows you to construct a function at runtime and execute it. The first arguments of its constructors are used for defining the function arguments. The last argument is used for setting the function body.
If we could pass alert(name)
as the last argument, we could create a function with our desired code.
Ideally, our construct would look like this: `${Function`a${`alert(name)`}```}`
. This looks quite complex, so let’s break it down.
The inner expression, Function`a${`alert(name)`}`
, creates a Function
object with a
as its first argument and alert(name)
as its second argument.
The two backticks that we put at the end of this are once again a tagged template, which we use to call the Function
object that we just created.
We’re then wrapping the entire thing in another expression with backticks and curly brackets to actually evaluate the expression.
This is nice progress, but we’re still using parentheses in our alert(name)
call. However, since we are in a template literal context now, we can make use of nested template literals to get our parentheses using some other expression.
I first came across this interesting payload by terjanq. This looks interesting, but we can’t get any outside help using variables like name
, since our final payload is rendered in a sandbox iframe that we don’t control.
Using this payload by BitK, which makes use of tagged templates and calls to String.fromCharCode
, we can inject any character we want. It requires numbers and spaces however, which we can’t use.
I then found the following snippet by cgvwzq.
In this snippet, he uses calls to atob
. This function will base64 decode its first argument.
Luckily for us, the base64 equivalent of (
and )
are only made up of alphabet characters.
Putting all of this together, we can create our final solution.
$age: `
$name: ${Function`a${`alert${atob`KA`}name${atob`KQ`}`}```}`//
The rendered page will look like this:
<script>
const age = `
const name = "${Function`a${`alert${atob`KA`}name${atob`KQ`}`}```}`//"
if (age > 18 && name.length ){
document.write(`Welcome to my adult website ${name} 8=D`)
} else {
document.write(`Go away`)
}
</script>
After rendering the page, we get the popup:
Great success!