Claude Skill

hunt-session

Hunt Session Management vulnerabilities — session fixation (no regeneration on login), insufficient invalidation on logout / password-change / email-change, predictable or low-entropy session IDs, JWT-as-session with no exp/revocation, refresh-token rotation/reuse-detection gaps,

LLM Mart · 0 points · 15 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download elementalsouls-claude-bughunter-skills_hunt-session-ab8bde4.zip · 8 KB
Part of elementalsouls/claude-bughunter — 83 skills

Install

skills CLI npx skills add https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-session
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install elementalsouls-claude-bughunter@llmmart
Git git clone https://github.com/elementalsouls/Claude-BugHunter.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole elementalsouls/claude-bughunter collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Autonomous Testing Priority

Missing HttpOnly on cookies is auto-detected — focus your active testing on lifecycle invalidation (higher impact).

Pattern 1 — Session survives logout (most common high-value finding):

  1. Login and note the session token/cookie value
  2. Call the logout endpoint (/logout, POST /api/logout, etc.)
  3. Try to use the OLD session token to access a protected resource (/api/me, /dashboard, /account)
  4. If 200 with user data → session not invalidated on logout = ATO persistence

Pattern 2 — Session not regenerated on login (session fixation):

  1. GET any page to receive a pre-authentication session token/cookie
  2. POST valid credentials to the login endpoint
  3. Compare the session token BEFORE and AFTER login
  4. If the token is unchanged → session fixation vulnerability

Pattern 3 — Session survives password change:

  1. Login → record session A value
  2. Change the password via the account settings endpoint
  3. Replay session A on a protected endpoint
  4. If 200 → token not rotated on credential change = persistent ATO (critical chain when combined with XSS/cookie theft)

Content-type: Login and session endpoints vary — use application/json for REST APIs, application/x-www-form-urlencoded for traditional web forms. Try both if the first returns an unexpected response.

Proof: A protected-resource 200 response (with user data) using a session token that should have been invalidated confirms the finding.


HUNT-SESSION — Session Management

Crown Jewel Targets

Session fixation leading to admin hijack = Critical. Session surviving a password change = High-to-Critical (persistent ATO from a stolen cookie that the victim believes they revoked by resetting their password).

Highest-value chains:

  • Session fixation — server accepts a session ID set by the client and does NOT regenerate it on login → attacker pre-plants an ID, victim authenticates, attacker rides the now-authenticated session → persistent ATO.
  • No invalidation on logout — old token still works after /logout → theft window never closes.
  • No invalidation on password / email change — a stolen session survives the victim's "I think I was hacked, let me reset" → persistent ATO. This is the single highest-paid session bug class.
  • Refresh-token reuse without rotation-detection — a leaked refresh token mints fresh access tokens forever; no reuse-detection means the legitimate user's later refresh does NOT revoke the attacker's branch.
  • Predictable / low-entropy session ID — sequential, timestamp- or userId-derived IDs → brute-force or compute other users' sessions.
  • JWT-as-session with no exp / no revocation list — stolen JWT = permanent access; logout is cosmetic.

Grounding — patterns that shaped each phase

No invented CVE/report IDs below. These are the named, publicly-documented patterns this skill encodes:

  • Session fixation, login-CSRF, no-regeneration-on-auth — OWASP WSTG-SESS-03 / WSTG-SESS-01; the classic ACROS / Mitja Kolšek session-fixation paper. Highest-impact variant: fixing the session of an SSO/admin user.
  • SameSite=Lax sibling-subdomain CSRF reaching session state — Argo CD CVE-2024-22424 (Lax cookies sent on top-level cross-site navigations from a sibling subdomain). Use this when a session cookie relies on SameSite=Lax as its only CSRF defence.
  • Refresh-token rotation & automatic reuse-detection — the Auth0/IETF OAuth-Security-BCP model: a rotated refresh token, if replayed, must invalidate the entire token family. Absence = the core bug to prove.
  • Device Bound Session Credentials (DBSC) — the W3C/Chrome DBSC draft binds a session to a TPM/device key. Test the downgrade: does the server still accept a non-bound cookie when the DBSC challenge is stripped?
  • Cookie attribute hardening — OWASP WSTG-SESS-02; __Host-/__Secure- prefixes per RFC 6265bis. Missing HttpOnly is only a finding when a real XSS/DOM sink exists (chain with hunt-xss/hunt-dom).
  • Entropy — NIST SP 800-63B requires ≥64 bits of entropy in a session identifier. Treat anything decodable to a counter/timestamp/userId as a finding regardless of length.

Cross-refs: ATO chaining → hunt-ato; JWT alg/kid tampering → hunt-api-misconfig; OAuth code/state flaws → hunt-oauth; CSRF mechanics → hunt-csrf; cookie-theft sinks → hunt-xss / hunt-dom.


Attack Surface Signals

Set-Cookie: session=...            # name varies: sid, JSESSIONID, connect.sid,
                                   # PHPSESSID, ASP.NET_SessionId, laravel_session, _csrf
