Hello hunters, 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.
alert(document.domain)
It’s pretty clear, all we need to do is roll up our sleeves.
Source : https://app.intigriti.com/researcher/programs/intigriti/challenge0523/detail
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 ;
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:
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.
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 :
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.
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 the “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)')
Close to the goal but it does not pass for the challenge.
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
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.
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 an anchor in the URL containing the javascript code to be executed via “javascript:”. The 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.
🥷🏽Update: My write-up has been selected and I am one of the three winners, thanks to Intigriti.
Thank you for reading me, if you have any questions do not hesitate to let me know. Happy hunting 🏹
My Twitter account : https://twitter.com/blank_cold