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 allows insafe-inline scripts for the data: scheme for the script-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:

reflect-1

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:

reflect-2

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 Reflectobject.

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:

reflect-3

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');

reflect-4

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.