关于 hunt-session
用于评估会话管理弱点的安全测试技能,覆盖会话固定、失效处理、令牌轮换和熵值分析。它提供分阶段方法、双会话检查及误报控制,帮助测试人员调查会话持久性与账户接管风险。
主要功能
- 01会话固定与登录后重新生成检查
- 02注销及凭据变更后的失效测试
- 03Cookie 属性与会话 ID 分析
- 04JWT 撤销和刷新令牌轮换检查
- 05双会话验证与误报控制
适用场景
- 01测试会话是否在注销或改密后仍然有效
- 02检查登录是否更换攻击者控制的会话 ID
- 03评估会话令牌可预测性和 Cookie 保护
- 04验证刷新令牌重用检测与 JWT 撤销
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ uphiago/ recon-skills --skill hunt-session安装后在 Claude Code 中输入/hunt-session确认已加载。
详细介绍
狩猎会话——会话管理
最高价值目标
会话固定导致管理员账号被劫持 = 严重。更改密码后会话仍然有效 = 高危至严重(攻击者可凭被盗 Cookie 持续接管账号,而受害者以为重置密码后已撤销该 Cookie)。
价值最高的攻击链:
- 会话固定——服务器接受客户端设置的会话 ID,且登录时不会重新生成 → 攻击者预先植入 ID,受害者完成身份验证后,攻击者利用已通过身份验证的会话 → 持续接管账号。
- 注销时未使会话失效——
/logout后旧令牌仍然有效 → 盗用窗口永不关闭。 - 更改密码 / 邮箱时未使会话失效——被盗会话在受害者“我好像被黑了,让我重置密码”后仍然有效 → 持续接管账号。这是收益最高的会话漏洞类别。
- 刷新令牌重复使用时未检测轮换——泄露的刷新令牌可以无限生成新的访问令牌;缺少重复使用检测意味着合法用户之后刷新令牌时,不会撤销攻击者的令牌分支。
- 会话 ID 可预测 / 熵低——顺序递增、由时间戳或用户 ID 派生的 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 位。无论长度如何,任何可解码为计数器 / 时间戳 / 用户 ID 的值都应视为漏洞。
交叉引用:账号接管攻击链 → 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -s -L -b "$JAR_A" "https://$TARGET/api/me")
# Logout A.
curl --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -s -L -H "Cookie: $A" "https://$TARGET/api/me" -w '\n[%{http_code}]')
echo "AFTER: $AFTER"
FP 原则(必须遵守):
- 不要相信状态码。缓存/边缘缓存返回的 200,或通用 SPA 外壳,可能会对所有人都返回 200。对比
AFTER与BEFORE的响应正文差异——只有当AFTER仍包含 A 的唯一身份标记(电子邮件、用户 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -s -L -H "Cookie: $SESSION_A" "https://$TARGET/api/profile" -w '\n[%{http_code}]')
echo "AFTER pw-change: $AFTER"
判断与 FP 原则:
- 只有当
AFTER返回 200 且正文仍包含账户的唯一数据(与BEFORE对比正文差异)时,才能确认发现。公共路由/SPA 路由仅返回 200 并不能证明问题存在。 - 再次运行无效 Cookie 负向对照,证明该端点受会话门控。
- 对修改电子邮件(
/settings/email)和注销所有设备重复相同流程——应用经常会使当前操作会话(B)失效,却不会使同一账户的其他会话(A)失效。这种其他会话仍存活的情况,正是hunt-ato会利用的持久 ATO 基础条件。 - **严重性门槛:**如果修改密码端点也缺少当前密码/MFA 二次验证(参见
hunt-mfa-bypass),A 就能从只读访问转向完全接管 → 提升级别。
阶段 4 — Cookie 属性分析
curl --max-time 30 --connect-timeout 10 -sI -L "https://$TARGET/" | grep -i '^set-cookie'
- 缺少 HttpOnly → Cookie 可通过
document.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 --max-time 30 --connect-timeout 10 -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 个样本远不足以得出结论——应收集数百个样本。 - **FP 防护:**看起来足够长且随机的令牌并不能证明其强度;只有结构解码加上大样本熵估算才能证明。反过来,字符较少但每字符熵高的令牌也可能没问题——应进行测量,而不是数字符。
阶段 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→ 服务端无法维护撤销列表 → 注销无法真正撤销令牌。 - **撤销测试:**注销后,针对
/api/me重放同一个 JWT。如果仍返回用户数据 → 令牌无法由服务端撤销;这就是 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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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 --max-time 30 --connect-timeout 10 -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,这属于泄露令牌持久化漏洞。**带外说明:**如果怀疑 SSRF/日志/JS 包中泄露了 RT,请使用 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 --max-time 30 --connect-timeout 10 -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/日志/捆绑包)→ 无限生成访问令牌 | 持续接管账户,密码更改后仍有效 |
仅使用 SameSite=Lax | 同级子域名顶层导航(CVE-2024-22424 类)→ CSRF | 状态更改 / 登录 CSRF → 会话固定 |
JWT 无 exp/jti | 令牌被盗,服务端无法撤销 | 永久访问 |
| 接受 DBSC 降级 | 尽管有设备绑定,仍可窃取普通 Cookie | 绕过唯一的防盗措施 |
| ID 可预测 | 计算/暴力破解其他用户的会话 | 跨用户接管账户 |
验证(严格控制误报)
报告任何会话发现之前:
- 使用两个真实会话,而非占位符 — 每个会话固定/失效声明都必须使用通过上述
curl流程捕获的 A 和 B。 - 对比响应正文,绝不能只看状态码 — 若响应正文中没有账户的唯一身份标记,且未与已认证基线进行差异比较,那么 200 毫无意义。
- 负向对照 — 垃圾/随机 Cookie 必须失败,而你的“仍有效” Cookie 必须成功;否则该端点并未受会话保护,不构成发现。
- 缓存/边缘检查 — 使用缓存破坏参数重新请求,并从第二个 IP 发起请求;排除边缘缓存或 IP 绑定导致的 200。
- 盗窃链需带外验证 — 当影响取决于外泄 Cookie/令牌(XSS、SSRF、日志泄露)时,应通过带外方式(Collaborator)确认已收到,而非仅作断言。
- 静态属性与状态验证 — 缺少
HttpOnly/Secure/SameSite属于策略观察结果;只有与真实攻击原语(XSS、网络中间人攻击、CSRF)结合后,才应报告为高危。单独的属性缺失属于低危/信息性问题。
严重程度:
- 会话固定 → 管理员/SSO 接管:严重
- 更改密码/电子邮件后未失效,或未检测刷新令牌重用:高危 → 严重(若 MFA/升级验证也缺失,则提高严重程度)
- 可预测/重复的会话 ID:高危
- 登出后未失效:中危 → 高危(取决于盗窃途径)
- 单独缺少
HttpOnly/SameSite:低危/信息性,直至与其他问题形成攻击链
验证
运行此自测,确认会话测试已准备就绪:
-
技能完整性 — 确认技能文件可读且格式正确:
grep -q "name: hunt-session" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL" grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL" -
类别检查 — 确认技能具有类别:
grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL" -
陷阱部分 — 确认已记录陷阱:
grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
这 3 项测试可验证技能结构正确且可供使用。
陷阱
- 登录前没有会话时的会话固定 — 如果应用在登录后会分配新的会话 ID,则无法进行会话固定。测试登录前后的会话 ID 行为。
- URL 中包含会话 ID —
?sessionid=xxx会通过 Referer 标头泄露。应证明 Referer 会泄露到第三方域名。 - 缺少 HttpOnly 但没有 XSS — 缺少 HttpOnly 属于纵深防御问题;若未证明存在 XSS,则不构成漏洞。
- 会话超时时间过长 — 会话超时时间过长属于策略问题,而非漏洞(除非同时存在物理访问或共享设备场景)。