How I met JavaScript Reflect (thanks to playing CTF)
2023-05-30
Recently I came across a challenge where the goal was to bypass some protections and getting a working Cross-Site Scripting (XSS). During the challenge I had the opportunity to discover a peculiar feature of JavaScript called Reflect
.
In this article I’d like to show how I was able to solve the challenge and how the Reflect
features came in handy.
Reflection in programming languages
From Wikipedia:
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
So, basically a reflection is a general concept in programming languages that allows a program to manipulate the properties of its own objects at runtime (sometimes this concept is also referred as metaprogramming).
In JavaScript there is a out-of-the-box built-in support for reflection. Since a JavaScript object is just a collection of name/value pairs, we can iterate through the properties of an object. This is the simplest example of reflection in JavaScript and the following code demonstrates how it works:
var user = {
name: "John",
lastName: "Doe",
age: 25,
getAge: function(){
return this.age;
}
}
for(var prop in user){
console.log(prop + ": " + user[prop])
}
If we run the previous code, we get the expected output:
name: John
lastName: Doe
age: 25
getAge: function(){
return this.age;
}
There are many other tools in JavaScript that allows you to introspect and modify the behaviour of a program, mostly using the Object
type , such as Object.keys()
, Object.getOwnPropertyNames()
, Object.defineProperty()
, Object.setPrototypeOf()
, etc..
Meeting JavaScript “Reflect”
Few days ago I was participating in a CTF challenge (hosted by Intigriti) where the goal was really simple: given a website, you need to find a way to trigger a Cross-Site Scripting on its domain without any user interaction (i.e. by opening a crafted “malicious” link). The XSS payload must alert
the current document.domain
value.
The challenge has just ended but it’s still available here, so you can play with it.
The client side code of the challenge is really simple and it’s shown below:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="script-src 'none'; script-src-elem data: 'unsafe-inline'">
<title>XSS Challenge</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>XSS Challenge</h1>
<form method="GET">
<label for="xss">Enter your XSS payload:</label>
<input type="text" name="xss" id="xss" placeholder="e.g. print()">
<input type="submit" value="Submit">
</form>
<script>
(()=>{
opener=null;
name='';
const xss = new URL(location).searchParams.get("xss") || '';
const characters = /^[a-zA-Z,'+\\.()]+$/;
const words =/alert|prompt|eval|setTimeout|setInterval|Function|location|open|document|script|url|HTML|Element|href|String|Object|Array|Number|atob|call|apply|replace|assign|on|write|import|navigator|navigation|fetch|Symbol|name|this|window|self|top|parent|globalThis|new|proto|construct|xss/i;
if(xss.length<100 && characters.test(xss) && !words.test(xss)){
script = document.createElement('script');
script.src='data:,'+xss
document.head.appendChild(script)
}
else{
console.log("try harder");
}
})()
</script>
</body>
</html>
We can immediately notice few interesting things:
- There is a form that accepts a user input
xss
and sends a GET request together with the user specified input - A JavaScript anonymous function is invoked. It gets the
xss
parameter from the URL and performs some checks on it. These checks basically allow only a limited set of characters, with a maximum length < 100 and prevent the input to include any forbidden words - There is a CSP (Content Security Policy) defined via the
meta
tag. However it allowsinsafe-inline
scripts for thedata:
scheme for thescript-src-elem
elements - If all the conditions are met, a new
script
tag is created and our input is concatenated to the document (allowing us to execute the JavaScript code) otherwise the string “try harder” is printed on the console log
The immediate straightforward attempt would be to just set the xss
parameter to alert(document.domain)
. But of course, it’s not that simple. Our payload is violating the checks that are performed by the application (it includes 2 forbidden words, alert
and document
).
So in order to inject and run our desired payload ( alert(document.domain)
) we first need to find a way to bypass the blacklist defined by the application. Also, notice that we can only use a limited set of characters (due to the regular expression) and the final payload must be shorter than 100 characters.
The first major issues is that we can’t use any of the blacklisted words. This is a problem, especially because we can’t refer many useful “global properties” like Object
, this
, window
or document
.
However, by reading the JavaScript documentation, we can find a useful property called frames. This property represents a list of frames of the current window and frames === window
is always true. The word frames
is not present in the blacklist, so we can use it to refer the window object.
Calling alert
or frames.alert
is exactly the same thing because the execution context of the alert
function is the global execution context (the same for the window
or frames
object). You can verify this by observing that frames.alert === alert
always returns true.
Now, if we could access the alert
property of the frames
object using the array-like notation, we could get the code for the alert function. Let’s try it to the console:
As we can see it works, but we have 2 problems:
- the word
alert
is included in the blacklist, so we can’t directly using it - the
[
and]
characters are forbidden by the regular expression
The first issue can be easily bypassed: we can simply use the +
(which is allowed by the regex) to concatenate 2 strings and bypass the blacklist. For example the following code will return the alert
function just like the previous one:
However, we still can’t use the bracket notation to get the reference to the alert
function, as it’s forbidden by the regular expression. How can we solve this problem?
This is where I got stuck and where I had to look for something new that I didn’t know before: the Reflect
object.
The ECMAScript version 6 (released in 2015) introduced several useful new features for JS developers, such as:
- the
const
keyword to support constants and immutable values - the
let
keyword for block-scoped variables - the spread operator
...
- the
class
keyword for writing programs in a more intuitive Object Oriented Programming style - and many others
Among these new features, also a Reflect
object has been introduced, which is basically another way to work with reflections in JavaScript. Refect
is a new global object which provides static methods that can be used for reflection and metaprogramming.
Let’s see a basic example to better understand Reflect
:
var user = {
name: "John",
lastName: "Doe",
age: 25,
getAge: function(){
return this.age;
}
}
console.log(Reflect.get(user, 'name'));
console.log(Reflect.get(user, 'getAge'));
If we run the previous snippet of code in a browser console, we will get the following result:
It means that Reflect.get(target, 'property')
is looking for the specified property
of the target
object and returns it if it exists.
We can also change the behaviour of an object at runtime, by adding/modifying new or existing properties:
Reflect.set(user, 'avatar', 'picture.png');
With that in mind, we can leverage the Reflect
capabilities, to bypass the security checks of the application and run our payload. Basically we want to invoke the alert()
function without actually directly calling alert()
.
So, if we put all together, we can get the alert
function reference by using Reflect
as follow (notice that we are using single quotes ('
) because double quotes ("
) are forbidden):
Reflect.get(frames, 'aler' + 't')
and from that we just need to invoke the function by passing the desired argument (document.domain)
:
Reflect.get(frames, 'aler' + 't')(document.domain)
The previous code works in a browser console, but it still gets blocked by the target application, because we are using a forbidden word (document
).
But guess what? We can use Reflect
again to get the reference to the document
object, just like we did for the alert
function!
So the final payload will be (we need to remove the spaces because are not allowed):
Reflect.get(frames,'aler'+'t')(Reflect.get(frames,'documen'+'t').domain)
And clicking on this link will finally trigger the XSS.
Challenge solved!
Conclusions
Playing CTF competitions is a great way to learn new stuff. In this case, it gave me the opportunity to learn a powerful JavaScript feature that I’ve never seen before. This also reminds me that “With great power comes great responsibility”: powerful features can be really helpful but at the same time they can introduce new attack vectors previously unknown.