/login /logout /api/login /oauth/token
/auth/refresh /api/token/refresh   # refresh-token rotation surface
/account/change-password /settings/email
?sid= ?session= in URL             # session-in-URL → leaks via Referer/logs (finding)
# Header signals worth flagging immediately:
Set-Cookie: session=abc; Path=/                 # no HttpOnly/Secure/SameSite
Set-Cookie: session=abc; SameSite=None          # None without Secure = rejected by modern browsers, but flag
Set-Cookie: __Host-sess=...; Secure; Path=/     # GOOD — hard to fixate
Sec-Session-Registration: ...                   # DBSC in play → test downgrade

Step-by-Step Hunting Methodology

Two-session rule. Every invalidation/fixation claim is proven with TWO concrete sessions captured by a real flow — attacker A and victim B — never with hardcoded placeholder strings. Helpers below capture real cookies from curl's Netscape jar.

TARGET=target.com
JAR_A=$(mktemp); JAR_B=$(mktemp)

# Robust session-cookie extractor: handles #HttpOnly_ prefix lines and any
# cookie name (sid/JSESSIONID/connect.sid/PHPSESSID/...). Prints name=value.
get_cookie () {  # $1=jar  $2=name-regex (default: common session names)
  local jar="$1" re="${2:-session|sid|sess|JSESSIONID|connect\.sid|PHPSESSID|laravel_session}"
  awk -v re="$re" '
    /^#HttpOnly_/ { sub(/^#HttpOnly_/,""); }   # strip jar HttpOnly marker
    /^#/ { next }                              # skip remaining comments
    NF>=7 && $6 ~ re { print $6"="$7 }         # field6=name field7=value
  ' "$jar" | tail -1
}

Phase 1 — Session Fixation (regeneration-on-login)

# Step 1: grab a pre-auth session the SERVER hands an anonymous client.
curl -s -L -c "$JAR_A" "https://$TARGET/login" -o /dev/null
PRE=$(get_cookie "$JAR_A"); echo "pre-auth: $PRE"

# Step 1b (stronger): can we FORCE an arbitrary ID? attacker-chosen value.
FIX="session=AAAAdeadbeefAAAA"

# Step 2: authenticate while CARRYING the pre-auth/forced cookie (reuse same jar).
curl -s -L -c "$JAR_A" -b "$JAR_A" -X POST "https://$TARGET/login" \
  -d "username=attacker@example.com&password=CorrectHorse1" -o /dev/null
POST=$(get_cookie "$JAR_A"); echo "post-auth: $POST"

# DECISION:
#  - If $POST == $PRE (value unchanged across the auth boundary) AND that value
#    now returns authenticated data → FIXATION. The server reused the anon ID.
#  - If the forced $FIX value is accepted and authenticates → CRITICAL fixation
#    (attacker controls the ID; no email/XSS needed to plant it).
AUTH=$(curl -s -L -b "$JAR_A" "https://$TARGET/api/me")
echo "$AUTH" | head -c 200

FP guard: a value change is not automatically safe — some apps rotate the readable cookie but keep a stable server-side session keyed by a second cookie. Diff the FULL Set-Cookie set and confirm the old value is genuinely dead (Phase 2). Also confirm /api/me returns your identity, not a generic 200/landing page.

Phase 2 — Invalidation on Logout

# A logs in for real (fresh jar), capture A's live session.
curl -s -L -c "$JAR_A" -X POST "https://$TARGET/api/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"attacker@example.com","password":"CorrectHorse1"}' -o /dev/null
A=$(get_cookie "$JAR_A"); echo "A=$A"

# Baseline: what does an authenticated /api/me look like for A? (capture body, not just code)
BEFORE=$(curl -s -L -b "$JAR_A" "https://$TARGET/api/me")

# Logout A.
curl -s -L -b "$JAR_A" -X POST "https://$TARGET/api/logout" -o /dev/null

# Replay A's OLD cookie value explicitly (do NOT reuse the jar — logout may have
# overwritten it). Compare body + code against the authenticated baseline.
AFTER=$(curl -s -L -H "Cookie: $A" "https://$TARGET/api/me" -w '\n[%{http_code}]')
echo "AFTER: $AFTER"

FP discipline (mandatory):

  • Don't trust the status code. A cached/edge 200 or a generic SPA shell returns 200 for everyone. Body-diff AFTER against BEFORE — the finding is only real if AFTER still contains A's unique identity marker (email, user-id, CSRF token, account name).
  • Confirm with a negative control: a random/garbage cookie value must NOT return the same authenticated body. If garbage also yields 200 with user data, the endpoint isn't session-gated and there's no finding here.
  • Re-test after a short delay and from a different IP — some servers lazily expire on next access or pin sessions to IP.

Phase 3 — Invalidation on Password / Email Change (persistent-ATO core)

# This is the real two-session flow. A = attacker holding a stolen/old session.
# B = the victim who changes their password believing it revokes access.
# (In a real engagement A is a session you legitimately captured for a TEST account
#  that you also control as B — never use a real third party.)

# 1) Log the TEST account in as session A, capture it.
curl -s -L -c "$JAR_A" -X POST "https://$TARGET/api/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"victim@example.com","password":"OldPass!1"}' -o /dev/null
SESSION_A=$(get_cookie "$JAR_A"); echo "SESSION_A=$SESSION_A"
BEFORE=$(curl -s -L -H "Cookie: $SESSION_A" "https://$TARGET/api/profile")

