Just show me to the solutions already!
Fair enough:
Solution #1
Solution #2 | No user-interaction
Solution #3 | An unintended solution
Solution #3.5 | A different approach to getting XSS
It’s December and this year Christmas came early! On 7/12, a new tweet ticked in from Intigriti announcing a new challenge:
Since Intigriti recently paid out €185.000 in bounties in one day they thought it would be a good idea to make us a calculator, but it seems like it can do a bit more than intended…
The rules
The solution to the challenge should meet the following requirements:
1. Should work on the latest version of Firefox or Chrome
2. Should execute the following JS:alert(document.domain)
.
3. Should be executed on this domain (challenge-1220.intigriti.io)
4. Shouldn’t be self-XSS or related to MiTM attacks
Let’s get started!
Navigating to https://challenge-1220.intigriti.io/ we’re met with a simple calculator that can do cool (but simple) calculations.
When using the calculator we can notice that some GET parameters are being set:
Time to check the source code
This seems like a good time to take a look under the hood and examine how the calculator works. The page only uses a single script called script.js
(the script can be found at the bottom of this article). Upon the page has loaded, a function called init
is called. This function then uses a custom functioncalled getQueryVariable
to get the parameters num1
, num2
and operator
from the URL. Let’s examine exactly how getQueryVariable
retrieves the parameters from the URL.
The function is passed the name of a GET
parameter to return the value of, and performs the following steps:
- Save everything after the first occurrence of ‘?’ in
location.href
as the variablesearchQueryString
- Split
searchQueryString
based on ‘&’ chars and save it in thevars
array - Split each element in
vars
based on the ‘=’ char and check if the first part matches the soughtGET
parameter - If it matches, save the second part in the variable
value
- Return
value
This means that if a GET
parameter is set more than once, only the last value is used. It also completely ignores the structure of the URL and looks at everything after the first occurrence of ‘?’.
After getting the values of the three parameters, they are passed along as arguments to the function calc
. This function combines them into an operation and calculates it. Looking at the source code ofcalc
immediately put a smile on my face seeing the use of eval
:
As can be seen, the three parameters num1
, num2
and operator
are combined with the use of template literals to a variable called operation
and then thrown into eval
! Well, that seems great, now let’s just set one of the parameters to some javascript payload and… wait. Seems like we went a bit too quick there. As can also be seen in the source code of calc
, the parameters aren’t allowed to be everything and are validated in 3 steps, before being passed along to eval
.
- A check is performed on the
operator
parameter to see if it matches it’s one of the predefined values from the arrayoperators
consisting of:["+","-","/","*","="]
- Parameter
num1
andnum2
are both tested against a regex pattern.¹ The checks are almost equal: Both parameters must only consist of alphanumeric characters except the fact thatnum1
is also allowed to contain hyphens (‘-’)². - A check is made to make sure the whole operation (all three parameters combined) doesn’t exceed a length of 20.
An initial thought
An interesting thing to note is that the operators
array contains the equal operator, which means that we are allowed to set the operator
parameter to an equal sign. This is interesting since we’re then able to do assignments, eg. setting the value of a variable.
Let’s test this! Imagine that we define the parameters as follows:num1=smth
,operator==
and num2=blabla
. This would then be combined and thrown into eval
as follows:
As shown, we’re able to do assignments! This seems very promising!
So what you can set a variable?
Well, it’s not just variables we can assign. In javascript it’s possible to override a function as follows:
thisFunction=thatFunction
Let’s try and do that with a function from script.js
and see what happens. Let’s set the function calc
to something else. What about alert
?
Hmm, did it work? Let’s open up the developer tool and check the calc
function:
Yay! We’ve successfully overridden calc
with alert
. Let’s click a random button on the calculator to call the calc
function again:
Cool! We got a beautiful alert box. Since num1
is the first argument passed along to calc
we are seeing the value ‘calc’ (remember that num1
is set to the value ‘calc’). Well, all we have to do now is to control the argument sent to alert
! How hard can it be? Well, quite hard actually.
Approaching a solution
After discovering that I could override a function, I started to look for functions where I had control over the arguments passed along. I recalled that the function getQueryVariable
ran through each GET
parameter and passed both their name and value to the function decodeURIComponent
. My first idea was to override decodeURIComponent
and set it to alert
and then have a parameter with either the name or value set to document.domain
. However, there was one problem with this approach. To override decodeURIComponent
and set it to alert
, I would need to have the following operation evaluated:
decodeURIComponent=alert
This operation consists of 24 chars… Remember step 3 in the validation in calc
? We can’t have our operation to be longer than 20 chars…
This caused me some problems for a while, since all the ideas I had, required operations longer than 20 chars.
Try harder
It’s time to try harder. Earlier we managed to pop an alert box with the value of num1
by overriding calc
. If only we where able to change the value of num1
before calc
is called again we could do something like this:
- Make the operation
num1=alert
- Set the value of
num1
todocument.domain
- Call
calc
The problem is that to change the num1
parameter, we would have to refresh the page. Right?
Let’s just not refresh
Remember how I mentioned that getQueryVariable
looks at everything after the first occurrence of ‘?’. What if we created a URL fragment…
At this point, my eyes widen since I knew I was getting on the right track. The great thing about URL fragments is that we can modify them without needing to refresh the page. This is great in our case since it means we potentially modify the value of num1
without the need of refreshing!
Let’s test if we can actually set the parameters from the fragment of the URL:
Sure enough, it works! We can now modify the fragment from the console with the following code:
location.hash = "?num1=document.domain"
And then click a random button to call calc
again:
Eh, that was not the value I expected to see. So what went wrong? Notice how the URL looks after we clicked a button. When clicking the button the string “num1=NaN” was prepended to the fragment effectively setting the value of num1
to NaN#?num1
. To fix this we need to add a ‘&’ before ‘num1’ to tell getQueryVariable
that we’re defining a new GET
parameter instead of specifying the value of the previous parameter. Let’s try with the following code instead:
location.hash = "?&num1=document.domain"
Sooo yeah. Well, we managed to alert “document.domain” but we did it a tad too literally. Let’s try and use our brain and actually alert the value of document.domain
instead of the string.
Eval to the rescue
We can simply fix our stupid problem by throwing in an extra eval. If we set calc
to eval
instead of alert
, we can then simply pass along the argument “alert(document.domain)” instead of “document.domain”. Our steps would then be:
- Make the operation
num1=eval
- Set the value of
num1
toalert(document.domain)
- Call
calc
Finally, we managed to pop the correct alert! Time to automate this!
Solution #1
Let’s try and automate our approach. To do so we need to have our own script control the value of the fragment of https://challenge-1220.intigriti.io/. Let’s spicy things up, cross our fingers, and try and load the challenge in an iframe on our own page.
Thank goodness we’re allowed to embed the page in an iframe.
Title
Let’s finally make some PoC code. Our code should do the following:
- Load the challenge page and set
calc=eval
- Change the fragment to set
num1=alert(document.domain)
Since our parent frame isn’t on the same domain as the challenge page, we run into CORS problems if we try to access location.hash
of the challenge page. But luckily, we can instead just modify the src
attribute of the iframe and as long as we only modify the fragment, the page won’t refresh.
The following code does the job:
And upon clicking a random button our XSS fires:
Voila!
Solution #2 | No user-interaction
But wouldn’t it be nice to have the XSS fire without user-interaction? Yes, it would. So let’s do that!
My thought process while working on this was simply as follows. We need to have multiple operations evaluated automatically. What could be used to trigger something? Well, isn’t that what event listeners are for? Let’s find an appropriate event. The ‘hashchange’ seems like a good fit for the job. Now let’s simply create an event listener for the ‘hashchange’ event by assigning onhashchange
. To have the calc
function be called with the GET
parameters, the init
function needs to be run so let’s make our event listener call that function!
Our steps should then be as follows:
- Set
onhashchange=init
- Set
calc=eval
- Set
num1=alert(document.domain)
The following code does the above:
Solution #3 | An unintended solution
This was actually the first solution I found. It was an unintended solution and a bit weird, but I thought I would include it. When initially looking at the getQueryVariable
function, I thought about the possibility of tricking the function into returning one value for num1
before clicking a button on the calculator and then another value after. Investigating the functions, the clear
function seemed interesting. The function was called when clicking the clear button on the calculator, and removed the three GET
parameters num1
, num2
and operator
. However, unlike getQueryVariable
which happily looked for GET
parameters in the fragment of the URL, clear
only removed the actual GET
parameters and not the values specified in the fragment. I really felt like this parsing difference between the two functions should be possible to exploit and after some time, I managed to do so.
The PoC-URL is as follows:
https://challenge-1220.intigriti.io/?num1=calc&operator=%3d&num2=eval&#?num1=alert(document.domain)
The exploit works because getQueryVariable
splits everything after the first occurrence of ‘?’ in location.href
into parameters based on the ‘&’ char. This means that when the URL above is initially processed, the last occurrence of “num1” will be viewed as a parameter with the name ?num1
which is not equal to the value “num1”. This results in the first occurrence of the parameter being used, which value is “calc” and makes the operation:
calc=eval
Then when the clear button is pressed, the actual GET
parameters are removed, and since no GET
parameters are left, the first occurrence of ‘?’ is also removed making the modified URL:
https://challenge-1220.intigriti.io/#?num1=alert(document.domain)
After the GET
parameters are removed, the init
function is called which will call the calc
function which we have overrode with eval
and the first argument will now be alert(document.domain)
completing the exploit!
Solution #3.5 | A different approach to getting XSS
As a last note, I thought I would cover a slightly different approach which can be incorporated in both solution #1 and solution #2. The idea is to set the location
parameter to an XSS payload using the javascript protocol. This can be achieved through the use of the variable searchQueryString
which is set to the content proceeding the first occurrence of ‘?’ inlocation.href
. However, we run into length problems again:
location=searchQueryString
The above is 26 chars, but we can use our fragment trick from solution #1 and solution #2 to perform multiple operations which allows us to simply use an alias:
a=searchQueryString
location=a
Finally, we just need to make sure that we insert our XSS payload in searchQueryString
in such a way that it’ll fire when setting the location.
Here’s an example of how to incorporate this approach for solution #2:
That was a lot, thanks for reading along! I hope you found this interesting and maybe learned something new!
Until next time,
[1] This tool is great for testing regular expressions: https://regexr.com/
[2] Well, thanks a lot for that red-herring Intigriti… ;)
Content of script.js
: