(Reflect)ed XSS — Intigriti Challenge-0523 Writeup

Mohammed Moiz Pasha
11 min readMay 29, 2023

--

Challenge 0523: XSS!

The challenge:

A little introduction: Every month, the Intigriti platform hosts a fun web security challenge created by community members. They also reward a few members who successfully solve the challenge with a €50 swag voucher for their swag shop (Check it out!)

The challenge this time is to pop an XSS payload on the challenge page.

Rules for the challenge:

The XSS payload:

  • Should work on the latest version of Chrome and FireFox.
  • Should execute alert(document.domain).
  • Should leverage a cross site scripting vulnerability on this domain.
  • Shouldn’t be self-XSS or related to MiTM attacks.
  • Should be reported at go.intigriti.com/submit-solution.
  • Should require no user interaction.

Our task: Bypass any restrictions placed on the webpage in order to execute alert(document.domain)

Phase 1: Understanding the problem

A good approach to solve any such challenge would be to first explore the page (without doing any active exploitation). The information gained through this will serve as a basis for the next phases of our assessment.

Upon opening the challenge page, we are greeted with a web page that contains a single input field:

The challenge page

Entering a normal value into the input field reveals… nothing on the page itself. A GET parameter called ‘xss’ appears with our payload, apart from which there is no visual cue about anything happening on the page.

However, if we try and input ‘print()’ (which the placeholder text helpfully says), something interesting happens:

The print() function works!

print() works!

This is interesting for a few reasons. One, it means that we are able to execute print(), which is a JavaScript function. JavaScript is useful since it is the main ingredient for XSS. Two, this means that somewhere in the webpage, our payload passed through the input is actually being parsed, validated and rendered.

Entering an alert() function, sadly, does not yield any results:

alert() does not work :(

An analysis of the page reveals no other features apart from the input field and Submit button.

So, in the initial phase of the assessment, a few points can be noted:

  1. An input field is the element on the page we will be interacting with
  2. The input field can execute a print() function, but not an alert() function. This means that there is likely some validation being performed on the input.

Now, we can begin the next phase.

Phase 2: Source code analysis and… RegEx?

By pressing ‘Ctrl + U’ on the keyboard, we can open the source code of the web page, in order to try and understand what happens to our input when we press ‘Submit’

The source code

Another quick analysis reveals no hidden links or references apart from a style sheet, which means that the current page is all we have to work with.

Within a <script></script>block in the page lies the main logic for what happens when an input is passed:

The script with a few comments for understanding

When we analyze the script, we understand why the inputs in our previous phase of assessment were behaving the way they were.

The script uses Regular Expressions in order to validate our input.

A Regular Expression is a sequence of characters which is used to match a pattern in some text. In JavaScript, a RegEx pattern is defined in between two / characters (/<regex pattern>/). For any input string, the test() function can be used to check if a match occurs. A positive match returns the boolean value true and a negative match returns false .

words.test(xss) /* if there is any text in 'xss' which matches 'words', return true, otherwise, return false */

Here, the test() function is used with an ifcondition is used to check the input validity.

The if condition

Based on the RegEx defined in the script, we can deduce a few points:

const characters = /^[a-zA-Z,;+\\.()]+$/; 
  1. This RegEx pattern indicates that the only characters allowed are:
  • Alphabets from a-z (in upper and lower case)
  • Comma(,), period(.), plus(+), backslash(\), semicolon(;), and parentheses( () )

The input must start and end with these characters, and must contain at least one character to be considered valid.

2.

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/;

Any word in the input which is present in the above RegEx is matched. In the ‘if’ condition, a negation ! is applied to the words.test()function.

What this effectively means is that any of the above words are not allowed in our input. This is why while the print() function was executed, alert() was not executed, since ‘alert’ is a blacklisted word.

3.

xss.length<100

This indicates that the size of our input must not exceed 100 characters

If the above conditions are satisfied, the input we enter is appended as a script element to the page. Otherwise, a message ‘try harder’ appears in the console.

try harder

In conclusion, the key takeaways from the analysis of the script are:

  1. The payload must contain only alphabets, comma(,), period(.), plus(+), semicolon (;), backslash(\) or parentheses( () )
  2. The payload must NOT contain any of the 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
  3. The size of the payload must be less than 100 characters.

With these points as a basis, we can begin crafting our payload.

Phase 3: Of functions and strings

While there are many approaches you can apply to start crafting a payload, the one I used is specifically helpful for challenges like these: Looking at the problem through our objective.

To be more specific, we look at the result we want to achieve, and work our way around (or through) the problem to achieve that objective.

In this case, our objective is to pop an alert() function :)

The method and technique notwithstanding, at the end of the day, we need to pop an alert with the document domain. We can use that as a starting point.

We need to execute alert(document.domain)

However, as we have already seen, the word ‘alert’ is blacklisted. The word ‘document’ is also blacklisted. In order to move forward, we need to find a way around this.

A good way to look at problems like this is to think of it not in terms of what you are not allowed to do, but in terms of what you can do. We can notice something interesting when we look at the characters allowed: Single quotes (‘) and the plus(+) sign are permissible.

In JavaScript, as with most programming languages, single quotes can be used to represent a string, and the plus (+) operator can be used for string concatenation.

This means that while alert does not pass the input validation, 'ale'+'rt' , equivalent to 'alert', will pass it since there is no complete ‘alert’ word in the input! The same can be applied to the worddocument as well.

So, our objective payload transforms from alert(document.domain) to 'ale'+'rt'('docu'+'ment'.domain)

Payload: Evolution 1

If we try and input the above “payload”, we see no ‘try harder’ message in the console. This means that our “payload” has successfully evaded the RegEx.

There is just a tiny problem however. The payload will not work.