# 2) Log the SAME account in as session B (separate jar = "the victim's browser").
curl -s -L -c "$JAR_B" -X POST "https://$TARGET/api/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"victim@example.com","password":"OldPass!1"}' -o /dev/null

# 3) Victim (session B) changes the password.
curl -s -L -b "$JAR_B" -X POST "https://$TARGET/api/change-password" \
  -H 'Content-Type: application/json' \
  -d '{"old_password":"OldPass!1","new_password":"BrandNew!2"}' -o /dev/null

# 4) THE TEST: replay the OLD SESSION_A captured in step 1.
AFTER=$(curl -s -L -H "Cookie: $SESSION_A" "https://$TARGET/api/profile" -w '\n[%{http_code}]')
echo "AFTER pw-change: $AFTER"

Decision + FP discipline:

  • Finding is confirmed only if AFTER returns 200 and the body still carries the account's unique data (body-diff vs BEFORE). A bare 200 on a public/SPA route is not proof.
  • Run the garbage-cookie negative control again to prove the endpoint is session-gated.
  • Repeat the identical flow for email-change (/settings/email) and for logout-all-devices — apps frequently invalidate the acting session (B) but not sibling sessions (A). That sibling-survival is the exact persistent-ATO primitive hunt-ato chains.
  • Severity gate: if the change-password endpoint also lacks a current-password / MFA step-up (per hunt-mfa-bypass), A can pivot from read-only to full takeover → escalate.

Phase 4 — Cookie Attribute Analysis

curl -sI -L "https://$TARGET/" | grep -i '^set-cookie'
  • HttpOnly missing → cookie reachable via document.cookie. Only a finding chained to a real XSS/DOM sink (hunt-xss/hunt-dom) — note it, don't report standalone as High.
  • Secure missing → cookie sent over cleartext HTTP; pair with hunt-tls-network (downgrade/HSTS-gap) for a network-attacker chain.
  • SameSite missing/None → CSRF reachability; SameSite=Lax is bypassable via sibling-subdomain top-level navigation (Argo CD CVE-2024-22424 class) → hand to hunt-csrf.
  • __Host- / __Secure- prefix absent → the session can be overwritten/fixated from a subdomain or non-secure context; its presence largely kills cookie-fixation, so flag the absence as the precondition for Phase 1.

Phase 5 — Session-ID Entropy

# Collect a LARGE sample (200+) of freshly-issued IDs. -L is required: a 302
# /login often sets the cookie on the redirect target, not the first response.
N=200; SAMP=$(mktemp)
for i in $(seq 1 $N); do
  J=$(mktemp)
  curl -s -L -c "$J" "https://$TARGET/login" -o /dev/null
  get_cookie "$J" | cut -d= -f2- >> "$SAMP"
  rm -f "$J"
done
sort "$SAMP" | uniq -d | head            # duplicates = catastrophic (re-use)
awk '{print length($0)}' "$SAMP" | sort -n | uniq -c   # length distribution

Then analyse, don't eyeball:

  • Sequential / monotonic — sort -n the decoded values; a steady +1/+N delta = predictable.
  • Decodable structure — base64 -d / hex-decode each ID and look for embedded userId, unix timestamps, or PIDs.
  • Bit entropy — feed the raw bytes to ent or dieharder; NIST SP 800-63B wants ≥64 bits. 10 samples is far too few to claim anything — gather hundreds.
  • FP guard: a long random-looking token is not proof of strength; only structural decode + a large-sample entropy estimate is. Conversely a short token with high per-char entropy may still be fine — measure, don't count characters.

Phase 6 — JWT-as-Session

JWT="eyJ..."        # captured from Authorization: Bearer or a cookie
# Decode header + payload safely (base64url padding fix).
b64url(){ local s="${1//-/+}"; s="${s//_//}"; printf '%s' "$s===" | base64 -d 2>/dev/null; }
b64url "$(cut -d. -f1 <<<"$JWT")" | jq .   # header: alg, kid
b64url "$(cut -d. -f2 <<<"$JWT")" | jq .   # claims: exp, iat, sub, jti
  • exp missing or years out → no expiry. jti missing → server cannot maintain a revocation list → logout can't truly revoke.
  • Revocation test: logout, then replay the same JWT against /api/me. If it still returns the user → tokens are not server-revocable; this is the JWT-session persistence finding. Body-diff to avoid a cached 200.
  • Tampering (alg/kid/key-confusion) is owned by hunt-api-misconfig — hand off jwt_tool $JWT -T / -X a there rather than duplicating it.

Phase 7 — Refresh-Token Rotation & Reuse-Detection

# 1) Obtain a refresh token (login or /oauth/token), then rotate it once.
RT1=$(curl -s -L -X POST "https://$TARGET/api/login" \
  -H 'Content-Type: application/json' \
  -d '{"email":"victim@example.com","password":"OldPass!1"}' | jq -r '.refresh_token')

# 2) Use RT1 to mint a new access token — server SHOULD return a rotated RT2.
R2=$(curl -s -L -X POST "https://$TARGET/auth/refresh" \
  -H 'Content-Type: application/json' -d "{\"refresh_token\":\"$RT1\"}")
