StubZero: $148,337 RCE in Google Cloud Production · Brutecat
Book an engagement<br>Back to research<br>What started as a debugging endpoint info leak escalated into full remote code execution on Google Cloud's production environment. Three months later, it happened again. This vulnerability was assigned CVE-2026-2031 .
This story starts with one of my automated fuzzing tools alerting me about the API cloudcrmipfrontend-pa.googleapis.com , as it was responding with status 200 to some suspicious endpoints. On further inspection, the API seems to have several public debugging endpoints:
Screenshot from an internal API explorer tool I built for testing internal Google APIs from a discovery document
#req2proto as a Service™<br>Some of the endpoints like GET /v1/integrationPlatform:listServicesByServer seemed to always return internal server error. However, the endpoint /v1/integrationPlatform:getProtoDefinition seemed to return the proto definitions of any protobuf message in google3 (google's internal source code monorepo), even for unrelated services like YouTube.
Request
GET /v1/integrationPlatform:getProtoDefinition?fullName=youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext&isEnum=false HTTP/2<br>Host: cloudcrmipfrontend-pa.clients6.google.com<br>Cookie:<br>Authorization: SAPISIDHASH<br>Origin: https://console.cloud.google.com<br>X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE<br>For authentication with this API, we are using Google's proprietary first-party authentication. This involves your Google account cookie header along with an Authorization header value calculated using the SAPISID cookie as well as the whitelisted origin https://console.cloud.google.com
Response
"protoDescriptor": {<br>"name": "InnerTubeContext",<br>"field": [<br>"name": "client",<br>"number": 1,<br>"label": "LABEL_OPTIONAL",<br>"type": "TYPE_MESSAGE",<br>"typeName": ".youtube.api.pfiinnertube.YoutubeApiInnertube.ClientInfo",<br>"jsonName": "client"<br>},<br>"name": "user",<br>"number": 3,<br>"label": "LABEL_OPTIONAL",<br>"type": "TYPE_MESSAGE",<br>"typeName": ".youtube.api.pfiinnertube.YoutubeApiInnertube.UserInfo",<br>"jsonName": "user"<br>},<br>...This was massive, because in Google, everything is proto. All APIs are defined internally as gRPC services using protobuf, and this would essentially allow for disclosing the request/response body of any endpoint, which for a blackbox target like Google is a gold mine.
In the past, I had developed a tool req2proto for this purpose, however that tool was limited only to finding the request body proto, not the response body, and it was also with the assumption that the API supported JSPB (application/json+protobuf) which most APIs didn't. As a joke, my friends and I started referring to this endpoint from then onwards as "req2proto as a service", since it was quite literally a hosted, much more powerful version of the tool.
Before probing further with this endpoint, I checked if there were any other endpoints leaking information.
#Leaking internal workflow execution queue
Initially, without any query parameters set, this endpoint was just returning INVALID_ARGUMENT errors. Trying filters like * also didn't work. However, from past experience, these filter parameters usually allow any filtering in accordance with https://google.aip.dev/160
As such, upon trying client_id>"123" as the filter, I got an interesting response:
"error": {<br>"code": 500,<br>"message": "Failed to convert server response to JSON",<br>"status": "INTERNAL"<br>}It looks like whatever response it was trying to give to me didn't have a JSON mapping. However, Google APIs support changing the response content-type via the standard ?alt= parameter. For instance, ?alt=proto would return the output in protobuf.
The only issue is that since we are using Google's proprietary first-party auth for authentication (Cookie and Authorization header), we have to send requests to the hostname cloudcrmipfrontend-pa.clients6.google.com instead of cloudcrmipfrontend-pa.googleapis.com, but Google does not allow raw proto responses to requests sent to *.google.com:
Request unsafe for browser client domain: cloudcrmipfrontend-pa.clients6.google.comThankfully, there's a way around this. We can use the header X-Goog-Encode-Response-If-Executable: base64 and this would get the response back in base64 instead of binary data:
GET /v1/integrationPlatform:listQuotaQueue?filter=client_id%3E%22123%22&alt=proto HTTP/2<br>Host: cloudcrmipfrontend-pa.clients6.google.com<br>Cookie:<br>Authorization: SAPISIDHASH<br>Origin: https://console.cloud.google.com<br>X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE<br>X-Goog-Encode-Response-If-Executable: base64The API returned a large base64 protobuf response. Using the proto definition leak from earlier to retrieve the schema for ListQuotaQueueResponse, I was able to decode it properly which revealed that this was some sort of internal workflow execution queue, which included workflows syncing data from Spanner to Salesforce:
"queue_items": [<br>"queued_request":...