hunt-session
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAutonomous 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):
- Login and note the session token/cookie value
- Call the logout endpoint (,
/logout, etc.)POST /api/logout - Try to use the OLD session token to access a protected resource (,
/api/me,/dashboard)/account - If 200 with user data → session not invalidated on logout = ATO persistence
Pattern 2 — Session not regenerated on login (session fixation):
- GET any page to receive a pre-authentication session token/cookie
- POST valid credentials to the login endpoint
- Compare the session token BEFORE and AFTER login
- If the token is unchanged → session fixation vulnerability
Pattern 3 — Session survives password change:
- Login → record session A value
- Change the password via the account settings endpoint
- Replay session A on a protected endpoint
- 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 for REST APIs, for traditional web forms. Try both if the first returns an unexpected response.
application/jsonapplication/x-www-form-urlencodedProof: A protected-resource 200 response (with user data) using a session token that should have been invalidated confirms the finding.
Cookie 缺少 HttpOnly 可自动检测——应将主动测试重点放在生命周期失效问题上(影响更高)。
模式 1——注销后 Session 仍然有效(最常见的高价值发现):
- 登录并记录 session token/cookie 值
- 调用 logout endpoint(、
/logout等)POST /api/logout - 尝试使用旧 session token 访问受保护资源(、
/api/me、/dashboard)/account - 如果返回 200 且包含 user data → session 未在 logout 时失效 = ATO 持久化
模式 2——登录时 Session 未重新生成(Session fixation):
- GET 任意页面以获取预认证 session token/cookie
- POST 有效凭据到 login endpoint
- 比较登录前后的 session token
- 如果 token 未发生变化 → Session fixation 漏洞
模式 3——Password change 后 Session 仍然有效:
- 登录 → 记录 session A 的值
- 通过账户设置 endpoint 修改密码
- 在受保护 endpoint 上重放 session A
- 如果返回 200 → 凭据变更后 token 未轮换 = 持久性 ATO(与 XSS/cookie 窃取结合时构成 Critical 攻击链)
**Content-type:**登录和 session endpoint 的类型各不相同——REST API 使用 ,传统 Web 表单使用 。如果第一次尝试返回的响应不符合预期,请两种都尝试。
application/jsonapplication/x-www-form-urlencoded**Proof:**使用一个本应已被失效的 session token 访问受保护资源时,若返回 200 响应且包含 user data,即可确认该 finding。
HUNT-SESSION — Session Management
HUNT-SESSION — 会话管理
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 → theft window never closes.
/logout - 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 / no revocation list — stolen JWT = permanent access; logout is cosmetic.
exp
Session fixation 导致管理员劫持 = Critical。Password change 后 session 仍然有效 = High-to-Critical(受害者以为通过重置密码已经撤销了被窃取的 cookie,但攻击者仍可借此实现持久性 ATO)。
最高价值的攻击链:
- Session fixation——服务器接受客户端设置的 session ID,且在登录时不重新生成它 → 攻击者预先植入一个 ID;受害者完成身份验证后,攻击者即可复用这个已经通过认证的 session → 持久性 ATO。
- Logout 时未失效——旧 token 在 后仍然有效 → 窃取窗口永远不会关闭。
/logout - Password/email change 时未失效——被窃取的 session 能在受害者认为“我被黑了,所以我重置”之后继续存在 → 持久性 ATO。这是价值最高的 session 漏洞类别。
- Refresh token 重用但没有轮换检测——泄漏的 refresh token 可以永远签发新的 access token;没有重用检测意味着合法用户后续执行 refresh 不会撤销攻击者的分支。
- 可预测/低熵 session ID——顺序型、基于时间戳或 userId 派生的 ID → 暴力破解或计算出其他用户的 session。
- 将 JWT 用作 session,且无 /无撤销列表——被窃取的 JWT = 永久访问;logout 只是表面操作。
exp
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 as its only CSRF defence.
SameSite=Lax - 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-prefixes per RFC 6265bis. Missing__Secure-is only a finding when a real XSS/DOM sink exists (chain withHttpOnly/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 → ; JWT alg/kid tampering → ; OAuth code/state flaws → ; CSRF mechanics → ; cookie-theft sinks → / .
hunt-atohunt-api-misconfighunt-oauthhunt-csrfhunt-xsshunt-dom以下内容不会编造 CVE/报告 ID。它们是本 skill 所编码的、具有明确名称且有公开文档的模式:
- Session fixation、login-CSRF、认证时不重新生成——OWASP WSTG-SESS-03/WSTG-SESS-01;经典的 ACROS/Mitja Kolšek session-fixation 论文。影响最大的变体:固定 SSO/管理员用户的 session。
- SameSite=Lax 兄弟子域 CSRF 触达 session 状态——Argo CD CVE-2024-22424(Lax cookie 会在来自兄弟子域的顶级跨站导航中发送)。当 session cookie 仅依赖 作为 CSRF 防线时使用此项。
SameSite=Lax - Refresh token 轮换与自动重用检测——Auth0/IETF OAuth-Security-BCP 模型:轮换后的 refresh token 一旦被重放,必须撤销整个 token family。缺失该机制 = 需要证明的核心漏洞。
- Device Bound Session Credentials(DBSC)——W3C/Chrome DBSC 草案将 session 绑定到 TPM/device key。测试降级:剥离 DBSC challenge 后,服务器是否仍接受未绑定的 cookie?
- Cookie 属性加固——OWASP WSTG-SESS-02;根据 RFC 6265bis 使用 /
__Host-前缀。只有在存在真实 XSS/DOM sink 时,缺少__Secure-才构成 finding(与HttpOnly/hunt-xss组成攻击链)。hunt-dom - Entropy——NIST SP 800-63B 要求 session identifier 具有至少 ≥64 bits 的 entropy。任何可解码为 counter/timestamp/userId 的内容,无论长度如何,都应视为 finding。
交叉引用:ATO 攻击链 → ;JWT alg/kid 篡改 → ;OAuth code/state 缺陷 → ;CSRF 机制 → ;cookie 窃取 sink → /。
hunt-atohunt-api-misconfighunt-oauthhunt-csrfhunt-xsshunt-domAttack 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)undefinedSet-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)undefinedHeader signals worth flagging immediately:
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
---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's Netscape jar.curl
bash
TARGET=target.com
JAR_A=$(mktemp); JAR_B=$(mktemp)**双 session 规则。**每一项失效/固定声明都必须使用通过真实流程捕获的两个具体 session——攻击者 A 和受害者 B——绝不能使用硬编码占位字符串。以下辅助函数从的 Netscape jar 中捕获真实 cookie。curl
bash
TARGET=target.com
JAR_A=$(mktemp); JAR_B=$(mktemp)Robust session-cookie extractor: handles #HttpOnly_ prefix lines and any
Robust session-cookie extractor: handles #HttpOnly_ prefix lines and any
cookie name (sid/JSESSIONID/connect.sid/PHPSESSID/...). Prints name=value.
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
}
undefinedget_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
}
undefinedPhase 1 — Session Fixation (regeneration-on-login)
阶段 1——Session Fixation(登录时重新生成)
bash
undefinedbash
undefinedStep 1: grab a pre-auth session the SERVER hands an anonymous client.
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"
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.
Step 1b (stronger): can we FORCE an arbitrary ID? attacker-chosen value.
FIX="session=AAAAdeadbeefAAAA"
FIX="session=AAAAdeadbeefAAAA"
Step 2: authenticate while CARRYING the pre-auth/forced cookie (reuse same jar).
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"
-d "username=attacker@example.com&password=CorrectHorse1" -o /dev/null POST=$(get_cookie "$JAR_A"); echo "post-auth: $POST"
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"
-d "username=attacker@example.com&password=CorrectHorse1" -o /dev/null POST=$(get_cookie "$JAR_A"); echo "post-auth: $POST"
DECISION:
DECISION:
- If $POST == $PRE (value unchanged across the auth boundary) AND that value
- If $POST == $PRE (value unchanged across the auth boundary) AND that value
now returns authenticated data → FIXATION. The server reused the anon ID.
now returns authenticated data → FIXATION. The server reused the anon ID.
- If the forced $FIX value is accepted and authenticates → CRITICAL fixation
- If the forced $FIX value is accepted and authenticates → CRITICAL fixation
(attacker controls the ID; no email/XSS needed to plant it).
(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.AUTH=$(curl -s -L -b "$JAR_A" "https://$TARGET/api/me")
echo "$AUTH" | head -c 200
**误报防护:**值发生变化并不自动意味着安全——某些应用会轮换可读 cookie,但通过第二个 cookie 维持一个稳定的服务端 session。对完整的 `Set-Cookie` 集合执行 diff,并确认*旧值*确实已经失效(阶段 2)。同时确认 `/api/me` 返回的是*你的*身份信息,而不是通用的 200/landing page。Phase 2 — Invalidation on Logout
阶段 2——Logout 时的失效
bash
undefinedbash
undefinedA logs in for real (fresh jar), capture A's live session.
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"
-H 'Content-Type: application/json'
-d '{"email":"attacker@example.com","password":"CorrectHorse1"}' -o /dev/null A=$(get_cookie "$JAR_A"); echo "A=$A"
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"
-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)
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")
BEFORE=$(curl -s -L -b "$JAR_A" "https://$TARGET/api/me")
Logout A.
Logout A.
curl -s -L -b "$JAR_A" -X POST "https://$TARGET/api/logout" -o /dev/null
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
Replay A's OLD cookie value explicitly (do NOT reuse the jar — logout may have
overwritten it). Compare body + code against the authenticated baseline.
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.AFTER=$(curl -s -L -H "Cookie: $A" "https://$TARGET/api/me" -w '\n[%{http_code}]')
echo "AFTER: $AFTER"
**误报纪律(强制):**
- 不要只信 status code。缓存/边缘节点的 200 或通用 SPA shell 会对所有人返回 200。对 `AFTER` 与 `BEFORE` 执行 **body-diff**——只有当 `AFTER` 仍包含 A 的*唯一身份标记*(email、user-id、CSRF token、account name)时,finding 才成立。
- 使用**负向对照**确认:随机/垃圾 cookie 值不得返回相同的已认证 body。如果垃圾值也返回带 user data 的 200,说明 endpoint 未受 session 控制,此处不存在 finding。
- 短暂延迟后并从**不同 IP** 重新测试——有些服务器会延迟到下一次访问时过期,或将 session 绑定到 IP。Phase 3 — Invalidation on Password / Email Change (persistent-ATO core)
阶段 3——Password/email change 时的失效(持久性 ATO 核心)
bash
undefinedbash
undefinedThis is the real two-session flow. A = attacker holding a stolen/old session.
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.
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
(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.)
that you also control as B — never use a real third party.)
1) Log the TEST account in as session A, capture it.
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")
-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")
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")
-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").
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
-H 'Content-Type: application/json'
-d '{"email":"victim@example.com","password":"OldPass!1"}' -o /dev/null
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
-H 'Content-Type: application/json'
-d '{"email":"victim@example.com","password":"OldPass!1"}' -o /dev/null
3) Victim (session B) changes the password.
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
-H 'Content-Type: application/json'
-d '{"old_password":"OldPass!1","new_password":"BrandNew!2"}' -o /dev/null
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
-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.
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.AFTER=$(curl -s -L -H "Cookie: $SESSION_A" "https://$TARGET/api/profile" -w '\n[%{http_code}]')
echo "AFTER pw-change: $AFTER"
**判定与误报纪律:**
- 只有在 `AFTER` 返回 200 **且** body 仍包含账户的唯一数据(与 `BEFORE` 做 body-diff)时,才能确认 finding。仅有公共/SPA 路由返回裸 200 并不能证明问题。
- 再次运行 **garbage-cookie negative control**,证明 endpoint 确实受 session 控制。
- 对 **email-change**(`/settings/email`)和 **logout-all-devices** 重复完全相同的流程——应用经常只使*当前操作者*的 session(B)失效,却不使*同级* session(A)失效。这种同级 session 存活的特性正是 `hunt-ato` 所链接的持久性 ATO 原语。
- **严重性门槛:**如果 change-password endpoint 同时缺少 current-password/MFA step-up(参见 `hunt-mfa-bypass`),A 可以从只读权限升级为完全接管 → 提升严重性。Phase 4 — Cookie Attribute Analysis
阶段 4——Cookie 属性分析
bash
curl -sI -L "https://$TARGET/" | grep -i '^set-cookie'- HttpOnly missing → cookie reachable via . Only a finding chained to a real XSS/DOM sink (
document.cookie/hunt-xss) — note it, don't report standalone as High.hunt-dom - Secure missing → cookie sent over cleartext HTTP; pair with (downgrade/HSTS-gap) for a network-attacker chain.
hunt-tls-network - SameSite missing/→ CSRF reachability;
Noneis bypassable via sibling-subdomain top-level navigation (Argo CD CVE-2024-22424 class) → hand toSameSite=Lax.hunt-csrf - /
__Host-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.__Secure-
bash
curl -sI -L "https://$TARGET/" | grep -i '^set-cookie'- 缺少 HttpOnly → cookie 可通过 访问。只有在与真实 XSS/DOM sink 串联时才是 finding(
document.cookie/hunt-xss)——应予记录,但不要作为 High 单独报告。hunt-dom - 缺少 Secure → cookie 会通过明文 HTTP 发送;结合 (降级/HSTS 缺口)可形成网络攻击者攻击链。
hunt-tls-network - 缺少 SameSite/值为 → CSRF 可达性;
None可通过兄弟子域顶级导航绕过(Argo CD CVE-2024-22424 类)→ 交给SameSite=Lax。hunt-csrf - 缺少 /
__Host-前缀 → session 可从子域或非安全上下文被覆写/固定;其存在基本能阻止 cookie fixation,因此将缺失视为阶段 1 的前置条件。__Secure-
Phase 5 — Session-ID Entropy
阶段 5——Session ID Entropy
bash
undefinedbash
undefinedCollect a LARGE sample (200+) of freshly-issued IDs. -L is required: a 302
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.
/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.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
然后进行分析,不要凭肉眼判断:
- **顺序型/单调递增**——对解码后的值执行 `sort -n`;稳定的 +1/+N 增量 = 可预测。
- **可解码的结构**——对每个 ID 执行 `base64 -d`/hex decode,并检查其中是否嵌有 `userId`、unix timestamp 或 PID。
- **Bit entropy**——将原始字节输入 `ent` 或 `dieharder`;NIST SP 800-63B 要求 ≥64 bits。仅采集 10 个样本远不足以得出结论——应采集数百个。
- **误报防护:**一个看起来很长且随机的 token 并不能证明其强度;只有结构化解码与大规模样本 entropy 估计才能证明。反之,一个字符数较短但单字符 entropy 很高的 token 仍可能安全——应当测量,而不是统计字符数。Phase 6 — JWT-as-Session
阶段 6——将 JWT 用作 Session
bash
JWT="eyJ..." # captured from Authorization: Bearer or a cookiebash
JWT="eyJ..." # captured from Authorization: Bearer or a cookieDecode header + payload safely (base64url padding fix).
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.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` 或过期时间远在数年之后** → 无过期机制。**缺少 `jti`** → 服务器无法维护撤销列表 → logout 无法真正撤销。
- **撤销测试:**logout 后,将*同一个* JWT 重放到 `/api/me`。如果仍返回用户信息 → token 无法由服务器撤销;这就是 JWT-session 持久化 finding。应执行 body-diff,以避免缓存导致的假 200。
- **篡改(alg/kid/key-confusion)**由 `hunt-api-misconfig` 负责——将 `jwt_tool $JWT -T`/`-X a` 交给该流程处理,而不是在此重复实现。Phase 7 — Refresh-Token Rotation & Reuse-Detection
阶段 7——Refresh Token 轮换与重用检测
bash
undefinedbash
undefined1) Obtain a refresh token (login or /oauth/token), then rotate it once.
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')
-H 'Content-Type: application/json'
-d '{"email":"victim@example.com","password":"OldPass!1"}' | jq -r '.refresh_token')
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')
-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.
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)"
-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)"
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)"
-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).
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"
-H 'Content-Type: application/json' -d "{"refresh_token":"$RT1"}" -w '\n[%{http_code}]') echo "RT1 replay: $REPLAY"
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"
-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
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.
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}]'
-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.curl -s -L -X POST "https://$TARGET/auth/refresh"
-H 'Content-Type: application/json' -d "{"refresh_token":"$RT2"}" -w '\n[%{http_code}]'
-H 'Content-Type: application/json' -d "{"refresh_token":"$RT2"}" -w '\n[%{http_code}]'
**发现:**不轮换(RT1==RT2)= 可长期窃取的有效凭据;有轮换**但无重用检测**(重放 RT1 仍可签发 token,或重放后 RT2 仍然有效)= OAuth Security BCP 所定义的泄漏 token 持久化漏洞。**OOB 提示:**如果怀疑 RT 通过 SSRF/log/JS bundle 泄漏,应使用 `hunt-ssrf`/`hunt-source-leak` 确认 token 的可达性,而不是靠猜测。Phase 8 — OAuth/SSO Session Linkage & DBSC Downgrade
阶段 8——OAuth/SSO Session Linkage 与 DBSC 降级
bash
undefinedbash
undefinedSSO linkage: after IdP callback, is the app session bound to the IdP session?
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
- 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).
data → app session outlives the IdP session (single-logout gap).
DBSC downgrade: if responses carry Sec-Session-Registration / Sec-Session-Id,
DBSC downgrade: if responses carry Sec-Session-Registration / Sec-Session-Id,
strip the device-bound proof header and replay the plain cookie:
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}]'
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,
If the plain (non-bound) cookie is still accepted → device-binding is advisory,
not enforced → a stolen cookie defeats DBSC entirely.
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.
---将 OAuth `state`/`redirect_uri`/code injection 交给 `hunt-oauth`;本阶段仅覆盖 *session 层*的绑定。
---Chain Table
攻击链表
| Session finding | Chain to | Impact |
|---|---|---|
Session fixation (forced | Trick admin/SSO user into authenticating on planted ID | Admin session takeover (Critical) |
| No logout/password-change invalidation | | 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 |
| Sibling-subdomain top-level nav (CVE-2024-22424 class) → CSRF | State change / login-CSRF → fixation |
JWT no | 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 |
| Session finding | 关联攻击链 | 影响 |
|---|---|---|
Session fixation(强制使用缺少 | 诱使管理员/SSO 用户在植入的 ID 上完成身份验证 | 管理员 session 接管(Critical) |
| Logout/password change 时未失效 | | 受害者重置密码后仍可实现持久性 ATO |
| Refresh token 缺少重用检测 | 泄漏的 RT(SSRF/log/bundle)→ 无限签发 access token | 持久性 ATO,在 password change 后仍然存在 |
仅使用 | 兄弟子域顶级导航(CVE-2024-22424 类)→ CSRF | 状态变更/login-CSRF → fixation |
JWT 缺少 | 窃取 token,且无服务器端撤销能力 | 永久访问 |
| 接受 DBSC 降级 | 即使存在设备绑定也能窃取普通 cookie | 击穿唯一的 token 窃取缓解措施 |
| 可预测的 ID | 计算或暴力破解其他用户的 session | 跨用户 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 flows above.
curl - 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/Secureabsence 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.SameSite
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 /
HttpOnlystandalone: Low/Informational until chainedSameSite
在声明任何 session finding 之前:
- 使用两个真实 session,而不是占位符——每一项 fixation/invalidation 声明都使用通过上述 流程捕获的 A 和 B。
curl - 执行 Body-diff,绝不只依赖 status code——如果 200 响应中未包含账户的唯一身份标记,则不能证明任何问题;必须与已认证基线进行 diff。
- 负向对照——在“存活”的 cookie 成功访问的位置,垃圾/随机 cookie 必须访问失败;否则该 endpoint 并未受 session 控制,不构成 finding。
- 缓存/边缘节点检查——使用 cache-buster 并从第二个 IP 重新请求;排除边缘缓存或 IP 绑定造成的假 200。
- 对窃取链执行 OOB——当影响取决于 cookie/token 的外泄(XSS、SSRF、log 泄漏)时,应通过带外方式(Collaborator)确认接收成功,而不是直接断言。
- Static-vs-state——缺少 /
HttpOnly/Secure属于策略层面的观察;只有与真实利用原语(XSS、网络 MITM、CSRF)结合后,才能报告为 High。单独的属性缺失属于 Low/Informational。SameSite
严重性:
- Session fixation → 管理员/SSO 接管:Critical
- Password/email change 时未失效,或 refresh token 重用但未检测:High → Critical(如果同时缺少 MFA/step-up,则升级)
- 可预测/重复的 session ID:High
- Logout 时未失效:Medium → High(取决于 token 窃取途径)
- 单独缺少 /
HttpOnly:Low/Informational,直到与其他漏洞串联SameSite