RT2=$(jq -r '.refresh_token' <<<"$R2"); echo "rotated? RT1!=RT2 -> $([ "$RT1" != "$RT2" ] && echo yes || echo NO-ROTATION)"

# 3) REUSE-DETECTION test: replay the OLD RT1 again (simulating the leaked token).
REPLAY=$(curl -s -L -X POST "https://$TARGET/auth/refresh" \
  -H 'Content-Type: application/json' -d "{\"refresh_token\":\"$RT1\"}" -w '\n[%{http_code}]')
echo "RT1 replay: $REPLAY"

# 4) Then confirm RT2 was KILLED by the replay (correct BCP behaviour invalidates
#    the whole family). If RT2 still works after RT1 was replayed → no family-revocation.
curl -s -L -X POST "https://$TARGET/auth/refresh" \
  -H 'Content-Type: application/json' -d "{\"refresh_token\":\"$RT2\"}" -w '\n[%{http_code}]'

Findings: no rotation (RT1==RT2) = a long-lived stealable credential; rotation without reuse-detection (RT1 replay still mints tokens, or RT2 survives the replay) = the leaked-token-persistence bug per the OAuth Security BCP. OOB note: if you suspect a leaked RT via SSRF/log/JS-bundle, confirm the token's reach with hunt-ssrf/hunt-source-leak, not by guessing.

Phase 8 — OAuth/SSO Session Linkage & DBSC Downgrade

# SSO linkage: after IdP callback, is the app session bound to the IdP session?
#  - Log out at the IdP only; replay the app session cookie. Still 200 with user
#    data → app session outlives the IdP session (single-logout gap).
# DBSC downgrade: if responses carry Sec-Session-Registration / Sec-Session-Id,
#  strip the device-bound proof header and replay the plain cookie:
curl -s -L -H "Cookie: $A" "https://$TARGET/api/me" -w '\n[%{http_code}]'
#  If the plain (non-bound) cookie is still accepted → device-binding is advisory,
#  not enforced → a stolen cookie defeats DBSC entirely.

Hand OAuth state/redirect_uri/code-injection to hunt-oauth; this phase only covers the session-layer binding.


Chain Table

Session finding Chain to Impact
Session fixation (forced __Host--less cookie) Trick admin/SSO user into authenticating on planted ID Admin session takeover (Critical)
No logout/password-change invalidation hunt-xss/hunt-dom cookie theft → replay surviving session Persistent ATO past victim's reset
Refresh token, no reuse-detection Leaked RT (SSRF/log/bundle) → infinite access-token minting Persistent ATO, survives password change
SameSite=Lax only Sibling-subdomain top-level nav (CVE-2024-22424 class) → CSRF State change / login-CSRF → fixation
JWT no exp/jti Stolen token, no server revocation Permanent access
DBSC downgrade accepted Steal plain cookie despite device-binding Defeats the only theft mitigation
Predictable ID Compute/brute another user's session Cross-user ATO

Validation (house FP discipline)

Before claiming ANY session finding:

  • Two real sessions, not placeholders — every fixation/invalidation claim uses A and B captured by the curl flows above.
  • Body-diff, never status-only — a 200 means nothing without the account's unique identity marker present in the body, diffed against the authenticated baseline.
  • Negative control — a garbage/random cookie must FAIL where your "surviving" cookie succeeds; otherwise the endpoint isn't session-gated and it's a non-finding.
  • Cache/edge check — re-request with a cache-buster and from a second IP; rule out an edge-cached or IP-pinned 200.
  • OOB for theft chains — when the impact depends on exfiltrating a cookie/token (XSS, SSRF, log leak), confirm receipt out-of-band (Collaborator) rather than asserting it.
  • Static-vs-state — HttpOnly/Secure/SameSite absence is a policy observation; only report as High once paired with a real exploit primitive (XSS, network-MITM, CSRF). Standalone attribute gaps are Low/Informational.

Severity:

  • Session fixation → admin/SSO takeover: Critical
  • No invalidation on password/email change, or refresh-token reuse without detection: High → Critical (escalate if MFA/step-up also absent)
  • Predictable/duplicate session ID: High
  • No invalidation on logout: Medium → High (depends on theft vector)
  • Missing HttpOnly/SameSite standalone: Low/Informational until chained
