hunt-ssrf
# hunt-ssrf This Claude Code skill identifies Server-Side Request Forgery (SSRF) vulnerabilities by analyzing attack vectors from fifteen documented bug bounty cases, including cloud metadata endpoints, DNS rebinding techniques, and headless browser exploitation chains. Deploy when testing web applications for SSRF, prioritizing cloud-hosted SaaS platforms, Kubernetes clusters, internal developer tools, and URL-fetching features, while requiring out-of-band confirmation through Burp Collaborator or similar mechanisms to validate blind SSRF cases.
git clone --depth 1 https://github.com/elementalsouls/Claude-BugHunter /tmp/hunt-ssrf && cp -r /tmp/hunt-ssrf/skills/hunt-ssrf ~/.claude/skills/hunt-ssrfSKILL.md
## Crown Jewel Targets
SSRF is highest-value when the target runs on cloud infrastructure (AWS, GCP, Azure) where metadata services expose credentials, or when the server sits inside a complex internal network (Kubernetes clusters, microservice meshes, internal APIs). Priority targets:
- **Cloud-hosted SaaS products** (GCP metadata at `169.254.169.254` or `metadata.google.internal`, AWS IMDSv1)
- **Kubernetes/orchestration platforms** — aggregated API servers, metrics-server, kubelet endpoints expose privileged cluster operations
- **Internal developer tooling** — CI/CD, workflow orchestration (Flyte, Argo), admin panels not exposed externally
- **Link preview / URL fetching features** — Reddit-style preview APIs, Slack-style unfurling, media processors
- **Dataset/file import pipelines** — anything that fetches remote URLs on behalf of a user
- **Enterprise self-hosted software** (GitHub Enterprise, GitLab) — SSRF frequently chains to RCE via internal services
Payouts are highest when SSRF reaches: cloud credentials → account takeover, internal admin APIs → data exfil, or chains to RCE.
---
## OOB-Or-It-Didn't-Happen Gate (Read First)
**Claims of blind SSRF require an out-of-band (OOB) confirmation. Always. No exceptions.**
OOB means: a Burp Collaborator domain, an `interactsh-client` listener, a canarytoken, or any DNS+HTTP receiver you control that confirms the server actually made an outbound network connection on your behalf.
### What is NOT confirmation of SSRF
- The server **echoing your URL back in an error message**. Example: `"The Web application at http://evil.example.com/x could not be found"` — this is the server formatting your input into an error string, NOT making an outbound HTTP request. The error came from string formatting, not from network failure.
- The server returning a different status code for an external URL vs `localhost`. Different error responses can come from URL-scheme validators, not from actual fetching.
- A delayed response when the URL is sent. Delay can come from DNS resolution attempts within the parser, not from completed HTTP fetches.
### What IS confirmation of SSRF
- A DNS lookup for your unique Collaborator subdomain appears in the OOB listener.
- An HTTP request to your Collaborator HTTP endpoint with the server's source IP and User-Agent.
- For SSRF in JavaScript-execution contexts (PDF renderers, headless browsers), a fetch from the server to your callback URL.
### Default workflow
1. **Plant the Collaborator payload first.** Sub-tagging (`dlsrcurl.<collab>`,
`import.<collab>`) only works if your listener actually reports the queried
subdomain back to you — **verify that before relying on it.** Burp's
`get_collaborator_interactions` keys results by **payload ID, not by subdomain**,
so several sub-tags generated from one payload are indistinguishable in the
output. When that is the case, **generate a fresh payload per candidate
parameter** and send exactly one request per payload.
2. **Send the request** to the target endpoint.
3. **Wait 30–120 seconds**, then poll the OOB listener.
4. **Only after a confirmed callback** do you claim SSRF.
5. If zero callbacks across all sub-tagged sinks: SSRF claims must be retracted, even if error messages echo URLs.
**Lesson from a authorized engagement:** SharePoint's `/_layouts/15/download.aspx?SourceUrl=` returned 500 with the title `"The Web application at <attacker-URL> could not be found"`. Initial scan flagged this as SSRF (server clearly processed the URL). 38 Collaborator-tagged payloads across 12+ URL-accepting parameters yielded **zero DNS or HTTP interactions**. The "echo" was client-side error-string formatting; the server never made an outbound HTTP request. The path is actually an SP-internal `SPFile`/`SPWebApplication` resolver, not a generic URL fetcher. Reporting this as SSRF would have been N/A'd at triage.
### Attribute the callback to ONE parameter before reporting
A callback proves the server made a request. It does **not** tell you which
parameter caused it, and the fix depends entirely on that.
```
BAD — four candidate fields, one payload, fired in one batch
-> callbacks arrive, attribution impossible, retest required
GOOD — fresh payload per field, one request each, poll between
url -> callbacks <- this is the sink
apiUrl -> none
endpoint -> none
target -> none
```
**Run the negative control.** A parameter that produces *no* callback is evidence,
and it belongs in the report — it is what lets the client fix the right field
instead of allowlisting the wrong one.
**Lesson from an authorized engagement.** A server-side request-forwarding endpoint
accepted both `url` and `apiUrl`. The application's own stored config used `apiUrl`,
so that was the obvious suspect — but `apiUrl` was inert and **`url` was the live
sink**.
Batch-firing both had produced callbacks with no attribution; only per-payload
isolation identified the real parameter. A report naming `apiUrl` would have sent
the client to patch a field that does nothing.
### Blind vs full-read — establish which before scoring
After a callback confirms the request leaves the server, **check whether the
upstream response body is returned to you.** These are different findings:
- **Blind** (callback only, no body): on the never-submit list standalone. Needs an
internal service reached, or data returned, to be reportable.
- **Full-read** (upstream body in the response): substantially higher severity —
read arbitrary internal endpoints directly.
```bash
# one request settles it: fetch something with a known, recognisable body
-d '{"url":"https://example.com/"}'
# {"statusCode":200,"data":"<!doctype html>...<title>Example Domain</title>..."}
# ^ body returned = full-read, not blind
```
Also body-diff a known-internal target against a known-external one. A **distinct
status** on a link-local address (e.g. `401` fromRun 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. Pulls NVD/GitHub-Advisory CVEs + bundled disclosed reports + hunt memory context. Usage: /intel target.com
Inspect or rotate the autopilot ledger JSONL files (findings.jsonl, negatives.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 and untested surface from the autopilot ledger. 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
Optional manual note on a target or the last confirmed finding. Capture is automatic during autopilot; this is for extra context. Usage: /remember