XSS Intigriti challenge 0523
Published:
Let me explain how did I overcome this XSS challenge set up by the bug bounty platform Intigriti. It may be a source of inspiration for some of you during your research.
To start, let’s see the rules
- The challenge runs from the 23rd of May until the 29th of May, 11:59 PM CET (the challenge is therefore over at the time you read this)
- Should work on the latest version of Chrome and FireFox Should execute alert(document.domain)
- Should leverage a cross-site scripting vulnerability on challenge-0523.intigriti.io domain
- Shouldn’t be self-XSS or related to MiTM attacks
- Should require no user interaction
It’s pretty clear, all we need to do is roll up our sleeves.
Source: https://app.intigriti.com/researcher/programs/intigriti/challenge0523/detail
Code and constraints
The challenge page is quite minimalist: an input and a submit button supposed to pop up an alert
window containing document.domain
.
As you can imagine, this is a challenge and it won’t be as simple as submitting its alert()
function directly. There are bound to be constraints, let’s take a look at the JavaScript code:
(()=>{
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");
}
})()
The code is clear and easily understandable, the xss parameter retrieves our payload and must meet three conditions to be taken into consideration and therefore, to be concatenated to the ‘data:,’ value of the src attribute of the newly created script tag. Let’s take a closer look at these three conditions ;
1 - The payload length must be less than 100 characters
xss.length<100
2 - The “characters” constant contains a regular expression acting here as a whitelist. If our payload contains a character that is not included in the regex then the condition will not be met
const characters = /^[a-zA-Z,'+\\.()]+$/;
characters.test(xss)
The accepted characters are:
- Alphabetical
- The characters
+ , ( ) ‘ . \
3 - The “words” constant here acts as a blacklist, if any of the words from our payload is there, the condition will not be met. Bad luck, these are the most interesting JavaScript keywords!
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 one of these conditions is not met, then the character string — motivating at the beginning, frustrating at the end — “try harder” appears in the console.
Start of investigations
When I first came across Intigriti’s tweet announcing the start of the challenge, a first hint had already been revealed by the team :
They’re cool, the hint is self-explanatory: part of the secret is in the ECMA6 documentation.
You don’t know what ECMA is? Go to your favorite search engine! In the meantime, a small definition from the Mozilla documentation:
Ecma International (formally European Computer Manufacturers Association) is a non-profit organization that develops standards in computer hardware, communications, and programming languages.
On the web it is famous for being the organization which maintain the ECMA-262 specification (aka. ECMAScript) which is the core specification for the JavaScript language. Source: developer.mozilla.org
After some time reading the docs, I think I found something useful in our case, it is the Reflect object, two of its methods are interesting get and set, a getter and a setter therefore. I take a look at the blacklist and it’s encouraging: the words Reflect, get and set are not there!
How Reflect()
works?
The two methods we are interested in are get and set :
Reflect.get()
: Acts as an accessor, the first parameter is the target object, and the second parameter is the property. The method returns the value of the property in question.Reflect.set()
: Acts as a setter, the first parameter is the target object, the second parameter the property to modify (or create) and the third parameter the (new) value to assign.
Both methods have an additional optional parameter which we are not interested in here.
Ok it’s very interesting for us and it will do the job. I spare you my many trial and error and we move on to creating the payload.
Close to the goal
As you could see earlier, most of the interesting keywords are blacklisted, but whether it’s in the context of a challenge like here or in the real world on a program, no matter how robust the security is, it’s enough an oversight or negligence to create a gaping hole in the security of the platform/website.
During my research phase, I noticed that the keyword frames
was not blacklisted, which is very interesting because frames
returns window
that was initially on the list!
With the Window object, the first thing that came to my mind was to change the value of location
via the “set” method of Reflect()
.
It was a bit complicated because it was necessary to have the character colon (:) to specify the protocol (javascript:
), but not being whitelisted (regex) it was impossible to use it directly. After several attempts, I had a payload that worked but was longer than 100 characters (106!):
Reflect.set(frames,'locatio'+'n','javasc'+'ript'+origin.at('zhero'.length)+'ale'+'rt(do'+'cument.domain)')
Concatenations bypass the blacklist:
‘locatio’+’n’
,‘javasc’+’ript’
,‘ale’+’rt(do’
,‘cument.domain)’
The
at()
method is used to retrieve the character located at the index specified in the parameter and origin returns ‘https://….’, so the colon character (:) is at index 5. As the use of numbers is prohibited (whitelist), I provide a character string with a length of 5 on which I call thelength()
method so that it returns me the number that will serve as a parameter for the at() method.
Close to the goal but it does not pass for the challenge.
Winning payload
Change of strategy and end of the suspense: the window object — returned by “frames” — has the alert()
method… It looks much simpler than expected. What if we used the Reflect getter to call alert via a concatenation?
Reflect.get(frames,'aler'+'t')(Reflect.get(frames,'docum'+'ent').domain)
It works, with 72 characters!
A brief explanation of the payload
Reflect.get(frames, ‘aler’+’t’)
➜ retrieves the alert method from frames (and therefore from window)(Reflect.get(frames, ‘docum’+’ent’).domain)
➜ Parentheses are used to execute thealert
method. Opening a simple alert window is not enough, you have to executedocument.domain
in it. For this, we call -again- the getter ofReflect()
to have the “document” property of “frames” (and therefore window) then -once the property is returned- we directly access the “domain” property!
Ok the challenge is validated, it’s good. But what about this payload in the real world? You will agree, the victim does not have too much to worry about. Let’s see if we can do better for a real context.
Obsession, arbitrary XSS, and end
I managed to get an arbitrary XSS via a payload in the URL, for this I took advantage of the fact that the various filters only check the “xss” parameter and not the whole URL.
To do this, I placed a hash (anchor) in the URL containing the javascript code to be executed via javascript:
. The hash/anchor is not subject to filters, any javascript code can be executed without any limitation.
The value of the “xss” parameter contains the JS code that allows you to retrieve the value of the anchor, and assign it to the location
property of the frames object :
https://challenge-0523.intigriti.io/challenge/xss.html?xss=Reflect.set(frames%2C%27locatio%27%2B%27n%27%2CReflect.get(frames%2C%27locatio%27%2B%27n%27).hash.split(%27\\%27).pop())#\javascript:alert('Im finally free from my shackles, saying "javascript", "eval" and "document" doesn`t scare me anymore!')
Explanation of the payload
1️⃣ https://challenge-0523.intigriti.io/challenge/xss.html?xss= ➜ I think it’s clear
2️⃣ Reflect.set(frames,’locatio’+’n’
, ➜ As explained previously we indicate that we want to assign a new value to the location
property of frames
3️⃣ Reflect.get(frames,’locatio’+’n’).hash.split(‘\\’).pop())
➜ This part is the third parameter of the “set” method of Reflect()
, so it will be the value to assign. It will allow us to retrieve the value of the anchor in the URL:
We start by retrieving location
via the Reflect getter, then we access “hash” which retrieves the #
in the URL followed by the fragment identifier of the URL.
I used a backslash \
between the #
character and my payload to act as a separator. I just have to use the split
method followed by the pop
method to get the part that interests me.
4️⃣ #\javascript:alert(‘Im finally free from my shackles, saying “javascript”, “eval” and “document” doesn
t scare me anymore!’) ➜ The anchor, followed by
javascript:` and the code to execute. As explained earlier it is possible to execute anything there without character limit. The fragment is not being checked by the various filters.
It was my first Intigriti challenge, I enjoyed it. My write-up has been selected and I am one of the three winners, thanks to Intigriti.