alert is a function, while 'alert' is a string. Similarly, document is a Document object, while 'document' is a String object. When we attempt to call the function on a string object, we get an error:

In order to proceed, we need to find a way in which we can pass a string as an argument, and have the literal string reference the object or the function.

Phase 4: ECMAScript 6 and a moment of reflection

There are few ways that a string can be used to reference an object in JavaScript.

  1. The most common way is to use the eval() function. The eval function evaluates the string and returns the object it references:
eval()

This would have been a quick solution to the challenge, but for the fact that the word ‘eval’ is also blacklisted. Also, there is one other factor which prevents any eval() code from being executed, and that is the CSP:

CSP. Does not allow eval()

2. Another method of passing a string uses a very interesting fact, which is not useful here but will help us later: alert() is an instance method of the window object, which is the highest object in the DOM hierarchy. This is why alert() can be called directly on a webpage without the [.] operator, since by default it refers to the windowobject. This means that another way to access the function alert would be window['alert']

object property access

However, this also does not work, since the characters ‘[‘ and ‘]’ are not allowed.

Here is where a feature of ECMAScript 6 comes in:

ECMAScript is a JavaScript standard intended to ensure the interoperability of web pages across different web browsers. In the 2015 release of ECMAScript (the 6th version), a lot of new features were added.

The release included two metaprogramming features: The Proxy class and the Reflect object.

In JavaScript, Proxyis an object that wraps around another object and can intercept or modify its behavior. However, the feature of our interest is the Reflect object.

Reflect

According to MDN web docs,

“The Reflect namespace object contains static methods for invoking interceptable JavaScript object internal methods.”

What does this mean? According to ChatGPT

The Reflect object in JavaScript has a collection of utility methods that allow you to perform common object-related operations, such as accessing properties, invoking methods, creating objects.

Here are the static methods of the Reflect object, linked with the documentation:

  1. Reflect.apply()
  2. Reflect.construct()
  3. Reflect.defineProperty()
  4. Reflect.deleteProperty()
  5. Reflect.get()
  6. Reflect.getOwnPropertyDescriptor()
  7. Reflect.getPrototypeOf()
  8. Reflect.has()
  9. Reflect.isExtensible()
  10. Reflect.ownKeys()
  11. Reflect.preventExtensions()
  12. Reflect.set()
  13. Reflect.setPrototypeOf()

Wait, come again?

“…such as accessing properties, invoking methods…”

“5. Reflect.get()"

This ECMAScript 6 feature will directly help us solve the biggest part of our problem

If we look at the documentation for accessing a property of an object using Reflect, we see that it accepts a target object, and the property value as a string

Semantic equivalent to property access

The get() method of the Reflect object is semantically equivalent to property access, and hence can be directly applied to our payload:

Reflect.get()

The words ‘Reflect’ and ‘get’ are also not blacklisted, which means we can use them in our payload directly.

Using this, window[‘alert’] is equivalent to Reflect.get(window, 'alert')

There is a problem here as well: Reflect.get() requires a target object to be passed as the first parameter. This object is a window object, since (as previously mentioned) alert() is a method of the global window object. The document object is also a property of the window object. This means that any payload will require the name of a blacklisted object as a parameter. However, for now, we can use the word ‘window’ as a placeholder in order to obtain the second evolution of our payload:

Reflect.get(window,'ale'+'rt')(Reflect.get(window,'docu'+'ment').domain)

The alert function to bypass validation
Payload: Evolution 2

This is the second evolution of our payload. Now, we just need to figure out how to reference the window object without using the word ‘window’, and out challenge is solved!

Phase 4: Fram(e)ing a solution

Now, we need to remove the word ‘window’. Luckily, we have a few constraints to narrow down our search:

  1. The property or method which we need must return the current window as an object
  2. The property or method should be accessed directly from the script

We know that since window is a global object, its properties can be accessed directly. This means that our property or method is a member of the window class.

We also know that it should return a window object.

This means that we need to find a property or instance of the window object, which returns the current window.

This narrows our search, and leads us to the documentation for the Window class:

Sifting through the documentation, we find that there are 4 properties which directly return a window object:

  1. self : Returns a reference to the current window. Does not work for us since the word ‘self’ is not allowed
  2. parent: Returns a reference to the parent window of the current window, which in this case would be the same window. Does not work again since the word ‘parent’ is not allowed
  3. top : Returns a reference to the topmost window, which is the current window in the challenge page. Does not work since ‘top’ is not allowed
  4. window : Self-explanatory :)

However, looking at the very bottom of the properties description in the MDN documentation, we can see something curious:

window.frames?

There is another property of the window class, which does not return a window object, but rather returns the window as an array-like object. This means we can use the frames property to directly reference the window!

window.frames!

And this is the final piece of the puzzle. Substituting window with frame we get our final payload:

Reflect.get(frames,'ale'+'rt')(Reflect.get(frames,'docu'+'ment').domain)

Payload: Final form

https://challenge-0523.intigriti.io/challenge/xss.html?xss=Reflect.get%28frames%2C%27ale%27%2B%27rt%27%29%28Reflect.get%28frames%2C%27docu%27%2B%27ment%27%29.domain%29

XSS

Conclusion

This challenge was a very fun way to understand restrictions on payloads in XSS, and work your way around them. Props to the creator Renwa and the Intigriti team for organizing this challenge!

P. S. If you want a harder challenge, try for arbitrary XSS :)

If you enjoyed the writeup, don’t forget to clap for the post! Make sure you follow Intigriti and the creator of the challenge Renwa for more cool stuff!

Thanks for reading!

--

--

Mohammed Moiz Pasha

Bug hunter | Detectify Crowdsource member | Interested in pretty much anything tech