BugPoC XSS CTF
A few days ago, BugPoC announced another one of their great CTF challenges on Twitter. Since I have always learned a lot when solving their challenges, it was without questions that I played this one as well.
The challenge rules were simple:
- You must
- 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
The challenge domain was wacky.buggywebsite.com. When opening the domain, the Wacky TeXt Generator displays a small editor and a button as it is shown in the title picture.
As the following GIF shows, we can enter a boring text and make it whacky. Make it whacky in this context means, the website sets the source for an iframe to the endpoint
/frame.html?param=<myText> which then shows the processed text.
My final XSS exploit consists of various stages that I describe in the following sections.
Step 1: Title Escape
First, I realized that the
/frame.html endpoint not only responds with my text in its body but also copies it in the title element of the HTML head. From this first trace, I went on and escaped the title element so that I could load arbitrary content in the HTML head and body (below).
Loading content into the body is the first step to a successful XSS attack. However, in order to send this challenge to someone as reflected XSS, I had to find a way to load the
Luckily, I recently watched a LiveOverflow video in which he exactly explains this trick with the window name. The
window.name property can be used cross-domain and is persistent in the browsing context. This means that we can set
window.name to “iframe”, load a new URL with
window.location = "https://wacky.buggywebsite.com/frame.html?param=1337%3c%2ftitle%3e%3C/head%3E%3ch1%3eHello%20Content%3ch1%3e
Step 2: Load Malicious Code
With the possibility to load arbitrary content on the
script-src ‘nonce-kvduhtgrdywi’ ‘strict-dynamic’; frame-src ‘self’; object-src ‘none’;
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’?
A quick code review showed that the website indeed loads some script from a relative path
files/analytics/js/frame-analytics.js. This can be seen in line 23 in the following code snippet or, if you look closely, in the whacky text generator GIF above.
Consequently, I only had to set the base-uri to a malicious domain and respond from there with my own code, whenever someone calls the frame-analytics.js.
The picture below shows the Flexible Redirect. It takes my mock endpoint URL and returns a redirect ID. This ID can then be used to reference the redirect in various URLs.
Thus, the following line sets the base-uri:
Step 3: Integrity Check
As you can see in the code snippet from the previous section, the script tag also checks integrity using base64-encoded sha256. Since my malicious code has another hash than the origin code, my code was not executed. Thus, I had to bypass the integrity check.
The integrity check with sha256 is standard and valid so there is no way to forge a file with the same sha256 hash. However, there is a hint from the challenge authors to show you where to look further.
As shown in the aforementioned code snippet, the integrity check uses the value of the variable
fileIntegrity.value. This value is set a few lines earlier with
window.fileIntegrity are the same object.
The hint here is that
window.fileIntegrity is only set, when it is not already existent. However, it cannot be already existent in the original code since this part is only executed once. Thus, this is unusual and I guess we must find a way to set the object before the code is executed.
document.getElementById('theIframe'). When we now create a DOM element that has the id
fileIntegrity and an attribute
For this purpose, I used a button element, since it contains a value attribute innately. I created it with the first-mentioned vulnerability in which I escape the title element and load arbitrary HTML content. As a result, the integrity check is valid.
<button id="fileIntegrity" name="fileIntegrity" type="submit" value="zhPJ/x4SM8T7tGc4VA8FonTCCb8dogeYrmjRZYzCbaI="></button>
sandbox(also given in the code snippet above). Sandbox is a mode for iframes in which they are restricted in their privileges. To execute our alert, for instance, the sandbox attribute additionally needs the value
allow-modals. However, only
allow-same-origin were set.
Searching on the internet, the MDN web docs tell us something interesting:
allow-same-origin: If this token is not used, the resource is treated as being from a special origin that always fails the same-origin policy.
allow-scripts: Lets the resource run scripts (but not create popup windows).
When the embedded document has the same origin as the embedding page, it is strongly discouraged to use both
allow-same-origin, as that lets the embedded document remove the
sandboxattribute — making it no more secure than not using the
sandboxattribute at all.
Basically, we can use
window.parent to access the main document and manipulate DOM elements. Thus, my idea was to steal the nonce tag from a script element, use it to create a new script element, append the script element to the parent document and eventually let it execute
Let’s summarize the attack chain:
- Set the window name and window location.
- Escape the title element to set the base-uri and a button element for the following integrity check.
- The malicious code escapes the iframe sandbox, steals the nonce, and creates, using the nonce, its own script element to execute the alert.
This was, again, a challenge that I really enjoyed. Thank you BugPoC and keep up the great work!