gRPC Service Enumeration and Exploitation
Disable gRPC reflection or face automated exposure of your entire attack surface.

gRPC combines HTTP/2 binary framing, protobuf schema enforcement, and remote procedure call semantics into a transport that is structurally different from REST. Protobuf serializes payloads as a compact binary wire format by default. It does support a canonical JSON encoding for interoperability, but most production gRPC traffic never touches that format. A WAF, API gateway, or proxy built to inspect JSON bodies simply cannot parse what's inside a protobuf frame. That means no visibility into the parameters an attacker controls, no pattern matching on malicious input, no logging that means anything to a human reading it later.
gRPC does include a mechanism for listing available endpoints, called Server Reflection, but it isn't on by default and has to be explicitly enabled by the server author. Many teams disable it, or never enable it, in production. So finding out what a gRPC service can actually do takes deliberate effort. There's no passive observation that reveals it, unlike a REST API where browsing a Swagger doc or watching traffic in a browser's network tab hands over the map for free.
The authentication model compounds the problem. gRPC enforces authentication and authorization at the method level, not the route level. A single service can protect one RPC with a strict role check and leave the next one wide open, and nothing about the protocol forces consistency between them. Developers can get this wrong easily, and because there's no central gatekeeper comparable to a route table, the inconsistency stays invisible until someone goes looking for it method by method.
Protobuf's strong typing adds a layer of false confidence. The schema guarantees that a message has the right fields in the right format. It says nothing about whether the caller sending that message is allowed to, or whether the business logic behind the RPC is sound. Structure and authorization are two different problems, and protobuf only solves the first one.
Most penetration testing tooling was built around REST assumptions: URL paths, query parameters, JSON bodies, cookie-based sessions. That tooling runs into trouble immediately against binary payloads, streaming RPCs, and HTTP/2 metadata headers carrying authentication tokens. None of those adapt to gRPC without real changes to the testing approach. A tester who applies REST-era instincts and REST-era tools to a gRPC service will miss entire categories of exposure, because the tooling never looked in the right place to begin with. gRPC needs its own enumeration methodology, starting with finding the services.
Finding gRPC services before testing them: port scanning and protocol fingerprinting
You locate gRPC services by targeting a non-standard set of ports, then you confirm the protocol through HTTP/2 negotiation behavior rather than by reading response bodies, because gRPC doesn't return the kind of human-readable content that gives REST APIs away. Port 443 over TLS is common for gRPC behind API gateways and ingress controllers. You'll see port 50051 constantly, because it's the plaintext gRPC default in development and internal networks. Port 80 sometimes carries h2c or a gRPC-Web gateway, and port 8080 is common on development services and proxies. Port 8443 serves as an alternate TLS endpoint, port 50052 appears as a secondary plaintext service port in the wild, and port 9090 is common in microservice deployments.
So the first step is to run Nmap with service-version detection against that port list, across individual hosts and whole subnets, with the open-port filter on so live candidates surface. From there, confirming the protocol means checking how TLS negotiates. gRPC over TLS uses ALPN to negotiate HTTP/2, so openssl s_client -alpn h2 -connect target.com:443 and curl --http2 -vk both confirm that negotiation is happening. For plaintext h2c, curl --http2-prior-knowledge -v does the same job without a TLS handshake to lean on.
Reflection being disabled doesn't always mean the trail goes cold. Servers frequently still respond to reflection requests with specific error codes that confirm gRPC is running underneath, even when the reflection service itself won't hand over a service list. That response is itself a fingerprint worth recording during this reconnaissance stage, because it sets up the enumeration work that follows.
Service enumeration when reflection is on: the production misconfiguration that hands attackers everything
Server Reflection is an API the gRPC server exposes so clients can query available services and methods dynamically. It exists purely for development convenience, and it carries no authentication of its own. When reflection ships to production, which happens routinely because teams enable it during development for tooling convenience and then forget to gate it by environment, an unauthenticated attacker can pull a complete, self-documenting map of every service, method, and message schema on the server in under five commands.
The sequence runs through grpcurl. Listing all services exposed by the target takes one line: grpcurl -insecure target.com:443 list. Describing a specific service reveals its methods: grpcurl -insecure target.com:443 describe package.ServiceName. Drilling into a single method shows its exact signature: grpcurl -insecure target.com:443 describe package.ServiceName.MethodName. The request and response message structures come out the same way: grpcurl -insecure target.com:443 describe package.GetItemRequest. And the entire structure can be dumped to a file in one pass by piping the service list into a loop: grpcurl -insecure target.com:443 list | while read service; do grpcurl -insecure target.com:443 describe "$service"; done > grpc-reflection.txt. For testers who prefer an interactive session over scripted calls, Evans offers a REPL: evans --host target.local --port 50051 --reflection repl.
What comes out of that handful of commands is everything a tester, or an attacker, needs to start working. You need exact, case-sensitive service and method names for every call that follows. Full input and output message schemas. You can build authorization tests and injection attempts with that much structural knowledge, without ever seeing a line of internal documentation. Five commands, no credentials, and the attack surface is mapped end to end.
The root cause here is a gap in release discipline, not a bug in gRPC. Reflection has to be deliberately enabled in virtually every gRPC implementation. Someone turned it on, almost always for local grpcurl convenience during development, and the same deliberate step needed to gate or strip it before production never happened. A service that has that step performed requires real reconnaissance work; a service that misses it hands over its own blueprint for free. The harder and more realistic case is what happens when that convenience is switched off; enumeration then has to work for its results.
Service enumeration when reflection is off: error-code differential and automated wordlist scanning
Disabling reflection removes the free map, but gRPC's error-code behavior still leaks which services and methods exist, and that leak can be automated into a full enumeration of the attack surface starting from nothing but a wordlist. The core insight is that gRPC gives back different, distinguishable error codes depending on what's actually on the server. A request against a name that doesn't exist at all comes back "unknown service FakeService." A request against a real service with a method name that isn't real comes back "unknown method FakeMethod for service UserService," which confirms the service exists even though the method doesn't. And a request against a method that genuinely exists, sent without credentials, comes back as an authentication failure, "Unauthenticated: missing authentication token," which confirms both the service and the method are real and simply gated behind a credential.
That differential is what made a real-world grpc-scan engagement work. Testers handed an obfuscated client SDK with no documentation and reflection turned off still identified six live services, proto.PingService, proto.UserService, proto.AuthService, proto.SecureService, proto.HelloService, and proto.ProductService, by running a 73-name wordlist against the target at roughly 8,900 checks per second. The tool doesn't just brute-force raw names either. Starting from a base word like "User," it generates the naming patterns real engineering teams actually use, UserService, Users, UserAPI, user.User, api.v1.User, com.company.User, and tests each variant against a set of common method names: Get, List, Create, Update, Delete, Search, Find.
That throughput is possible because of how HTTP/2 handles connections. A single TCP connection can carry somewhere around 100 to 200 concurrent requests, governed by the SETTINGS_MAX_CONCURRENT_STREAMS parameter, so a scanner can fire thousands of checks per second over one connection without tripping the kind of connection-count limits that would catch a traditional port-by-port scan.
Confirming that a service exists is only half the job. Once you confirm a method, you still need the real proto definition, or you reverse engineer the SDK's serialization logic, or you simply guess, to craft a valid protobuf request against it. The scanning tool surfaces the target. It does not hand over a working payload. Closing that gap usually means recovering the schema some other way, through gRPC-Web JavaScript bundles shipped to client-side single-page apps, which often embed proto definitions directly, or through client SDKs and generated stubs bundled inside mobile apps. Both are common enough to check before resorting to guesswork.
Intercepting and decoding live gRPC traffic without owning the proto file
Enumeration tells a tester what exists. Turning that into active testing means intercepting and modifying live traffic, and two proxy-layer tools make that possible without ever holding the proto definition, which removes the single biggest piece of friction between discovery and exploitation.
NCC Group's blackboxprotobuf handles schema-free protobuf decoding: it can take a serialized message it's never seen a definition for and still break it into inspectable, editable fields. That turns an opaque binary blob into something a tester can actually read and manipulate field by field, even on a target where no proto file has been recovered.
Handling the connection itself depends on how the target is set up. For TLS-terminating gRPC on port 443, ALPN negotiation has to be handled correctly before anything useful can happen, since both tools work at the HTTP/2 frame level sitting above the TLS layer. For h2c, plaintext HTTP/2 running on ports like 50051 or 8080, standard proxy chaining works with no TLS handling needed at all, so you can intercept internal and development targets far more easily than production endpoints sitting behind TLS.
When a proto file has been recovered, through any of the schema recovery paths covered in the previous section, grpcurl can use it directly to build fully-formed requests: grpcurl -plaintext -import-path./proto -proto service.proto -d '{"id":"123"}' target.local:50051 package.ServiceName/GetItem. So you no longer guess at a payload structure; you send one that's guaranteed to parse correctly on the server side.
gRPC authentication tokens, tenant identifiers, and routing context don't travel in the request body, so a tester who only inspects the body will never see them. They travel as HTTP/2 metadata headers. In grpcurl, those are passed with -H flags, and in Burp, bRPC makes the same headers visible and editable mid-session. If a testing strategy only looks at message bodies and ignores metadata headers, it will miss the exact place where access control decisions get made.
Missing and inconsistent per-method authentication
gRPC enforces no authentication at the protocol level. Every method's access control is left entirely to the application built on top of it, and the result, in production, is services that expose sensitive methods with no credential check at all, or that apply role checks inconsistently from one RPC to the next inside the same service. This is the direct, practical consequence of the method-level authentication design described at the start: nothing about the protocol forces consistency, so consistency has to be built and maintained by hand, and it routinely isn't.
This kind of gap is caused less by carelessness than by the accumulation of method variants over time, as services age and grow. Enumerate every method first, using reflection where it's available or grpc-scan where it isn't, then call each one three times: once with no token at all, once with an invalid token, and once with a valid but low-privilege token. The response differential across those three calls shows exactly which methods are actually guarded. In practice that looks like grpcurl -insecure -d '{}' target.com:443 package.UserService/ListUsers with no token, then the same call with -H 'authorization: Bearer LOW_PRIV_TOKEN', then again with -H 'authorization: Bearer INVALID_TOKEN'. A method that returns the same successful response regardless of which of the three gets sent has no real access control.
A pattern that recurs across real engagements: a DeleteUser() method checks that a token is present, but never checks what role that token belongs to, so a basic low-privilege user token is sufficient to delete records that should require admin authority. That's not a sophisticated exploit. You find it by sending the same request three different ways and comparing what comes back.
The structural cause behind this kind of gap isn't carelessness so much as time. Real services accumulate method variants as they age: GetUser gets proper authentication added in a version 2 update, GetUserDetails gets built later by a different team with no auth check at all, FetchUserByID gets marked deprecated but stays live and reachable, and GetUserWithPreferences calls GetUser internally but skips the auth interceptor entirely on its own path in. The attack surface grows with development velocity, across teams and over product generations, not out of malice.
Package namespace archaeology makes that history visible from the outside. Service discovery often reveals names like com.startup.api.Users, the original implementation, alongside platform.users.v1.UserAPI, built after a merger, and internal.batch.UserBulkService, marked internal in name only while sitting exposed on the same port as the public-facing APIs. Each of those generations carries whatever security assumptions were standard when it was written, and nothing about gRPC guarantees that the older ones were ever hardened to match the newer ones. Mapping that namespace history is often the fastest way to find exactly where the authentication gaps are likely to be.


