Googleユーザーの電話番号に対するブルートフォース攻撃
(brutecat.com)- GoogleのNo-JSユーザー名復旧フォームが、電話番号と表示名の組み合わせを確認できてしまったことで、特定のGoogleユーザーの完全な電話番号を特定できる攻撃チェーンが成立していた
- 核心は
/signin/usernamerecoveryと/signin/usernamerecovery/lookupのリダイレクトの違いで、アカウントなしとアカウントありの状態を見分けられた点にある - IPベースのレート制限とCAPTCHAはあったが、JSフォームのBotGuardトークンをNo-JSフォームの
bgresponseに入れると制限なく動作し、さらにIPv6ローテーションも組み合わせられた - 攻撃者はLooker StudioでGoogleアカウントの表示名を取得し、パスワード回復フローのマスクされた電話番号と国別の番号形式を組み合わせることで候補を大幅に絞り込めた
- 16 vCPU級で時間あたり0.30ドルのサーバーで約毎秒4万回の照合が可能で、Googleは2025年6月6日にNo-JSユーザー名復旧フォームを完全に廃止し、合計5,000ドルを支払った
No-JSユーザー名復旧フォームから始まった脆弱性
- ブラウザでJavaScriptを無効にした状態でGoogleサービスの挙動を確認していたところ、ユーザー名復旧フォームが依然として動作していることが発見された
- アカウント復旧フォームは以前から、難読化されたproof-of-work JavaScriptコードが生成するBotGuard系の防御に依存しており、JavaScriptが必要だと考えられていた
- しかしNo-JSフォームは、復旧メールまたは電話番号が特定の表示名に紐付いているかを確認できるフローを提供していた
2つのリクエストでアカウントの存在を確認
- 1つ目のリクエストは
/signin/usernamerecoveryに電話番号を送信し、レスポンスのLocationからその電話番号に紐付くess値を得る方式だった- Cookieと
gxf値は初期ページのHTMLから取得する
- Cookieと
- 続いて
/signin/usernamerecovery/lookupにess、名、姓、bgresponse=js_disabledを入れて送信する - レスポンスのリダイレクトが変わることで、その電話番号と表示名を持つGoogleアカウントが存在するかどうかを判定できた
- アカウントなし:
usernamerecovery/noaccountsfound - アカウントあり:
usernamerecovery/challenge
- アカウントなし:
IP制限、IPv6、BotGuard回避
- 初期の試行では、数回のリクエスト後にIPアドレスがレート制限にかかり、CAPTCHAが表示された
- オランダの携帯電話番号の例では、パスワード回復フローは
•• ••••••03のようなヒントを提供していた- オランダの携帯番号は
06で始まるため、残り6桁、つまり10^6 = 1,000,000個の候補を総当たりする必要があった
- オランダの携帯番号は
- Vultrのようなプロバイダーは
/64のIPv6範囲を提供しており、理論上は18,446,744,073,709,551,616個のアドレスを利用できる reqwestのClientBuilder.local_addressでサブネット内のランダムなIPv6アドレスを各リクエストごとに設定するPoCが作られた- データセンターIPでJS無効フォームを使うと引き続きCAPTCHAが出たが、JS有効のアカウント復旧フォームのBotGuardトークンをNo-JSフォームの
bgresponseに入れるとリクエストが通った- No-JSフォームでは、そのBotGuardトークンに対するレート制限がないように見えた
誤ヒットの除外
- 最初の実行結果には、名が
Henryで姓が空欄、かつ電話番号の末尾2桁が03のアカウントが多数含まれていた - この場合、名が
Henryであれば姓がどの値でもusernamerecovery/challengeが返された - 可能性のあるヒットを検証するため、同じ名に対して
0fasfk1AFko1wfのような任意の姓を入れて再確認した- それでもヒットになる場合は誤検知として除外した
- 同じ完全な表示名、同じ国コード、同じ末尾2桁の番号を持つ別のGoogleユーザーが存在する可能性は低いと判断された
国コードと表示名の取得
- パスワード回復フローの電話番号マスクは、国コードの推定に利用できた
- Googleは各番号のnational formatに libphonenumber を使用していた
- 国ごとのマスクされたnational formatを収集して mask.json を作成した
- ロシア、オランダ、英国、米国はそれぞれ異なるマスクパターンを持つ
- Googleは2023年に、対象と直接やり取りがある場合にのみ名前を表示する方針へ変更し、2024年4月にはInternal People APIが未認証アカウントに対する表示名の返却を完全に停止した
- しかし Looker Studio のドキュメントを作成して被害者に所有権を移すと、被害者の操作なしでホーム画面に表示名が露出した
候補範囲の最適化とツール
- libphonenumberの番号検証を使って、国ごとのモバイル接頭辞、既知の市外局番、桁数を含む format.json を生成した
- オランダの例では、国コード
31、地域コード61、62、63、64、65、68、桁数[7]を含む
- オランダの例では、国コード
- リアルタイムlibphonenumber検証 によって、無効な番号に対するGoogle APIクエリを減らした
- BotGuardトークン生成のために、chromedp を使う Goスクリプト を作成した
http://localhost:7912/api/generate_bgtokenを呼び出すことでbgTokenを取得できる
- 攻撃フロー全体は3段階で進む
- Looker StudioでGoogleアカウントの表示名を漏えいさせる
- パスワード回復フロー でマスクされた電話番号を取得する
- 表示名とマスクされた電話番号を入力して
gpbで完全な電話番号をブルートフォースする
ブルートフォース性能と想定時間
- 時間あたり0.30ドルのサーバーとコンシューマ向け相当の16 vCPUで、約毎秒4万回の照合が可能だった
- パスワード回復フローで末尾2桁しか与えられない場合の想定所要時間:
- 米国
+1: 20分 - 英国
+44: 4分 - オランダ
+31: 15秒 - シンガポール
+65: 5秒
- 米国
- PayPalのような他サービスのパスワード再設定フローがより多くの数字ヒントを提供する場合、時間は大幅に短縮されうる
- 例:
+14•••••1779
- 例:
Googleへの報告と対応タイムライン
- 2025-04-14: ベンダーへレポートを送信
- 2025-04-15: ベンダーがレポートを受理し分類
- 2025-04-25: 「Nice catch!」との返答
- 2025-05-15: パネルが1,337ドル + swagの報奨を決定
- 悪用可能性が低いとの判断が根拠で、問題はhigh impactのabuse-related methodologyとして認定された
- 2025-05-15: 報奨理由に異議を申し立て
- Abuse VRP表 によれば、probability/exploitabilityは攻撃の前提条件と被害者が悪用を発見できるかどうかで決まる
- この攻撃には前提条件がなく、被害者が悪用を発見できない点を提示した
- 2025-05-22: パネルが追加で3,663ドルを支払い、総報奨額を5,000ドルに調整
- likelihood を medium に引き上げた
- 2025-05-22: ベンダーは、グローバルなエンドポイント廃止までの間に進行中の緩和策を展開したと確認
- 2025-05-22: 2025-06-09の公開日程を調整
- 2025-06-06: ベンダーがNo-JSユーザー名復旧フォームの完全廃止を確認
- 2025-06-09: レポート公開
まだコメントはありません。