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
- 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
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:
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!
This is interesting for a few reasons. One, it means that we are able to execute
alert() function, sadly, does not yield any results:
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:
- An input field is the element on the page we will be interacting with
- 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’
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.
<script></script>block in the page lies the main logic for what happens when an input is passed:
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.
/<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
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.
Based on the RegEx defined in the script, we can deduce a few points:
const characters = /^[a-zA-Z,;+\\.()]+$/;
- 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.
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
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.
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.
In conclusion, the key takeaways from the analysis of the script are:
- The payload must contain only alphabets, comma(,), period(.), plus(+), semicolon (;), backslash(\) or parentheses( () )
- 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
- 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
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.
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 word
document as well.
So, our objective payload transforms from
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
- The most common way is to use the
eval()function. The eval function evaluates the string and returns the object it references:
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:
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
However, this also does not work, since the characters ‘[‘ and ‘]’ are not allowed.
Here is where a feature of ECMAScript 6 comes in:
The release included two metaprogramming features: The Proxy class and the Reflect object.
Proxyis an object that wraps around another object and can intercept or modify its behavior. However, the feature of our interest is the
According to MDN web docs,
What does this mean? According to ChatGPT
Here are the static methods of the Reflect object, linked with the documentation:
Wait, come again?
“…such as accessing properties, invoking methods…”
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
The get() method of the Reflect object is semantically equivalent to property access, and hence can be directly applied to our payload:
The words ‘Reflect’ and ‘get’ are also not blacklisted, which means we can use them in our payload directly.
window[‘alert’] is equivalent to
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:
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:
- The property or method which we need must return the current window as an object
- 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
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:
Window - Web APIs | MDN
This property indicates whether the current window is closed or not. Returns a reference to the console object which…
Sifting through the documentation, we find that there are 4 properties which directly return a window object:
self: Returns a reference to the current window. Does not work for us since the word ‘self’ is not allowed
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
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
window: Self-explanatory :)
However, looking at the very bottom of the properties description in the MDN documentation, we can see something curious:
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!
And this is the final piece of the puzzle. Substituting
frame we get our final payload:
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 :)
Thanks for reading!