Goal:
- ๐ Understand OAuth account linking vulnerabilities
- ๐ฏ Exploit missing state parameter validation
- ๐ Perform CSRF attack on OAuth flow
- ๐ฅ Link victim's OAuth account to attacker's profile
- ๐ค Access admin panel and delete
carlos - ๐ Complete lab
๐ง Concept Recap
Forced OAuth Profile Linking exploits applications that don't properly validate the OAuth state parameter during account linking. When an attacker can trick a victim into completing an OAuth flow initiated by the attacker, the victim's social media account becomes linked to the attacker's application account, allowing the attacker to log in as the victim.
๐ The Vulnerability
OAuth Account Linking Flow:
Normal Account Linking Process:
User with existing account โ Wants to link social media
โ
Initiates OAuth flow
โ
Authorizes at OAuth provider
โ
Callback returns to application
โ
Social account linked to user's profile
โ
User can now login with social media
Purpose:
โโโ Convenience: Single sign-on capability
โโโ Security: Leverage trusted OAuth providers
โโโ User experience: Fewer passwords to remember
Security Mechanism (Should exist):
โโโ State parameter: Prevents CSRF attacks
โโโ Random token tied to user's session
โโโ Validated on callback to ensure same userThe Attack Flow:
VULNERABLE FLOW (Missing state validation):
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 1. Attacker initiates OAuth linking โ
โ โโโ Starts linking their social account โ
โ โโโ GET /auth?client_id=... โ
โ โโโ No state parameter! ๐จ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 2. Attacker captures OAuth callback URL โ
โ โโโ Intercepts before completing flow โ
โ โโโ URL: /oauth-linking?code=xyz789 โ
โ โโโ Drops request (preserves code) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 3. Attacker crafts malicious page โ
โ โโโ iframe pointing to /oauth-linking?code=xyz โ
โ โโโ Victim visits attacker's page โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 4. Victim's browser makes request โ
โ โโโ GET /oauth-linking?code=xyz789 โ
โ โโโ Uses victim's session cookie โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 5. Application links accounts (VULNERABLE) โ
โ โโโ No state validation! โ
โ โโโ Attacker's social account โ
โ โโโ Linked to victim's profile! ๐ฅ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ 6. Attacker logs in with social media โ
โ โโโ Uses their own social account โ
โ โโโ Now logged in as admin! ๐ฏ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโWhy This Works:
Root Cause: Missing CSRF Protection on OAuth Callback
Secure OAuth Flow (Should have):
GET /oauth-linking?code=xyz789&state=random_token_123
Server validates:
โโโ 1. Is 'state' present?
โโโ 2. Does 'state' match session?
โโโ 3. Is 'code' fresh and unused?
If all pass:
โโโ Link social account to current user โ
Vulnerable Flow (This lab):
GET /oauth-linking?code=xyz789
Server only validates:
โโโ Is 'code' valid?
Missing validations:
โโโ โ No state parameter check
โโโ โ No CSRF token validation
โโโ โ Trusts any authenticated request๐ ๏ธ Step-by-Step Attack
๐ง Step 1 โ Access Lab and Login
- ๐ Click "Access the lab"
- ๐ค Click "My account" in top-right corner
- โ๏ธ Login with blog credentials:
- Username:
wiener - Password:
peter
4. โ Successfully logged in to the blog website
What you see:
My Account Page:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Email: wiener@normal-user.net โ
โ โ
โ [Attach a social profile] โ
โ โ
โ This feature is currently in beta! โ
โ Link your social media account for โ
โ a more seamless login experience. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key observation:
โโโ "Attach a social profile" button available
โโโ This is the vulnerable feature!Note: There are two sets of credentials in this lab:
- Blog website:
wiener/peter - Social media profile:
peter.wiener/hotdog
๐ก Step 2 โ Analyze Account Linking Flow
First, complete the full OAuth linking flow to understand it:
- ๐ Click "Attach a social profile"
- ๐ You are redirected to the social media OAuth provider
- โ๏ธ Login with social media credentials:
- Username:
peter.wiener - Password:
hotdog
4. โ Authorize โ you'll be redirected back to the blog
5. ๐ Open Burp Suite โ Proxy โ HTTP history and study the flow
Look for this request in history:
GET /auth?client_id=xxxxxxxxxxxx&redirect_uri=https://YOUR-LAB-ID.web-security-academy.net/oauth-linking&response_type=code&scope=openid%20profile%20email HTTP/1.1
Host: oauth-YOUR-OAUTH-ID.oauth-server.net
Note: No 'state' parameter! ๐จ
โโโ This is the vulnerability indicator!Key finding:
โ Authorization URL has NO state parameter
โ redirect_uri sends code to /oauth-linking (not /oauth-callback!)
โ This means the callback endpoint is /oauth-linking?code=...
โ No CSRF protection โ exploitable!๐ฏ Step 3 โ Intercept and Capture the /oauth-linking Request
Now intercept the flow to steal the authorization code:
- ๐ ๏ธ Open Burp Suite โ Proxy โ Intercept
- ๐ด Turn Intercept ON
- ๐ Click "Attach a social profile" again
- ๐ Keep forwarding requests until you intercept this one:
GET /oauth-linking?code=h8Kd9fJ2mN5pQ7rS4tU6vW1xY3zA0bC8 HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=wiener_session_tokenโ ๏ธ STOP HERE! Do NOT forward this request!
- ๐ฑ๏ธ Right-click on this request โ "Copy URL"
- ๐ Paste the URL somewhere safe โ you'll need it shortly
- ๐๏ธ Click "Drop" to drop the request
- ๐ด Turn Intercept OFF
Why drop it:
Authorization code is single-use:
โโโ If we forward it โ our social account links to our account
โโโ Code gets consumed
โโโ Can no longer be used to exploit the victim!
By dropping it:
โโโ Code remains valid and unused
โโโ We'll make the victim's browser consume it
โโโ Linking our social account to their (admin) profile!The copied URL looks like:
https://YOUR-LAB-ID.web-security-academy.net/oauth-linking?code=h8Kd9fJ2mN5pQ7rS4tU6vW1xY3zA0bC8๐ Step 4 โ Craft Exploit on Exploit Server
Log out of the blog website first:
- ๐ช Click "Log out"
Access exploit server:
2. ๐ Click "Go to exploit server" button in lab
Create the exploit payload using the URL you copied:
<iframe src="https://YOUR-LAB-ID.web-security-academy.net/oauth-linking?code=h8Kd9fJ2mN5pQ7rS4tU6vW1xY3zA0bC8"></iframe>How it works:
Exploit mechanism:
โโโ Victim (admin) visits exploit page
โโโ iframe silently loads /oauth-linking?code=...
โโโ Victim's browser sends their session cookie automatically
โโโ Server receives: attacker's code + admin's session
โโโ Links attacker's social ID โ admin's account! ๐ฅ
The application is vulnerable because:
โโโ No state parameter to validate
โโโ Server blindly links whatever code arrives
โโโ To whoever's session cookie is presentPaste this in the Body field of the exploit server:
<iframe src="https://YOUR-LAB-ID.web-security-academy.net/oauth-linking?code=YOUR_AUTHORIZATION_CODE"></iframe>3. ๐พ Click "Store"
๐ Step 5 โ Deliver Exploit to Victim
- ๐ฏ Click "Deliver exploit to victim"
- โณ Wait a few seconds
What happens behind the scenes:
Victim (administrator) is logged in on the blog.
1. Victim receives the exploit link and opens it
โโโ Their browser loads the exploit page
2. iframe loads /oauth-linking?code=ATTACKER_CODE
โโโ Admin's session cookie sent automatically
โโโ Cookie: session=admin_session_token
3. Application processes the callback:
โโโ Exchanges code โ gets attacker's social profile (peter.wiener)
โโโ Links peter.wiener's social ID โ admin's account
โโโ CSRF attack complete! โ๐ Step 6 โ Log In as Administrator
- ๐ค Click "My account" on the blog
- ๐ Click "Log in with social media" option
- โ๏ธ Login with social media credentials:
- Username:
peter.wiener - Password:
hotdog
4. โ Authorize
What happens:
Social login flow:
1. OAuth provider confirms identity: peter.wiener
โโโ Returns social ID
2. Blog application looks up social ID in database
โโโ Finds: peter.wiener โ administrator's account
โโโ (Because of our exploit!)
3. Session created for administrator โ
โโโ You're logged in as admin! ๐ฏ๐ Step 7 โ Delete Carlos and Solve Lab
- ๐ Navigate to Admin panel (top navigation)
- ๐๏ธ Find carlos in the user list
- โ Click "Delete" next to carlos
- โ Lab solved!
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
Congratulations! โ
โ โ
โ You successfully exploited โ
โ forced OAuth profile linking โ
โ to access the administrator โ
โ account and deleted carlos! โ
โ โ
โ Lab: Forced OAuth profile linking โ
โ Status: SOLVED โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ๐ Complete Attack Chain
Step 1: Login to blog as wiener (wiener:peter)
โโโ Understand the account linking feature
โ
Step 2: Complete full OAuth flow once to study it
โโโ Social media credentials: peter.wiener:hotdog
โโโ Confirm /auth URL has NO state parameter ๐จ
โโโ Confirm callback goes to /oauth-linking?code=...
โ
Step 3: Turn on Burp Intercept โ Click "Attach a social profile"
โโโ Forward until you see GET /oauth-linking?code=XXXXX
โโโ Right-click โ Copy URL
โโโ DROP the request (keep code unused!)
โ
Step 4: Log out of blog
โโโ Go to exploit server
โโโ Create: <iframe src="/oauth-linking?code=XXXXX">
โโโ Store exploit
โ
Step 5: Deliver exploit to victim
โโโ Admin's browser loads iframe
โโโ Admin's session cookie sent
โโโ Admin's account links to peter.wiener's social ID โ
โ
Step 6: Click "Log in with social media"
โโโ Login with peter.wiener:hotdog
โโโ Logged in as administrator! ๐ฏ
โ
Step 7: Admin panel โ Delete carlos โ Lab solved! ๐โ๏ธ Understanding the Vulnerability
OAuth State Parameter
Purpose of state parameter:
State Parameter Fundamentals:
Definition:
โโโ Cryptographically random value
โโโ Tied to user's session
โโโ Prevents CSRF attacks on OAuth flow
How it works (SECURE):
1. Application generates random state
โโโ state = generate_random_token()
2. Store in user's session
โโโ session['oauth_state'] = state
3. Include in authorization URL
โโโ GET /auth?client_id=...&state=a7f3d9c2e5b8k1m4n6p9q2r5
4. OAuth provider returns state in callback
โโโ GET /oauth-linking?code=xyz&state=a7f3d9c2e5b8k1m4n6p9q2r5
5. Application validates state
โโโ if request_state == session['oauth_state']:
# Proceed with linking
else:
# CSRF attack detected!
abort(403)
Security guarantee:
โโโ Only the user who initiated OAuth flow
โโโ Can complete the flow
โโโ Attackers can't force victims to use attacker's codesThis lab's vulnerability:
Missing state validation:
Authorization URL:
GET /auth?client_id=xxx&redirect_uri=.../oauth-linking
โโโ No state parameter! โ
Callback URL:
GET /oauth-linking?code=xyz
โโโ No state parameter! โ
Server-side (VULNERABLE):
def oauth_linking(code):
profile = exchange_code(code)
current_user = get_current_user() # from session cookie
# โ CRITICAL FLAW: No CSRF protection!
link_profile(current_user, profile)
Attack succeeds because:
โโโ No state parameter to validate
โโโ Server trusts any authenticated request
โโโ Authorization code can come from anyone
โโโ Attacker's code + Victim's session = Account takeover!Attack Prerequisites
Required conditions:
1. Account linking feature exists
โโโ "Attach social profile" or similar
2. Missing state parameter
โโโ Authorization URL has no state
โโโ OR state not validated on callback
3. Authorization code usable in any session
โโโ Code from attacker's OAuth flow
โโโ Can be consumed by victim's session
4. Victim is authenticated
โโโ Admin must be logged in to have account to link to
5. Attacker can deliver payload to victim
โโโ Exploit server iframe
Attack fails if:
โโโ State parameter properly validated โ
โโโ Code tied to specific session โ
โโโ CSRF tokens required โ
โโโ SameSite=Strict cookie attribute โKey Indicators of Vulnerability
Vulnerability indicators:
1. Authorization URL analysis:
GET /auth?client_id=xxx&redirect_uri=.../oauth-linking&response_type=code
โโโ โ No state parameter โ VULNERABLE!
2. Callback endpoint is /oauth-linking (not /authenticate):
GET /oauth-linking?code=xxx
โโโ โ No state parameter โ VULNERABLE!
3. Code works across different sessions:
โโโ No session-binding on code โ VULNERABLE!๐ก๏ธ How to Fix (Secure Code)
Fix 1: Implement Proper State Validation
# โ
COMPLETE SECURE IMPLEMENTATION
import secrets
import time
from flask import Flask, session, request, redirect, abort
@app.route('/oauth-linking')
def initiate_oauth_linking():
# โ
Generate cryptographically secure random state
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
session['oauth_timestamp'] = time.time()
params = {
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URI,
'response_type': 'code',
'scope': 'openid profile email',
'state': state # โ
Include state!
}
return redirect(f"{OAUTH_PROVIDER}/auth?{urlencode(params)}")
@app.route('/oauth-linking')
def oauth_linking_callback():
code = request.args.get('code')
state = request.args.get('state')
# โ
VALIDATION: Check state exists and matches session
if not state or state != session.get('oauth_state'):
abort(403, 'CSRF detected: State mismatch')
# โ
Check not expired
if time.time() - session.get('oauth_timestamp', 0) > 600:
abort(400, 'OAuth session expired')
# โ
Clear one-time use state
session.pop('oauth_state', None)
session.pop('oauth_timestamp', None)
# Exchange code and link profile
token = exchange_code_for_token(code)
user_info = get_user_info(token['access_token'])
link_social_profile(get_current_user(), user_info)
return redirect('/my-account?success=account_linked')Fix 2: Additional Security Headers
# โ
SECURITY HEADERS
@app.after_request
def set_security_headers(response):
# โ
Prevent clickjacking (defeats iframe attacks)
response.headers['X-Frame-Options'] = 'DENY'
response.headers['Content-Security-Policy'] = "frame-ancestors 'none'"
return response
# Configure session cookie securely
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Lax', # Blocks cross-site iframe requests
)๐ If this helped you โ clap it up (you can clap up to 50 times!)
๐ Follow for more writeups โ dropping soon
๐ Share with your pentest team
๐ฌ Drop a comment