mcpField
H
hunt-session

hunt-session

github.com/uphiago安全1.3k43 次安装8 周前更新
代码仓库

关于 hunt-session

用于评估会话管理弱点的安全测试技能,覆盖会话固定、失效处理、令牌轮换和熵值分析。它提供分阶段方法、双会话检查及误报控制,帮助测试人员调查会话持久性与账户接管风险。

主要功能

  1. 01会话固定与登录后重新生成检查
  2. 02注销及凭据变更后的失效测试
  3. 03Cookie 属性与会话 ID 分析
  4. 04JWT 撤销和刷新令牌轮换检查
  5. 05双会话验证与误报控制

适用场景

  1. 01测试会话是否在注销或改密后仍然有效
  2. 02检查登录是否更换攻击者控制的会话 ID
  3. 03评估会话令牌可预测性和 Cookie 保护
  4. 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:低危/信息性,直至与其他问题形成攻击链

验证

运行此自测,确认会话测试已准备就绪:

  1. 技能完整性 — 确认技能文件可读且格式正确:

    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"
    
  2. 类别检查 — 确认技能具有类别:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. 陷阱部分 — 确认已记录陷阱:

    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,则不构成漏洞。
  • 会话超时时间过长 — 会话超时时间过长属于策略问题,而非漏洞(除非同时存在物理访问或共享设备场景)。

评论

支持 Markdown