Wacky XSS challenge write-up
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).
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.
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.
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.
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.
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.
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.
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.
The code responsible for dynamically loading the frame-analytics.js file was the following.
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.
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.
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.
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.
To evaluate the policy, the challenger could have used the CSP Evaluator tool as shown below.
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.
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.
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.
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.
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.
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.
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.
A real-life usage example is shown below.
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:
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.
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:
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.
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.
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.
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.