关于 hunt-session
hunt-session 是面向安全测试人员的会话管理漏洞狩猎技能,重点检查会话固定、注销或改密后令牌仍有效、令牌轮换及 Cookie 属性,并强调以真实会话和负向对照验证结果。
主要功能
- 01测试登录时会话是否重新生成
- 02检查注销和凭据更改后的会话失效
- 03分析 Cookie 属性与会话标识熵
- 04检查 JWT 会话的过期与撤销
适用场景
- 01验证注销后旧会话能否访问受保护资源
- 02测试改密后其他会话是否仍有效
- 03检查登录是否存在会话固定风险
- 04评估 JWT 会话是否可通过注销撤销
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-session安装后在 Claude Code 中输入/hunt-session确认已加载。
详细介绍
自动化测试优先级
缺少 Cookie 的 HttpOnly 属性可自动检测——请将主动测试重点放在生命周期失效上(影响更大)。
模式 1——注销后会话仍然有效(最常见的高价值发现):
- 登录并记下会话令牌 / Cookie 值
- 调用注销端点(
/logout、POST /api/logout等) - 尝试使用旧会话令牌访问受保护资源(
/api/me、/dashboard、/account) - 如果返回 200 且包含用户数据 → 注销时会话未失效 = ATO 持续有效
模式 2——登录时会话未重新生成(会话固定):
- GET 任意页面,获取预认证会话令牌 / Cookie
- 向登录端点 POST 有效凭据
- 比较登录前后的会话令牌
- 如果令牌未改变 → 存在会话固定漏洞
模式 3——更改密码后会话仍然有效:
- 登录 → 记录会话 A 的值
- 通过账户设置端点更改密码
- 在受保护端点重放会话 A
- 如果返回 200 → 凭据更改时令牌未轮换 = ATO 持续有效(与 XSS / Cookie 窃取结合时会形成严重攻击链)
**内容类型:**登录和会话端点各不相同——REST API 使用 application/json,传统 Web 表单使用 application/x-www-form-urlencoded。如果首次尝试返回意外响应,请两种都试试。
**证明:**使用本应已失效的会话令牌,成功获得受保护资源的 200 响应(且包含用户数据),即可确认此发现。
HUNT-SESSION — 会话管理
核心目标
会话固定导致管理员账户被劫持 = 严重。更改密码后会话仍然有效 = 高危至严重(攻击者通过被盗 Cookie 持续访问,而受害者以为重置密码已撤销访问权限)。
高价值攻击链:
- 会话固定——服务器接受由客户端设置的会话 ID,且登录时不重新生成 → 攻击者预先植入 ID,受害者完成身份验证,攻击者利用现已通过身份验证的会话 → ATO 持续有效。
- 注销时未使会话失效——旧令牌在
/logout后仍可使用 → 窃取窗口永不关闭。 - 更改密码 / 电子邮件时未使会话失效——被盗会话在受害者“我好像被黑了,我来重置一下”之后仍然有效 → ATO 持续有效。这是收益最高的会话漏洞类别。
- 刷新令牌可重复使用且不检测轮换——泄露的刷新令牌可无限签发新的访问令牌;缺少重复使用检测意味着合法用户之后刷新令牌时,不会撤销攻击者的令牌分支。
- 会话 ID 可预测 / 熵低——使用连续值、时间戳或 userId 派生的 ID → 可暴力破解或计算其他用户的会话。
- 将 JWT 用作会话且没有
exp/ 撤销列表——JWT 被盗 = 永久访问;注销只是表面操作。
基础依据 — 塑造各阶段的模式
以下没有虚构的 CVE/报告 ID。这些是本技能编码的有名称、公开记录的模式:
- 会话固定、登录 CSRF、认证时未重新生成会话 — OWASP WSTG-SESS-03 / WSTG-SESS-01;经典的 ACROS / Mitja Kolšek 会话固定论文。影响最大的变体:固定 SSO/管理员用户的会话。
- SameSite=Lax 同级子域 CSRF 触及会话状态 — Argo CD CVE-2024-22424(从同级子域发起的顶级跨站导航会发送 Lax Cookie)。当会话 Cookie 仅依靠
SameSite=Lax防御 CSRF 时,使用此项。 - 刷新令牌轮换与自动重用检测 — Auth0/IETF OAuth-Security-BCP 模型:如果轮换后的刷新令牌遭到重放,必须使整个令牌族失效。缺少此机制 = 需要证明的核心漏洞。
- 设备绑定会话凭据(DBSC) — W3C/Chrome DBSC 草案将会话绑定到 TPM/设备密钥。测试降级:剥除 DBSC 挑战后,服务器是否仍接受未绑定的 Cookie?
- Cookie 属性强化 — OWASP WSTG-SESS-02;遵循 RFC 6265bis 的
__Host-/__Secure-前缀。只有存在真实 XSS/DOM 接收点时,缺少HttpOnly才算发现(与hunt-xss/hunt-dom串联)。 - 熵 — NIST SP 800-63B 要求会话标识符具有 ≥64 位熵。无论长度如何,任何可解码为计数器/时间戳/userId 的值都应视为发现。
交叉引用:ATO 串联 → hunt-ato;JWT alg/kid 篡改 → hunt-api-misconfig;OAuth code/state 漏洞 → hunt-oauth;CSRF 机制 → hunt-csrf;Cookie 窃取接收点 → hunt-xss / hunt-dom。
攻击面信号
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
逐步狩猎方法
双会话规则。 每项失效/固定声明都必须通过真实流程捕获的两个具体会话来证明 — 攻击者 A 和受害者 B — 绝不能使用硬编码的占位字符串。下面的辅助程序会从
curl的 Netscape Cookie jar 中捕获真实 Cookie。
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
}
阶段 1 — 会话固定(登录时重新生成)
# 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 "[email protected]&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
**误报防护:**值发生变化并不自动意味着安全 — 有些应用会轮换可读 Cookie,却保留由第二个 Cookie 标识的稳定服务器端会话。对完整的 Set-Cookie 集合进行差异比较,并确认旧值确实已失效(阶段 2)。另请确认 /api/me 返回的是你的身份,而不是通用的 200/着陆页。
阶段 2 — 注销时失效
# 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":"[email protected]","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"
误报防范(必需):
- 不要相信状态码。缓存/边缘节点的 200 响应或通用 SPA 框架页面对所有人都会返回 200。对比
AFTER与BEFORE的响应正文差异 — 只有当AFTER仍包含 A 的唯一身份标记(电子邮件、user-id、CSRF 令牌、账户名称)时,发现才成立。 - 使用负向对照确认:随机/垃圾 Cookie 值不得返回相同的已认证响应正文。如果垃圾值也能返回含用户数据的 200,说明该端点不受会话控制,此处不存在发现。
- 短暂延迟后并从不同 IP 重新测试 — 有些服务器会在下次访问时才延迟过期,或将会话绑定到 IP。
阶段 3 — 更改密码/电子邮件时失效(持久 ATO 核心)
# 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":"[email protected]","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":"[email protected]","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"
判定与误报防范:
- 只有当
AFTER返回 200,且响应正文仍包含账户的唯一数据(与BEFORE进行正文差异比较)时,才能确认发现。公开路由/SPA 路由单独返回 200 并不能作为证明。 - 再次运行垃圾 Cookie 负向对照,证明该端点受会话控制。
- 对更改电子邮件(
/settings/email)和注销所有设备重复完全相同的流程 — 应用经常会使操作会话(B)失效,却不使同级会话(A)失效。同级会话存活正是hunt-ato串联的持久 ATO 基础手段。 - **严重性门槛:**如果更改密码端点也缺少当前密码/MFA 二次验证步骤(参见
hunt-mfa-bypass),A 就能从只读访问转向完全接管 → 提升严重性。
阶段 4 — Cookie 属性分析
curl -sI -L "https://$TARGET/" | grep -i '^set-cookie'
- 缺少 HttpOnly → 可通过
document.cookie访问 Cookie。只有在与真实 XSS/DOM 接收点形成链路时才算发现(hunt-xss/hunt-dom)——记录下来,不要单独将其报告为高危。 - 缺少 Secure → Cookie 会通过明文 HTTP 发送;与
hunt-tls-network(降级/HSTS 缺口)结合,形成网络攻击者链路。 - 缺少 SameSite/设为
None→ 存在 CSRF 可达性;SameSite=Lax可通过同级子域的顶层导航绕过(Argo CD CVE-2024-22424 类型)→ 移交给hunt-csrf。 - 缺少
__Host-/__Secure-前缀 → 会话可从子域或非安全上下文中被覆盖/固定;若存在此前缀,基本就能杜绝 Cookie 固定,因此应将其缺失标记为阶段 1 的前置条件。
阶段 5 — 会话 ID 熵
# 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
接下来要分析,而不是凭直觉判断:
- 顺序递增 / 单调递增 — 对解码后的值执行
sort -n;稳定的 +1/+N 增量意味着可预测。 - 可解码的结构 — 对每个 ID 执行
base64 -d/ 十六进制解码,查找嵌入的userId、Unix 时间戳或 PID。 - 比特熵 — 将原始字节传给
ent或dieharder;NIST SP 800-63B 要求 ≥64 比特。10 个样本远不足以得出结论——应收集数百个。 - **误报防护:**看起来随机的长令牌并不能证明其强度;只有结构解码加上大样本熵估算才能证明。反过来,字符较少但每个字符熵较高的令牌也可能足够安全——应测量,而不是只数字符。
阶段 6 — JWT 作为会话
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或过期时间设为数年后 → 不会过期。缺少jti→ 服务端无法维护撤销列表 → 注销无法真正撤销令牌。 - 撤销测试:注销后,将同一个 JWT 重放到
/api/me。如果仍返回用户信息 → 令牌无法由服务端撤销;这就是 JWT 会话持久化问题。应对比响应正文,以避免缓存的 200 响应。 - 篡改(alg/kid/密钥混淆)由
hunt-api-misconfig负责——将jwt_tool $JWT -T/-X a移交给该工具,不要重复处理。
阶段 7 — 刷新令牌轮换与重用检测
# 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":"[email protected]","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}]'
发现:未轮换(RT1==RT2)= 可被盗取且长期有效的凭证;已轮换但未检测重用(重放 RT1 仍能铸造令牌,或 RT2 在重放后仍有效)= 根据 OAuth Security BCP,属于泄露令牌持久化漏洞。**带外说明:**若怀疑 RT 通过 SSRF/日志/JS 包泄露,应通过 hunt-ssrf/hunt-source-leak 确认令牌的可达性,而不是靠猜测。
阶段 8 — OAuth/SSO 会话关联与 DBSC 降级
# 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.
将 OAuth state/redirect_uri/代码注入问题移交给 hunt-oauth;本阶段仅涵盖会话层绑定。
链路表
| 会话发现 | 链接至 | 影响 |
|---|---|---|
会话固定(强制植入不带 __Host- 的 Cookie) | 诱使管理员/SSO 用户使用植入的 ID 进行身份验证 | 管理员会话被接管(严重) |
| 注销/密码更改后会话未失效 | hunt-xss/hunt-dom 窃取 Cookie → 重放仍有效的会话 | 受害者重置后仍可持续接管账户(ATO) |
| 刷新令牌,未检测重用 | 泄露的 RT(SSRF/日志/包)→ 无限铸造访问令牌 | 持续接管账户(ATO),更改密码后仍然有效 |
仅使用 SameSite=Lax | 同级子域顶层导航(CVE-2024-22424 类型)→ CSRF | 状态更改 / 登录 CSRF → 会话固定 |
JWT 缺少 exp/jti | 令牌被盗,服务端无法撤销 | 永久访问 |
| 接受 DBSC 降级 | 尽管有设备绑定,仍能窃取普通 Cookie | 使唯一的防盗缓解措施失效 |
| ID 可预测 | 计算/暴力破解其他用户的会话 | 跨用户接管账户(ATO) |
验证(house FP 规范)
在声称发现任何会话问题之前:
- 使用两个真实会话,不用占位值 — 每项固定/失效声明都必须使用通过上述
curl流程捕获的 A 和 B。 - 对比响应正文,绝不只看状态码 — 如果正文中没有该账户独有的身份标记,并且未与已认证基线进行差异对比,那么 200 毫无意义。
- 负向对照 — 垃圾/随机 Cookie 必须失败,而你所说的“仍有效”的 Cookie 必须成功;否则该端点不受会话控制,这不构成发现。
- 缓存/边缘节点检查 — 添加缓存破坏参数重新请求,并从第二个 IP 发起请求;排除边缘缓存或 IP 绑定导致的 200。
- 窃取攻击链需进行带外验证 — 如果影响取决于 Cookie/令牌被外传(XSS、SSRF、日志泄露),应通过带外方式(Collaborator)确认收到,而不是直接断言。
- 静态属性与实际状态 — 缺少
HttpOnly/Secure/SameSite属于策略观察;只有与真实利用原语(XSS、网络中间人、CSRF)结合后,才应报告为高危。单独的属性缺失属于低危/信息级。
严重性:
- 会话固定 → 管理员/SSO 接管:严重
- 更改密码/邮箱后会话未失效,或刷新令牌重放未检测:高危 → 严重(若 MFA/额外验证也缺失,则提高严重性)
- 会话 ID 可预测/重复:高危
- 登出后会话未失效:中危 → 高危(取决于窃取途径)
- 单独缺少
HttpOnly/SameSite:低危/信息级,直至与其他问题串联