{"id":"hunt-session","name":"hunt-session","summary":"Hunt Session Managementの脆弱性 — セッション固定(ログイン時の再生成なし)、ログアウト/パスワード変更/メール変更時の無効化不足、予測可能または低エントロピーのセッションID、経験/取り消しなしのJWT-as-session、リフレッシュトークン回転/再利用検出のギャップ、OAuth/SSO…","body":"## Autonomous Testing Priority\n\n**Missing HttpOnly on cookies is auto-detected — focus your active testing on lifecycle invalidation (higher impact).**\n\n**Pattern 1 — Session survives logout (most common high-value finding):**\n1. Login and note the session token/cookie value\n2. Call the logout endpoint (`/logout`, `POST /api/logout`, etc.)\n3. Try to use the OLD session token to access a protected resource (`/api/me`, `/dashboard`, `/account`)\n4. If 200 with user data → session not invalidated on logout = ATO persistence\n\n**Pattern 2 — Session not regenerated on login (session fixation):**\n1. GET any page to receive a pre-authentication session token/cookie\n2. POST valid credentials to the login endpoint\n3. Compare the session token BEFORE and AFTER login\n4. If the token is unchanged → session fixation vulnerability\n\n**Pattern 3 — Session survives password change:**\n1. Login → record session A value\n2. Change the password via the account settings endpoint\n3. Replay session A on a protected endpoint\n4. If 200 → token not rotated on credential change = persistent ATO (critical chain when combined with XSS/cookie theft)\n\n**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.\n\n**Proof:** A protected-resource 200 response (with user data) using a session token that should have been invalidated confirms the finding.\n\n---\n\n# HUNT-SESSION — Session Management\n\n## Crown Jewel Targets\n\nSession 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).\n\n**Highest-value chains:**\n- **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.\n- **No invalidation on logout** — old token still works after `/logout` → theft window never closes.\n- **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.\n- **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.\n- **Predictable / low-entropy session ID** — sequential, timestamp- or userId-derived IDs → brute-force or compute other users' sessions.\n- **JWT-as-session with no `exp` / no revocation list** — stolen JWT = permanent access; logout is cosmetic.\n\n---\n\n## Grounding — patterns that shaped each phase\n\nNo invented CVE/report IDs below. These are the *named, publicly-documented* patterns this skill encodes:\n\n- **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.\n- **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.\n- **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.\n- **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?\n- **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`).\n- **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.\n\nCross-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`.\n\n---\n\n## Attack Surface Signals\n\n```\nSet-Cookie: session=...            # name varies: sid, JSESSIONID, connect.sid,\n                                   # PHPSESSID, ASP.NET_SessionId, laravel_session, _csrf\n/login /logout /api/login /oauth/token\n/auth/refresh /api/token/refresh   # refresh-token rotation surface\n/account/change-password /settings/email\n?sid= ?session= in URL             # session-in-URL → leaks via Referer/logs (finding)\n```\n```\n# Header signals worth flagging immediately:\nSet-Cookie: session=abc; Path=/                 # no HttpOnly/Secure/SameSite\nSet-Cookie: session=abc; SameSite=None          # None without Secure = rejected by modern browsers, but flag\nSet-Cookie: __Host-sess=...; Secure; Path=/     # GOOD — hard to fixate\nSec-Session-Registration: ...                   # DBSC in play → test downgrade\n```\n\n---\n\n## Step-by-Step Hunting Methodology\n\n> **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.\n\n```bash\nTARGET=target.com\nJAR_A=$(mktemp); JAR_B=$(mktemp)\n\n# Robust session-cookie extractor: handles #HttpOnly_ prefix lines and any\n# cookie name (sid/JSESSIONID/connect.sid/PHPSESSID/...). Prints name=value.\nget_cookie () {  # $1=jar  $2=name-regex (default: common session names)\n  local jar=\"$1\" re=\"${2:-session|sid|sess|JSESSIONID|connect\\.sid|PHPSESSID|laravel_session}\"\n  awk -v re=\"$re\" '\n    /^#HttpOnly_/ { sub(/^#HttpOnly_/,\"\"); }   # strip jar HttpOnly marker\n    /^#/ { next }                              # skip remaining comments\n    NF>=7 && $6 ~ re { print $6\"=\"$7 }         # field6=name field7=value\n  ' \"$jar\" | tail -1\n}\n```\n\n### Phase 1 — Session Fixation (regeneration-on-login)\n```bash\n# Step 1: grab a pre-auth session the SERVER hands an anonymous client.\ncurl -s -L -c \"$JAR_A\" \"https://$TARGET/login\" -o /dev/null\nPRE=$(get_cookie \"$JAR_A\"); echo \"pre-auth: $PRE\"\n\n# Step 1b (stronger): can we FORCE an arbitrary ID? attacker-chosen value.\nFIX=\"session=AAAAdeadbeefAAAA\"\n\n# Step 2: authenticate while CARRYING the pre-auth/forced cookie (reuse same jar).\ncurl -s -L -c \"$JAR_A\" -b \"$JAR_A\" -X POST \"https://$TARGET/login\" \\\n  -d \"username=attacker@example.com&password=CorrectHorse1\" -o /dev/null\nPOST=$(get_cookie \"$JAR_A\"); echo \"post-auth: $POST\"\n\n# DECISION:\n#  - If $POST == $PRE (value unchanged across the auth boundary) AND that value\n#    now returns authenticated data → FIXATION. The server reused the anon ID.\n#  - If the forced $FIX value is accepted and authenticates → CRITICAL fixation\n#    (attacker controls the ID; no email/XSS needed to plant it).\nAUTH=$(curl -s -L -b \"$JAR_A\" \"https://$TARGET/api/me\")\necho \"$AUTH\" | head -c 200\n```\n**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.\n\n### Phase 2 — Invalidation on Logout\n```bash\n# A logs in for real (fresh jar), capture A's live session.\ncurl -s -L -c \"$JAR_A\" -X POST \"https://$TARGET/api/login\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"attacker@example.com\",\"password\":\"CorrectHorse1\"}' -o /dev/null\nA=$(get_cookie \"$JAR_A\"); echo \"A=$A\"\n\n# Baseline: what does an authenticated /api/me look like for A? (capture body, not just code)\nBEFORE=$(curl -s -L -b \"$JAR_A\" \"https://$TARGET/api/me\")\n\n# Logout A.\ncurl -s -L -b \"$JAR_A\" -X POST \"https://$TARGET/api/logout\" -o /dev/null\n\n# Replay A's OLD cookie value explicitly (do NOT reuse the jar — logout may have\n# overwritten it). Compare body + code against the authenticated baseline.\nAFTER=$(curl -s -L -H \"Cookie: $A\" \"https://$TARGET/api/me\" -w '\\n[%{http_code}]')\necho \"AFTER: $AFTER\"\n```\n**FP discipline (mandatory):**\n- 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).\n- 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.\n- Re-test after a **short delay** and from a **different IP** — some servers lazily expire on next access or pin sessions to IP.\n\n### Phase 3 — Invalidation on Password / Email Change (persistent-ATO core)\n```bash\n# This is the real two-session flow. A = attacker holding a stolen/old session.\n# B = the victim who changes their password believing it revokes access.\n# (In a real engagement A is a session you legitimately captured for a TEST account\n#  that you also control as B — never use a real third party.)\n\n# 1) Log the TEST account in as session A, capture it.\ncurl -s -L -c \"$JAR_A\" -X POST \"https://$TARGET/api/login\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"victim@example.com\",\"password\":\"OldPass!1\"}' -o /dev/null\nSESSION_A=$(get_cookie \"$JAR_A\"); echo \"SESSION_A=$SESSION_A\"\nBEFORE=$(curl -s -L -H \"Cookie: $SESSION_A\" \"https://$TARGET/api/profile\")\n\n# 2) Log the SAME account in as session B (separate jar = \"the victim's browser\").\ncurl -s -L -c \"$JAR_B\" -X POST \"https://$TARGET/api/login\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"victim@example.com\",\"password\":\"OldPass!1\"}' -o /dev/null\n\n# 3) Victim (session B) changes the password.\ncurl -s -L -b \"$JAR_B\" -X POST \"https://$TARGET/api/change-password\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"old_password\":\"OldPass!1\",\"new_password\":\"BrandNew!2\"}' -o /dev/null\n\n# 4) THE TEST: replay the OLD SESSION_A captured in step 1.\nAFTER=$(curl -s -L -H \"Cookie: $SESSION_A\" \"https://$TARGET/api/profile\" -w '\\n[%{http_code}]')\necho \"AFTER pw-change: $AFTER\"\n```\n**Decision + FP discipline:**\n- 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.\n- Run the **garbage-cookie negative control** again to prove the endpoint is session-gated.\n- 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.\n- **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.\n\n### Phase 4 — Cookie Attribute Analysis\n```bash\ncurl -sI -L \"https://$TARGET/\" | grep -i '^set-cookie'\n```\n- **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.\n- **Secure** missing → cookie sent over cleartext HTTP; pair with `hunt-tls-network` (downgrade/HSTS-gap) for a network-attacker chain.\n- **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`.\n- **`__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.\n\n### Phase 5 — Session-ID Entropy\n```bash\n# Collect a LARGE sample (200+) of freshly-issued IDs. -L is required: a 302\n# /login often sets the cookie on the redirect target, not the first response.\nN=200; SAMP=$(mktemp)\nfor i in $(seq 1 $N); do\n  J=$(mktemp)\n  curl -s -L -c \"$J\" \"https://$TARGET/login\" -o /dev/null\n  get_cookie \"$J\" | cut -d= -f2- >> \"$SAMP\"\n  rm -f \"$J\"\ndone\nsort \"$SAMP\" | uniq -d | head            # duplicates = catastrophic (re-use)\nawk '{print length($0)}' \"$SAMP\" | sort -n | uniq -c   # length distribution\n```\nThen analyse, don't eyeball:\n- **Sequential / monotonic** — `sort -n` the decoded values; a steady +1/+N delta = predictable.\n- **Decodable structure** — `base64 -d` / hex-decode each ID and look for embedded `userId`, unix timestamps, or PIDs.\n- **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.\n- **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.\n\n### Phase 6 — JWT-as-Session\n```bash\nJWT=\"eyJ...\"        # captured from Authorization: Bearer or a cookie\n# Decode header + payload safely (base64url padding fix).\nb64url(){ local s=\"${1//-/+}\"; s=\"${s//_//}\"; printf '%s' \"$s===\" | base64 -d 2>/dev/null; }\nb64url \"$(cut -d. -f1 <<<\"$JWT\")\" | jq .   # header: alg, kid\nb64url \"$(cut -d. -f2 <<<\"$JWT\")\" | jq .   # claims: exp, iat, sub, jti\n```\n- **`exp` missing or years out** → no expiry. **`jti` missing** → server cannot maintain a revocation list → logout can't truly revoke.\n- **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.\n- **Tampering (alg/kid/key-confusion) is owned by `hunt-api-misconfig`** — hand off `jwt_tool $JWT -T` / `-X a` there rather than duplicating it.\n\n### Phase 7 — Refresh-Token Rotation & Reuse-Detection\n```bash\n# 1) Obtain a refresh token (login or /oauth/token), then rotate it once.\nRT1=$(curl -s -L -X POST \"https://$TARGET/api/login\" \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\":\"victim@example.com\",\"password\":\"OldPass!1\"}' | jq -r '.refresh_token')\n\n# 2) Use RT1 to mint a new access token — server SHOULD return a rotated RT2.\nR2=$(curl -s -L -X POST \"https://$TARGET/auth/refresh\" \\\n  -H 'Content-Type: application/json' -d \"{\\\"refresh_token\\\":\\\"$RT1\\\"}\")\nRT2=$(jq -r '.refresh_token' <<<\"$R2\"); echo \"rotated? RT1!=RT2 -> $([ \"$RT1\" != \"$RT2\" ] && echo yes || echo NO-ROTATION)\"\n\n# 3) REUSE-DETECTION test: replay the OLD RT1 again (simulating the leaked token).\nREPLAY=$(curl -s -L -X POST \"https://$TARGET/auth/refresh\" \\\n  -H 'Content-Type: application/json' -d \"{\\\"refresh_token\\\":\\\"$RT1\\\"}\" -w '\\n[%{http_code}]')\necho \"RT1 replay: $REPLAY\"\n\n# 4) Then confirm RT2 was KILLED by the replay (correct BCP behaviour invalidates\n#    the whole family). If RT2 still works after RT1 was replayed → no family-revocation.\ncurl -s -L -X POST \"https://$TARGET/auth/refresh\" \\\n  -H 'Content-Type: application/json' -d \"{\\\"refresh_token\\\":\\\"$RT2\\\"}\" -w '\\n[%{http_code}]'\n```\n**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.\n\n### Phase 8 — OAuth/SSO Session Linkage & DBSC Downgrade\n```bash\n# SSO linkage: after IdP callback, is the app session bound to the IdP session?\n#  - Log out at the IdP only; replay the app session cookie. Still 200 with user\n#    data → app session outlives the IdP session (single-logout gap).\n# DBSC downgrade: if responses carry Sec-Session-Registration / Sec-Session-Id,\n#  strip the device-bound proof header and replay the plain cookie:\ncurl -s -L -H \"Cookie: $A\" \"https://$TARGET/api/me\" -w '\\n[%{http_code}]'\n#  If the plain (non-bound) cookie is still accepted → device-binding is advisory,\n#  not enforced → a stolen cookie defeats DBSC entirely.\n```\nHand OAuth `state`/`redirect_uri`/code-injection to `hunt-oauth`; this phase only covers the *session-layer* binding.\n\n---\n\n## Chain Table\n\n| Session finding | Chain to | Impact |\n|----------------|----------|--------|\n| Session fixation (forced `__Host-`-less cookie) | Trick admin/SSO user into authenticating on planted ID | Admin session takeover (Critical) |\n| No logout/password-change invalidation | `hunt-xss`/`hunt-dom` cookie theft → replay surviving session | Persistent ATO past victim's reset |\n| Refresh token, no reuse-detection | Leaked RT (SSRF/log/bundle) → infinite access-token minting | Persistent ATO, survives password change |\n| `SameSite=Lax` only | Sibling-subdomain top-level nav (CVE-2024-22424 class) → CSRF | State change / login-CSRF → fixation |\n| JWT no `exp`/`jti` | Stolen token, no server revocation | Permanent access |\n| DBSC downgrade accepted | Steal plain cookie despite device-binding | Defeats the only theft mitigation |\n| Predictable ID | Compute/brute another user's session | Cross-user ATO |\n\n---\n\n## Validation (house FP discipline)\n\nBefore claiming ANY session finding:\n- **Two real sessions, not placeholders** — every fixation/invalidation claim uses A and B captured by the `curl` flows above.\n- **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.\n- **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.\n- **Cache/edge check** — re-request with a cache-buster and from a second IP; rule out an edge-cached or IP-pinned 200.\n- **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.\n- **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.\n\n**Severity:**\n- Session fixation → admin/SSO takeover: **Critical**\n- No invalidation on password/email change, or refresh-token reuse without detection: **High → Critical** (escalate if MFA/step-up also absent)\n- Predictable/duplicate session ID: **High**\n- No invalidation on logout: **Medium → High** (depends on theft vector)\n- Missing `HttpOnly`/`SameSite` standalone: **Low/Informational** until chained","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-session","license":"MIT","category":"productivity","lang":"en","tokens":5084,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}