Files (claude-bughunter)
  • SKILL.md 19.4 KB
    ---
    name: hunt-session
    description: "Hunt Session Management vulnerabilities — session fixation (no regeneration on login), insufficient invalidation on logout / password-change / email-change, predictable or low-entropy session IDs, JWT-as-session with no exp/revocation, refresh-token rotation/reuse-detection gaps, OAuth/SSO session linkage, device-bound-session (DBSC) downgrade, and cookie attribute issues (Secure/HttpOnly/SameSite/__Host-). Validate with TWO real sessions (attacker A + victim B), body-diff every 200, and OOB confirmation for theft chains. Medium to Critical (fixation→admin hijack, no-invalidation→persistent ATO)."
    sources: hackerone_public, portswigger_research, owasp_wstg
    report_count: 18
    ---
    
    ## Autonomous Testing Priority
    
    **Missing HttpOnly on cookies is auto-detected — focus your active testing on lifecycle invalidation (higher impact).**
    
    **Pattern 1 — Session survives logout (most common high-value finding):**
    1. Login and note the session token/cookie value
    2. Call the logout endpoint (`/logout`, `POST /api/logout`, etc.)
    3. Try to use the OLD session token to access a protected resource (`/api/me`, `/dashboard`, `/account`)
    4. If 200 with user data → session not invalidated on logout = ATO persistence
    
    **Pattern 2 — Session not regenerated on login (session fixation):**
    1. GET any page to receive a pre-authentication session token/cookie
    2. POST valid credentials to the login endpoint
    3. Compare the session token BEFORE and AFTER login
    4. If the token is unchanged → session fixation vulnerability
    
    **Pattern 3 — Session survives password change:**
    1. Login → record session A value
    2. Change the password via the account settings endpoint
    3. Replay session A on a protected endpoint
    4. If 200 → token not rotated on credential change = persistent ATO (critical chain when combined with XSS/cookie theft)
    
    **Content-type:** Login and session endpoints vary — use `application/json` for REST APIs, `application/x-www-form-urlencoded` for traditional web forms. Try both if the first returns an unexpected response.
    
    **Proof:** A protected-resource 200 response (with user data) using a session token that should have been invalidated confirms the finding.
    
    ---
    
    # HUNT-SESSION — Session Management
    
    ## Crown Jewel Targets
    
    Session fixation leading to admin hijack = Critical. Session surviving a password change = High-to-Critical (persistent ATO from a stolen cookie that the victim believes they revoked by resetting their password).
    
    **Highest-value chains:**
    - **Session fixation** — server accepts a session ID set by the client and does NOT regenerate it on login → attacker pre-plants an ID, victim authenticates, attacker rides the now-authenticated session → persistent ATO.
    - **No invalidation on logout** — old token still works after `/logout` → theft window never closes.
    - **No invalidation on password / email change** — a stolen session survives the victim's "I think I was hacked, let me reset" → persistent ATO. This is the single highest-paid session bug class.
    - **Refresh-token reuse without rotation-detection** — a leaked refresh token mints fresh access tokens forever; no reuse-detection means the legitimate user's later refresh does NOT revoke the attacker's branch.
    - **Predictable / low-entropy session ID** — sequential, timestamp- or userId-derived IDs → brute-force or compute other users' sessions.
    - **JWT-as-session with no `exp` / no revocation list** — stolen JWT = permanent access; logout is cosmetic.
    
    ---
    
    ## Grounding — patterns that shaped each phase
    
    No invented CVE/report IDs below. These are the *named, publicly-documented* patterns this skill encodes:
    
    - **Session fixation, login-CSRF, no-regeneration-on-auth** — OWASP WSTG-SESS-03 / WSTG-SESS-01; the classic ACROS / Mitja Kolšek session-fixation paper. Highest-impact variant: fixing the session of an SSO/admin user.
    - **SameSite=Lax sibling-subdomain CSRF reaching session state** — Argo CD **CVE-2024-22424** (Lax cookies sent on top-level cross-site navigations from a sibling subdomain). Use this when a session cookie relies on `SameSite=Lax` as its only CSRF defence.
    - **Refresh-token rotation & automatic reuse-detection** — the Auth0/IETF OAuth-Security-BCP model: a rotated refresh token, if replayed, must invalidate the *entire token family*. Absence = the core bug to prove.
    - **Device Bound Session Credentials (DBSC)** — the W3C/Chrome DBSC draft binds a session to a TPM/device key. Test the *downgrade*: does the server still accept a non-bound cookie when the DBSC challenge is stripped?
    - **Cookie attribute hardening** — OWASP WSTG-SESS-02; `__Host-`/`__Secure-` prefixes per RFC 6265bis. Missing `HttpOnly` is only a finding when a real XSS/DOM sink exists (chain with `hunt-xss`/`hunt-dom`).
    - **Entropy** — NIST SP 800-63B requires ≥64 bits of entropy in a session identifier. Treat anything decodable to a counter/timestamp/userId as a finding regardless of length.
    
    Cross-refs: ATO chaining → `hunt-ato`; JWT alg/kid tampering → `hunt-api-misconfig`; OAuth code/state flaws → `hunt-oauth`; CSRF mechanics → `hunt-csrf`; cookie-theft sinks → `hunt-xss` / `hunt-dom`.
    
    ---
    
    ## Attack Surface Signals
    
    ```
    Set-Cookie: session=...            # name varies: sid, JSESSIONID, connect.sid,
                                       # PHPSESSID, ASP.NET_SessionId, laravel_session, _csrf
    /login /logout /api/login /oauth/token
    /auth/refresh /api/token/refresh   # refresh-token rotation surface
    /account/change-password /settings/email
    ?sid= ?session= in URL             # session-in-URL → leaks via Referer/logs (finding)
    ```
    ```
    # Header signals worth flagging immediately:
    Set-Cookie: session=abc; Path=/                 # no HttpOnly/Secure/SameSite
    Set-Cookie: session=abc; SameSite=None          # None without Secure = rejected by modern browsers, but flag
    Set-Cookie: __Host-sess=...; Secure; Path=/     # GOOD — hard to fixate
    Sec-Session-Registration: ...                   # DBSC in play → test downgrade
    ```
    
    ---
    
    ## Step-by-Step Hunting Methodology
    
    > **Two-session rule.** Every invalidation/fixation claim is proven with TWO concrete sessions captured by a real flow — attacker **A** and victim **B** — never with hardcoded placeholder strings. Helpers below capture real cookies from `curl`'s Netscape jar.
    
    ```bash
    TARGET=target.com
    JAR_A=$(mktemp); JAR_B=$(mktemp)
    
    # Robust session-cookie extractor: handles #HttpOnly_ prefix lines and any
    # cookie name (sid/JSESSIONID/connect.sid/PHPSESSID/...). Prints name=value.
    get_cookie () {  # $1=jar  $2=name-regex (default: common session names)
      local jar="$1" re="${2:-session|sid|sess|JSESSIONID|connect\.sid|PHPSESSID|laravel_session}"
      awk -v re="$re" '
        /^#HttpOnly_/ { sub(/^#HttpOnly_/,""); }   # strip jar HttpOnly marker
        /^#/ { next }                              # skip remaining comments
        NF>=7 && $6 ~ re { print $6"="$7 }         # field6=name field7=value
      ' "$jar" | tail -1
    }
    ```
    
    ### Phase 1 — Session Fixation (regeneration-on-login)
    ```bash
    # Step 1: grab a pre-auth session the SERVER hands an anonymous client.
    curl -s -L -c "$JAR_A" "https://$TARGET/login" -o /dev/null
    PRE=$(get_cookie "$JAR_A"); echo "pre-auth: $PRE"
    
    # Step 1b (stronger): can we FORCE an arbitrary ID? attacker-chosen value.
    FIX="session=AAAAdeadbeefAAAA"
    
    # Step 2: authenticate while CARRYING the pre-auth/forced cookie (reuse same jar).
    curl -s -L -c "$JAR_A" -b "$JAR_A" -X POST "https://$TARGET/login" \
      -d "username=attacker@example.com&password=CorrectHorse1" -o /dev/null
    POST=$(get_cookie "$JAR_A"); echo "post-auth: $POST"
    
    # DECISION:
    #  - If $POST == $PRE (value unchanged across the auth boundary) AND that value
    #    now returns authenticated data → FIXATION. The server reused the anon ID.
    #  - If the forced $FIX value is accepted and authenticates → CRITICAL fixation
    #    (attacker controls the ID; no email/XSS needed to plant it).
    AUTH=$(curl -s -L -b "$JAR_A" "https://$TARGET/api/me")
    echo "$AUTH" | head -c 200
    ```
    **FP guard:** a value *change* is not automatically safe — some apps rotate the readable cookie but keep a stable server-side session keyed by a second cookie. Diff the FULL `Set-Cookie` set and confirm the *old* value is genuinely dead (Phase 2). Also confirm `/api/me` returns *your* identity, not a generic 200/landing page.
    
    ### Phase 2 — Invalidation on Logout
    ```bash
    # A logs in for real (fresh jar), capture A's live session.
    curl -s -L -c "$JAR_A" -X POST "https://$TARGET/api/login" \
      -H 'Content-Type: application/json' \
      -d '{"email":"attacker@example.com","password":"CorrectHorse1"}' -o /dev/null
    A=$(get_cookie "$JAR_A"); echo "A=$A"
    
    # Baseline: what does an authenticated /api/me look like for A? (capture body, not just code)
    BEFORE=$(curl -s -L -b "$JAR_A" "https://$TARGET/api/me")
    
    # Logout A.
    curl -s -L -b "$JAR_A" -X POST "https://$TARGET/api/logout" -o /dev/null
    
    # Replay A's OLD cookie value explicitly (do NOT reuse the jar — logout may have
    # overwritten it). Compare body + code against the authenticated baseline.
    AFTER=$(curl -s -L -H "Cookie: $A" "https://$TARGET/api/me" -w '\n[%{http_code}]')
    echo "AFTER: $AFTER"
    ```
    **FP discipline (mandatory):**
    - Don't trust the status code. A cached/edge 200 or a generic SPA shell returns 200 for everyone. **Body-diff** `AFTER` against `BEFORE` — the finding is only real if `AFTER` still contains A's *unique identity marker* (email, user-id, CSRF token, account name).
    - Confirm with a **negative control**: a random/garbage cookie value must NOT return the same authenticated body. If garbage also yields 200 with user data, the endpoint isn't session-gated and there's no finding here.
    - Re-test after a **short delay** and from a **different IP** — some servers lazily expire on next access or pin sessions to IP.
    
    ### Phase 3 — Invalidation on Password / Email Change (persistent-ATO core)
    ```bash
    # This is the real two-session flow. A = attacker holding a stolen/old session.
    # B = the victim who changes their password believing it revokes access.
    # (In a real engagement A is a session you legitimately captured for a TEST account
    #  that you also control as B — never use a real third party.)
    
    # 1) Log the TEST account in as session A, capture it.
    curl -s -L -c "$JAR_A" -X POST "https://$TARGET/api/login" \
      -H 'Content-Type: application/json' \
      -d '{"email":"victim@example.com","password":"OldPass!1"}' -o /dev/null
    SESSION_A=$(get_cookie "$JAR_A"); echo "SESSION_A=$SESSION_A"
    BEFORE=$(curl -s -L -H "Cookie: $SESSION_A" "https://$TARGET/api/profile")
    
    # 2) Log the SAME account in as session B (separate jar = "the victim's browser").
    curl -s -L -c "$JAR_B" -X POST "https://$TARGET/api/login" \
      -H 'Content-Type: application/json' \
      -d '{"email":"victim@example.com","password":"OldPass!1"}' -o /dev/null
    
    # 3) Victim (session B) changes the password.
    curl -s -L -b "$JAR_B" -X POST "https://$TARGET/api/change-password" \
      -H 'Content-Type: application/json' \
      -d '{"old_password":"OldPass!1","new_password":"BrandNew!2"}' -o /dev/null
    
    # 4) THE TEST: replay the OLD SESSION_A captured in step 1.
    AFTER=$(curl -s -L -H "Cookie: $SESSION_A" "https://$TARGET/api/profile" -w '\n[%{http_code}]')
    echo "AFTER pw-change: $AFTER"
    ```
    **Decision + FP discipline:**
    - Finding is confirmed only if `AFTER` returns 200 **and** the body still carries the account's unique data (body-diff vs `BEFORE`). A bare 200 on a public/SPA route is not proof.
    - Run the **garbage-cookie negative control** again to prove the endpoint is session-gated.
    - Repeat the identical flow for **email-change** (`/settings/email`) and for **logout-all-devices** — apps frequently invalidate the *acting* session (B) but not *sibling* sessions (A). That sibling-survival is the exact persistent-ATO primitive `hunt-ato` chains.
    - **Severity gate:** if the change-password endpoint also lacks a current-password / MFA step-up (per `hunt-mfa-bypass`), A can pivot from read-only to full takeover → escalate.
    
    ### Phase 4 — Cookie Attribute Analysis
    ```bash
    curl -sI -L "https://$TARGET/" | grep -i '^set-cookie'
    ```
    - **HttpOnly** missing → cookie reachable via `document.cookie`. Only a finding **chained to a real XSS/DOM sink** (`hunt-xss`/`hunt-dom`) — note it, don't report standalone as High.
    - **Secure** missing → cookie sent over cleartext HTTP; pair with `hunt-tls-network` (downgrade/HSTS-gap) for a network-attacker chain.
    - **SameSite** missing/`None` → CSRF reachability; `SameSite=Lax` is bypassable via sibling-subdomain top-level navigation (Argo CD **CVE-2024-22424** class) → hand to `hunt-csrf`.
    - **`__Host-` / `__Secure-` prefix absent** → the session can be overwritten/fixated from a subdomain or non-secure context; its presence largely kills cookie-fixation, so flag the *absence* as the precondition for Phase 1.
    
    ### Phase 5 — Session-ID Entropy
    ```bash
    # Collect a LARGE sample (200+) of freshly-issued IDs. -L is required: a 302
    # /login often sets the cookie on the redirect target, not the first response.
    N=200; SAMP=$(mktemp)
    for i in $(seq 1 $N); do
      J=$(mktemp)
      curl -s -L -c "$J" "https://$TARGET/login" -o /dev/null
      get_cookie "$J" | cut -d= -f2- >> "$SAMP"
      rm -f "$J"
    done
    sort "$SAMP" | uniq -d | head            # duplicates = catastrophic (re-use)
    awk '{print length($0)}' "$SAMP" | sort -n | uniq -c   # length distribution
    ```
    Then analyse, don't eyeball:
    - **Sequential / monotonic** — `sort -n` the decoded values; a steady +1/+N delta = predictable.
    - **Decodable structure** — `base64 -d` / hex-decode each ID and look for embedded `userId`, unix timestamps, or PIDs.
    - **Bit entropy** — feed the raw bytes to `ent` or `dieharder`; NIST SP 800-63B wants ≥64 bits. 10 samples is far too few to claim anything — gather hundreds.
    - **FP guard:** a long random-*looking* token is not proof of strength; only structural decode + a large-sample entropy estimate is. Conversely a short token with high per-char entropy may still be fine — measure, don't count characters.
    
    ### Phase 6 — JWT-as-Session
    ```bash
    JWT="eyJ..."        # captured from Authorization: Bearer or a cookie
    # Decode header + payload safely (base64url padding fix).
    b64url(){ local s="${1//-/+}"; s="${s//_//}"; printf '%s' "$s===" | base64 -d 2>/dev/null; }
    b64url "$(cut -d. -f1 <<<"$JWT")" | jq .   # header: alg, kid
    b64url "$(cut -d. -f2 <<<"$JWT")" | jq .   # claims: exp, iat, sub, jti
    ```
    - **`exp` missing or years out** → no expiry. **`jti` missing** → server cannot maintain a revocation list → logout can't truly revoke.
    - **Revocation test:** logout, then replay the *same* JWT against `/api/me`. If it still returns the user → tokens are not server-revocable; this is the JWT-session persistence finding. Body-diff to avoid a cached 200.
    - **Tampering (alg/kid/key-confusion) is owned by `hunt-api-misconfig`** — hand off `jwt_tool $JWT -T` / `-X a` there rather than duplicating it.
    
    ### Phase 7 — Refresh-Token Rotation & Reuse-Detection
    ```bash
    # 1) Obtain a refresh token (login or /oauth/token), then rotate it once.
    RT1=$(curl -s -L -X POST "https://$TARGET/api/login" \
      -H 'Content-Type: application/json' \
      -d '{"email":"victim@example.com","password":"OldPass!1"}' | jq -r '.refresh_token')
    
    # 2) Use RT1 to mint a new access token — server SHOULD return a rotated RT2.
    R2=$(curl -s -L -X POST "https://$TARGET/auth/refresh" \
      -H 'Content-Type: application/json' -d "{\"refresh_token\":\"$RT1\"}")
    RT2=$(jq -r '.refresh_token' <<<"$R2"); echo "rotated? RT1!=RT2 -> $([ "$RT1" != "$RT2" ] && echo yes || echo NO-ROTATION)"
    
    # 3) REUSE-DETECTION test: replay the OLD RT1 again (simulating the leaked token).
    REPLAY=$(curl -s -L -X POST "https://$TARGET/auth/refresh" \
      -H 'Content-Type: application/json' -d "{\"refresh_token\":\"$RT1\"}" -w '\n[%{http_code}]')
    echo "RT1 replay: $REPLAY"
    
    # 4) Then confirm RT2 was KILLED by the replay (correct BCP behaviour invalidates
    #    the whole family). If RT2 still works after RT1 was replayed → no family-revocation.
    curl -s -L -X POST "https://$TARGET/auth/refresh" \
      -H 'Content-Type: application/json' -d "{\"refresh_token\":\"$RT2\"}" -w '\n[%{http_code}]'
    ```
    **Findings:** no rotation (RT1==RT2) = a long-lived stealable credential; rotation **without** reuse-detection (RT1 replay still mints tokens, or RT2 survives the replay) = the leaked-token-persistence bug per the OAuth Security BCP. **OOB note:** if you suspect a leaked RT via SSRF/log/JS-bundle, confirm the token's reach with `hunt-ssrf`/`hunt-source-leak`, not by guessing.
    
    ### Phase 8 — OAuth/SSO Session Linkage & DBSC Downgrade
    ```bash
    # SSO linkage: after IdP callback, is the app session bound to the IdP session?
    #  - Log out at the IdP only; replay the app session cookie. Still 200 with user
    #    data → app session outlives the IdP session (single-logout gap).
    # DBSC downgrade: if responses carry Sec-Session-Registration / Sec-Session-Id,
    #  strip the device-bound proof header and replay the plain cookie:
    curl -s -L -H "Cookie: $A" "https://$TARGET/api/me" -w '\n[%{http_code}]'
    #  If the plain (non-bound) cookie is still accepted → device-binding is advisory,
    #  not enforced → a stolen cookie defeats DBSC entirely.
    ```
    Hand OAuth `state`/`redirect_uri`/code-injection to `hunt-oauth`; this phase only covers the *session-layer* binding.
    
    ---
    
    ## Chain Table
    
    | Session finding | Chain to | Impact |
    |----------------|----------|--------|
    | Session fixation (forced `__Host-`-less cookie) | Trick admin/SSO user into authenticating on planted ID | Admin session takeover (Critical) |
    | No logout/password-change invalidation | `hunt-xss`/`hunt-dom` cookie theft → replay surviving session | Persistent ATO past victim's reset |
    | Refresh token, no reuse-detection | Leaked RT (SSRF/log/bundle) → infinite access-token minting | Persistent ATO, survives password change |
    | `SameSite=Lax` only | Sibling-subdomain top-level nav (CVE-2024-22424 class) → CSRF | State change / login-CSRF → fixation |
    | JWT no `exp`/`jti` | Stolen token, no server revocation | Permanent access |
    | DBSC downgrade accepted | Steal plain cookie despite device-binding | Defeats the only theft mitigation |
    | Predictable ID | Compute/brute another user's session | Cross-user ATO |
    
    ---
    
    ## Validation (house FP discipline)
    
    Before claiming ANY session finding:
    - **Two real sessions, not placeholders** — every fixation/invalidation claim uses A and B captured by the `curl` flows above.
    - **Body-diff, never status-only** — a 200 means nothing without the account's unique identity marker present in the body, diffed against the authenticated baseline.
    - **Negative control** — a garbage/random cookie must FAIL where your "surviving" cookie succeeds; otherwise the endpoint isn't session-gated and it's a non-finding.
    - **Cache/edge check** — re-request with a cache-buster and from a second IP; rule out an edge-cached or IP-pinned 200.
    - **OOB for theft chains** — when the impact depends on exfiltrating a cookie/token (XSS, SSRF, log leak), confirm receipt out-of-band (Collaborator) rather than asserting it.
    - **Static-vs-state** — `HttpOnly`/`Secure`/`SameSite` absence is a *policy* observation; only report as High once paired with a real exploit primitive (XSS, network-MITM, CSRF). Standalone attribute gaps are Low/Informational.
    
    **Severity:**
    - Session fixation → admin/SSO takeover: **Critical**
    - No invalidation on password/email change, or refresh-token reuse without detection: **High → Critical** (escalate if MFA/step-up also absent)
    - Predictable/duplicate session ID: **High**
    - No invalidation on logout: **Medium → High** (depends on theft vector)
    - Missing `HttpOnly`/`SameSite` standalone: **Low/Informational** until chained
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related