hunt-session について
hunt-sessionは、セキュリティテスト担当者向けのセッション管理脆弱性調査スキルです。セッション固定、ログアウトや認証情報変更後も有効なトークン、ローテーション、Cookie属性を調べ、実セッションと陰性対照による検証を重視します。
主な機能
- 01ログイン時のセッション再生成をテスト
- 02ログアウトや認証情報変更後の無効化を確認
- 03Cookie属性とセッションIDのエントロピーを分析
- 04JWTセッションの有効期限と失効を確認
活用シーン
- 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 盗難と組み合わさると、重大な連鎖攻撃になります)
Content-type: ログインとセッションのエンドポイントによって異なります。REST API には application/json、従来型の Web フォームには application/x-www-form-urlencoded を使用してください。最初のリクエストで予期しない応答が返された場合は、両方を試してください。
証拠: 無効化されているはずのセッショントークンを使用して、保護されたリソースからユーザーデータを含む 200 応答が返されれば、問題が確認できます。
HUNT-SESSION — セッション管理
最重要ターゲット
管理者の乗っ取りにつながるセッション固定はクリティカルです。パスワード変更後もセッションが有効な場合は、高リスクからクリティカルに該当します(被害者がパスワードをリセットして無効化したと思っている、盗まれた Cookie による ATO の持続)。
特に価値の高い攻撃の連鎖:
- セッション固定 — サーバーがクライアントによって設定されたセッション ID を受け入れ、ログイン時に再生成しない → 攻撃者が ID を事前に仕込み、被害者が認証し、攻撃者が認証済みセッションを乗っ取る → ATO が持続します。
- ログアウト時に無効化されない —
/logout後も古いトークンが使える → 盗難の危険にさらされる期間が終わりません。 - パスワード/メールアドレス変更時に無効化されない — 盗まれたセッションが、被害者の「ハッキングされたかもしれない。パスワードをリセットしよう」という対応後も有効です → ATO が持続します。これはセッションの脆弱性の種類の中で、報奨金が最も高いものです。
- ローテーション検出なしのリフレッシュトークン再利用 — 漏えいしたリフレッシュトークンから、いつまでも新しいアクセストークンを発行できます。再利用を検出しない場合、正規ユーザーが後でトークンを更新しても、攻撃者のトークン系列は失効しません。
- 予測可能/エントロピーの低いセッション ID — 連番、タイムスタンプ、または userId 由来の ID → ブルートフォースや計算によって、他のユーザーのセッションを取得できます。
expも失効リストもない JWT をセッションとして使用 — 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のモデルでは、ローテーション済みのリフレッシュトークンが再利用された場合、トークンファミリー全体を無効化する必要があります。これがないことが、立証すべき核心的な問題です。
- Device Bound Session Credentials(DBSC) — W3C/ChromeのDBSCドラフトでは、セッションをTPM/デバイスキーに紐付けます。テストすべきはダウングレードです。DBSCチャレンジが取り除かれたとき、サーバーは依然として紐付けのないCookieを受け入れるでしょうか。
- Cookie属性の強化 — OWASP WSTG-SESS-02、RFC 6265bisに基づく
__Host-/__Secure-プレフィックスです。HttpOnlyがないことが問題となるのは、実際のXSS/DOMシンクが存在する場合に限られます(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
ステップごとの調査方法
2セッションルール。 無効化/固定に関する主張はすべて、実際のフローで取得した具体的な2つのセッションで立証します。攻撃者のAと被害者のBを使用し、ハードコードされたプレースホルダー文字列は決して使いません。以下のヘルパーは、
curlのNetscape 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
FP防止策: 値が変化したからといって、自動的に安全とは限りません。読み取り可能なCookieはローテーションする一方で、2つ目のCookieをキーとするサーバー側セッションは維持するアプリもあります。Set-Cookieのセット全体を比較し、古い値が本当に無効になっていることを確認してください(フェーズ2)。また、/api/meが汎用的な200応答/ランディングページではなく、あなた自身のIDを返すことも確認してください。
フェーズ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"
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 -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"
判定とFPを避けるための手順:
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)に限ります。記録はしますが、単独で High として報告しないでください。 - Secure がない → Cookie が暗号化されていない HTTP で送信されます。ネットワーク攻撃者による連鎖(ダウングレード/HSTS の不備)については、
hunt-tls-networkと組み合わせます。 - 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/16 進デコードし、埋め込まれた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がない → サーバーで失効リストを維持できないため、ログアウトしても実際には失効させられません。- **失効テスト:**ログアウト後、同じ 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 に定められた漏えいトークン永続化のバグです。**OOB に関する注記:**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 -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 で認証させる | 管理者セッションの乗っ取り(Critical) |
| ログアウト/パスワード変更時に無効化されない | 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 の規律)
セッションに関する所見を報告する前に、必ず以下を確認してください。
- プレースホルダーではなく、実際のセッションを2つ使う — 固定化/無効化に関するすべての主張では、上記の
curlフローで取得した A と B を使用します。 - ステータスだけでなく、本文を比較する — 200 であっても、本文にアカウント固有の識別マーカーが含まれ、それを認証済みベースラインと比較していなければ意味がありません。
- ネガティブコントロール — 「有効なまま残る」Cookie で成功するエンドポイントでは、デタラメなランダム Cookie は失敗しなければなりません。そうでなければ、そのエンドポイントはセッションで制御されておらず、所見には該当しません。
- キャッシュ/エッジの確認 — キャッシュバスターを付けて再リクエストし、2つ目の IP からも確認します。エッジキャッシュまたは IP 固定による 200 ではないことを確かめます。
- 窃取チェーンでは OOB を確認する — Cookie/トークンの流出(XSS、SSRF、ログ漏えい)によって影響が生じる場合は、断定するのではなく、帯域外(Collaborator)で受信を確認します。
- 静的な設定と状態を区別する —
HttpOnly/Secure/SameSiteが設定されていないことは、ポリシー上の所見です。High として報告するのは、実際に悪用可能なプリミティブ(XSS、ネットワーク経由の MITM、CSRF)と組み合わせた場合に限ります。属性の欠落だけであれば、Low/Informational です。
重大度:
- セッション固定化 → 管理者/SSO の乗っ取り:Critical
- パスワード/メールアドレス変更時に無効化されない、またはリフレッシュトークンの再利用が検知されない:High → Critical(MFA/ステップアップ認証もない場合は重大度を引き上げます)
- 予測可能/重複するセッション ID:High
- ログアウト時に無効化されない:Medium → High(窃取の手段によります)
HttpOnly/SameSiteの欠落のみ:チェーン化されるまでは Low/Informational