Imported from sickn33/agentic-awesome-skills (
skills/hunt-session/SKILL.md). Install upstream withnpx skills add sickn33/agentic-awesome-skills --skill hunt-session. Copyright stays with the author (MIT).
⚠️ AUTHORIZED USE ONLY This skill is for educational purposes or authorized security assessments only. You must have explicit, written permission from the system owner before using this tool. Misuse of this tool is illegal and strictly prohibited.
Mandatory confirmation gate Before running any command that probes, exploits, changes, persists on, extracts data from, or attempts credential access against a target:
- Ask the user to state the exact target URL, IP, account, or resource.
- Ask the user to confirm written authorization and the permitted scope.
- Show the exact command(s) and explain their expected effect.
- Wait for explicit confirmation in the current conversation.
Without that confirmation, remain read-only and provide defensive guidance only. Prefer a sandbox, disposable VM, or controlled lab.
Autonomous Testing Priority
Missing HttpOnly on cookies may be detected by separately configured tooling — 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,POST /api/logout, etc.) - Try to use the OLD session token to access a protected resource (
/api/me,/dashboard,/account) - If protected data remains accessible with the old token after confirmed logout, investigate the revocation boundary and expected policy
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 unchanged, verify an attacker can pre-establish/control the same authenticated session before claiming fixation
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 protected data remains accessible, compare the documented session-revocation policy and attack preconditions before assigning impact
Content-type: Login and session endpoints vary — use application/json for REST APIs, application/x-www-form-urlencoded for traditional web forms. Try both if the first returns an unexpected response.
Proof: A protected-resource 200 response (with user data) using a session token that should have been invalidated confirms the finding.
HUNT-SESSION — Session Management
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
/logout→ theft window never closes. - No invalidation on password / email change — a stolen session survives the victim's "I think I was hacked, let me reset" → persistent ATO. Impact and bounty eligibility depend on the program and demonstrated threat model.
- Refresh-token reuse without rotation-detection — a leaked refresh token may mint access tokens within its enforced lifetime; 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
exp/ no revocation list — inspect server-enforced lifetime and revocation; claims alone do not establish permanent access.
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.
- Same-parent-domain CSRF reaching session state — Argo CD CVE-2024-22424 (attacker-controlled HTML on the same parent domain; inspect the advisory’s content-type/CSRF preconditions). Use this when a session cookie relies on
SameSite=Laxas its only CSRF defence. - Refresh-token rotation & automatic reuse-detection — the Auth0/IETF OAuth-Security-BCP model: public-client refresh tokens require rotation with replay detection or sender-constraining; test the implemented protection and replay policy.
- 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-/__Secure-prefixes per RFC 6265bis. MissingHttpOnlycan be a hardening finding; high-impact theft claims require an actual applicable attack path (chain withhunt-xss/hunt-dom). - Entropy — NIST SP 800-63B-4 requires session-binding secrets from an approved random bit generator and at least 64 bits in length. Decodable claims alone are not a finding: signed/encrypted tokens can contain predictable claims while preserving integrity and sufficient unpredictable secrets.
Cross-refs: ATO chaining → hunt-ato; JWT alg/kid tampering → hunt-api-misconfig; OAuth code/state flaws → hunt-oauth; CSRF mechanics → hunt-csrf; cookie-theft sinks → hunt-xss / hunt-dom.
Attack 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)
# 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
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
curl's Netscape jar.
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
}
Phase 1 — Session Fixation (regeneration-on-login)
# 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 "username=attacker@example.com&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 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.
Phase 2 — Invalidation on Logout
# 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"
# 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 discipline (mandatory):
- Don't trust the status code. A cached/edge 200 or a generic SPA shell returns 200 for everyone. Body-diff
AFTERagainstBEFORE— the finding is only real ifAFTERstill 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 user data, stop and investigate a possible separate unauthenticated disclosure/cache flaw; it does not prove session replay.
- Re-test after a short delay and from a different IP — some servers lazily expire on next access or pin sessions to IP.
Phase 3 — Invalidation on Password / Email Change (persistent-ATO core)
# 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":"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").
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
# 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"
Decision + FP discipline:
- Finding is confirmed only if
AFTERreturns 200 and the body still carries the account's unique data (body-diff vsBEFORE). 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 primitivehunt-atochains. - 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.
Phase 4 — Cookie Attribute Analysis
curl -sI -L "https://$TARGET/" | grep -i '^set-cookie'
- HttpOnly missing → cookie reachable via
document.cookie. A hardening observation whose severity requires context; a demonstrated XSS/DOM sink can establish a theft chain (hunt-xss/hunt-dom) — note it, don't report standalone as High. - Secure missing → cookie sent over cleartext HTTP; pair with
hunt-tls-network(downgrade/HSTS-gap) for a network-attacker chain. - SameSite missing/
None→ CSRF reachability; sibling subdomains may be same-site, so Lax alone is not sufficient (Argo CD CVE-2024-22424 class) → hand tohunt-csrf. __Host-/__Secure-prefix absent → record cookie scope and transport hardening; absence alone does not prove overwrite or fixation, and presence does not replace lifecycle validation.
Phase 5 — Session-ID Entropy
# 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 need investigation: stable sessions/rate limits/error responses can repeat without cross-user reuse
awk '{print length($0)}' "$SAMP" | sort -n | uniq -c # length distribution
Then analyse, don't eyeball:
- Sequential / monotonic —
sort -nthe decoded values; a steady +1/+N delta = predictable. - Decodable structure —
base64 -d/ hex-decode each ID and look for embeddeduserId, unix timestamps, or PIDs. - Bit entropy — feed the raw bytes to
entordieharder; Small samples cannot establish cryptographic entropy; review generation design and the applicable standard. 10 samples is far too few to claim anything — gather hundreds. - FP guard: a long random-looking token is not proof of strength; statistical samples cannot prove cryptographic unpredictability; inspect the generation design where possible. Conversely a short token with high per-char entropy may still be fine — measure, don't count characters.
Phase 6 — JWT-as-Session
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
expmissing or years out → inspect enforced server-side lifetime; claims alone do not prove permanent access.jtimissing does not prove absent revocation: servers can revoke by token hash, session, user version or other state.- 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 offjwt_tool $JWT -T/-X athere rather than duplicating it.
Phase 7 — Refresh-Token Rotation & Reuse-Detection
# 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')
# 2) Use RT1 to mint a new access token — check the configured protection: rotation or sender-constraining; this example tests rotation only.
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}]'
Interpretation: no rotation requires checking sender-constraining and client type; 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.
Phase 8 — OAuth/SSO Session Linkage & DBSC Downgrade
# 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,
# inspect the refresh challenge and cookie lifetime; acceptance alone does not demonstrate a DBSC downgrade.
Hand OAuth state/redirect_uri/code-injection to hunt-oauth; this phase only covers the session-layer binding.
Chain Table
| Session finding | Chain to | Impact |
|---|---|---|
Session fixation (forced __Host--less cookie) |
Trick admin/SSO user into authenticating on planted ID | Admin session takeover (Critical) |
| No logout/password-change invalidation | hunt-xss/hunt-dom cookie theft → replay surviving session |
Persistent ATO past victim's reset |
| Refresh token, no reuse-detection | Leaked RT (SSRF/log/bundle) → access-token minting within the demonstrated lifetime | Persistent ATO, survives password change |
SameSite=Lax only |
Sibling-subdomain top-level nav (CVE-2024-22424 class) → CSRF | State change / login-CSRF → fixation |
| JWT lifetime/revocation failure | Demonstrated replay after expected expiry/revocation | Demonstrated post-logout access within observed lifetime |
| 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 |
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
curlflows above. - 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 investigate a separate access-control/cache issue rather than asserting session replay.
- 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/Secure/SameSiteabsence 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.
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
HttpOnly/SameSitestandalone: Low/Informational until chained
When to Use
- You have explicit, written authorization to assess the target in scope, and the task matches this skill's vulnerability class or technique within a bug-bounty or penetration-test engagement.
- You need the recon, exploitation, or validation workflow described below — executed strictly inside the approved scope.
Limitations
- Authorized scope only: the confirmation gate above is mandatory before any probing, exploitation, or credential-access command.
- Docs-only import: upstream helper scripts, commands, engine, and research assets are not bundled; reinstall tooling from the source repo when needed.
- Validate every finding (see
triage-validation) before reporting; report viareport-writing. Prefer a sandbox, disposable VM, or controlled lab.
Example
# Read-only first step; confirm scope before anything active.
cat scope.txt # target list from the authorized engagement brief
Adapted from elementalsouls/Claude-BugHunter (MIT); frontmatter, When to Use/Limitations, and safety boundaries added for upstream compliance. Docs-only import: executable helpers, commands, engine, and research assets not bundled.
Interpretation and evidence limits
These commands are endpoint-specific sketches, not an automatic detector. Use only controlled test accounts and pre-approved routes/rates; do not recruit or trick real users. Keep cookies, credentials and response bodies in private temporary artifacts rather than console/chat output; redact evidence and clean it up under the engagement retention rules. Do not follow redirects outside the authorized origin or send credential bodies across an unexpected redirect. The cookie extractor ignores domain/path and can select the wrong cookie; validate the exact cookie scope and include any companion cookies. A stable pre/post cookie, absent cookie prefix, surviving password-change session or absent jti is not independently proof of takeover; establish attacker controllability, expected revocation semantics and real protected data access. Sender-constrained refresh tokens can satisfy RFC 9700 without rotation; apply the appropriate client threat model. IdP and RP sessions can be independent unless single logout is promised. DBSC deliberately allows short-lived cookies: plain-cookie acceptance before expiry does not by itself defeat refresh-time device binding. SameSite is site-based, so sibling subdomains may be same-site; do not infer a general Lax bypass from a named CVE without reproducing its actual preconditions. Statistical tests on hundreds of tokens cannot certify entropy or the RNG. Severity is evidence/program dependent, not fixed by these tables.
