It was late June when I received an invitation to test out a new product from YouTube: a video building tool that made it easy to put together advertisements with custom fonts and logos.
I don’t know where they got my information — I must have touched every Trusted Tester form on the web — but it presented an opportunity. New products aren’t usually as hardened against attacks. When I tried logging into the website a day after requesting access, to my surprise, it let me in. (Strange, since I hadn’t gotten any sort of notification like they said I would.)
The tool is pretty basic, and it works like this: you choose a template, customize it with colors, photos, and text, and then the server renders it for you. To see how it works better, I opened my swiss army knife of debugging proxies: Charles. Then I watched the traffic go through as I followed the steps.
Nothing particularly stood out to me at first — the file upload feature was secure, they used long encrypted strings to identify resources, and all the fields were sanitized. Bummer. But then I reached the final step and noticed something.
There’s a menu that allows you to choose from the channels associated with your Google account, and beneath that, a small disclaimer: your video will be saved to your channel as “Unlisted.” So the server was both rendering and uploading the video, and it was giving me the option to choose a channel to upload to. This set off alarm bells. I had to know how it worked.
Every channel on YouTube has its own ID that usually looks like this: UCxXX0xx_X0XxxXXxxxx00Xx
. You can usually find them in a channel URL or the source of any video page. When I went back to see how the site was getting the YouTube channels on my account, I found a response from the server that looked like this:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8{
"1": [{
"1": "UCBCW8XFPYIENZPC659Pk0kg",
"2": "Ryan Kovatch",
"3": "https://yt3.ggpht.com/a/..."
}]
}
There was my channel ID. Then I hit “Save” to see what would happen.
POST /u/0/videobuilder/_/rpc/Image2VideoUiService/UploadToYouTube HTTP/1.1
Host: director.youtube.com
Content-Length: 483
Content-Type: application/x-www-form-urlencoded__ar=%7B%221%22%3A%7B%221%22%3A%22AFbu1Vq...%22%7D%2C%222%22%3A%22UCBCW8XFPYIENZPC659Pk0kg%22%2C%223%22%3A%221%22%2C%224%22%3A%22Created+with+YouTube+Video+Builder+using+template+%5C%22Introduce+your+brand+%286s%29.%5C%22%22%2C%225%22%3A%222755bb19-6e4b-4cd4-beca-46cc29f26625%22%7D
A form request. Decoded, the value of __ar
was:
{
"1":{
"1":"AFbu1Vq..."
},
"2":"UCBCW8XFPYIENZPC659Pk0kg",
"3":"Main message",
"4":"Created with YouTube Video Builder using template \"Introduce your brand (6s).\"",
"5":"2755bb19-6e4b-4cd4-beca-46cc29f26625"
}
Yep, there was my channel ID. Alongside it were the title, description, and template ID. When I figured this out, my first move was to swap it with the channel ID of my test account. And then…
HTTP/1.1 200 OK
Boom. The server returned an ID to track the progress of the upload, and then I got a link to it on YouTube. Sure enough, it was uploaded as an Unlisted video to a channel I didn’t own. I reported it right away.
Then I got this email right after, which — okay.
And then I went to bed.
Lots of movement happened in the early hours of the morning. My report had seemed to cause quite a stir.
I woke up to a “Nice catch!” in my mailbox. It prompted me to actually think through the implications of an issue like this — on a platform like YouTube, it would catalyze the spread of misinformation. The kind of stuff that sways elections and threatens democracy. I had to dig deeper.
At this point, the limitations were that a.) I could not upload a video publicly and b.) I could only upload videos created by the site, not my own. The root issue was plenty harmful, but if I could figure out how to bypass these, it would be considerably more serious. So I revisited the UploadToYouTube
function.
{
"1":{
"1":"AFbu1Vq..."
},
"2":"UCBCW8XFPYIENZPC659Pk0kg",
"3":"Main message",
"4":"Created with YouTube Video Builder using template \"Introduce your brand (6s).\"",
"5":"2755bb19-6e4b-4cd4-beca-46cc29f26625"
}
That identifier? It’s an encrypted string containing information about the video file. (It’s actually much longer.) I would later learn that this is called a scottyResourceId
, and it’s used all over YouTube to identify data that’s uploaded to their servers. An idea popped into my head: What if I could upload a file to another YouTube site, pull the resource ID from there, and use it in this API instead? That would allow me to upload a custom video and point the server to it.
I pulled up YouTube Studio and uploaded a test file. The server responded with this:
HTTP/1.1 200 OK
X-GUploader-UploadID: ...
X-Goog-Upload-Status: final
Content-Length: 405
Server: UploadServer
Connection: keep-alive{
"status":"STATUS_SUCCESS",
"scottyResourceId":"ACKujmz..."
}
Bingo! This ID started with ACK
, but was just as long as the other one. I sent a new UploadToYouTube
request to the video builder, this time with the ID from YouTube Studio, and… got an error.
com.google.apps.framework.request.CanonicalCodeException:
com.google.security.keymaster.KeyUnavailableException:No matching decryption key found; ciphertext had key hash XXYYZZ but no key version matched it; existing versions:keyhashes are 1:YYZZXX, 2:ZZXXYY, 3:YYXXZZ, 4:XXZZYY, see go/key_unavailable_exception.Code: PERMISSION_DENIED
So it didn’t work. But exceptions like this can actually be more severe issues than the functionality they prevent. In this case, the server had tried to decrypt the resource ID, but since it was from another server, it couldn’t find the right decryption key to use. When the exception was raised, it included the hashes of all the keys it had stored. This was a critical data leak.
I reported it and got another “Nice catch!” Further research around changing the video from unlisted to public wasn’t successful, so I stopped there.
Even the biggest players in the game are prone to bugs like this. Sometimes they’re more obvious, and sometimes they’re trickier to find, but they’re there. I hope this first write-up of mine is helpful to anyone trying to find a way to their first bug bounty, or maybe already has a few under their belt. There’s no such thing as an impenetrable application.