Вам труба! Как независимый исследователь взломал YouTube
Если ты когда‑нибудь заливал на YouTube приватное видео и подумывал, что оно может все равно стать достоянием общественности, то находка независимого багхантера Давида Шюца (David Shütz) усилит твои опасения. Шюц нашел несколько способов получить доступ к приватным видео на YouTube и извлечь инфу из аккаунта. Мы расскажем о том, какие баги сервиса к этому привели.
КАК УКРАСТЬ ПРИВАТНОЕ ВИДЕО
В декабре 2019 года в ходе работы по программе багбаунти Google Vulnerability Reward Program (VRP) Давид Шюц заинтересовался возможностью просмотра приватных видео на YouTube. В настройках каждого загруженного на видеохостинг ролика пользователь должен указать параметры доступа к нему. Всего предусмотрено три варианта:
- «Открытый доступ» (видео доступно всем без исключения и свободно индексируется поисковыми системами);
- «Доступ по ссылке» (для просмотра ролика нужно перейти на сайт YouTube по специальной ссылке);
- «Ограниченный доступ» (контент видят только отдельные зарегистрированные пользователи, которых отметил владелец аккаунта).
Самая интересная, с точки зрения хакера, безусловно, третья категория видеороликов. В попытке просмотреть загруженное им на собственный аккаунт видео со статусом «Ограниченный доступ» из‑под другой учетки Давид щелкал по всем кнопкам и ссылкам на сайте видеохостинга и пытался всячески модифицировать URL ролика. Но попытки оказывались тщетными: YouTube с завидным упорством возвращал сообщение об ошибке доступа.
Тогда исследователь решил пойти обходным путем. Он руководствовался простой идеей, которую часто берут на вооружение пентестеры: если основной сервис в достаточной степени защищен от компрометации, могут отыскаться связанные с ним сторонние сервисы, использующие его API, или ресурсы, у которых дела с безопасностью обстоят не так хорошо. В качестве цели он выбрал сервис контекстной рекламы Google Ads.
Давид обратил внимание на то, что Google Ads взаимодействует со многими службами Google. Например, реклама, которая демонстрируется при просмотре видео на YouTube, настраивается с помощью Google Ads. Ради эксперимента Шюц зарегистрировал аккаунт в рекламном кабинете Google и попытался настроить рекламу с использованием ID его приватного видео. Безуспешно — система не позволила провернуть такой трюк.
После этого Шюц принялся изучать имеющиеся в его распоряжении настройки рекламного кабинета. Целью поисков было что‑нибудь, имеющее непосредственное отношение к YouTube. Среди прочего в рекламном аккаунте обнаружился раздел «Видео» (Video), в котором отображаются используемые для рекламы ролики. Щелчок мышью на превью открывает раздел «Аналитика» (Analytics), где приводятся сведения о ролике и располагается встроенный проигрыватель. Давид обратил внимание на функцию «Моменты» (Moments) — она позволяет рекламодателю отмечать определенные эпизоды на видео. С ее помощью можно выделить кадр, в котором на экране демонстрируется рекламируемый продукт или логотип компании, и изучить действия пользователей в этот момент. Лучше всего механизм действия данного инструмента демонстрирует анимация, созданная самим Давидом Шюцем.
Проанализировав логи прокси, Давид обнаружил, что всякий раз, когда он создает «момент» в ролике, сайт Google Ads отсылает POST-запрос на эндпойнт /GetThumbnails
, тело которого содержит ID целевого видеоролика и имеет следующий вид:
POST /aw_video/_/rpc/VideoMomentService/GetThumbnails HTTP/1.1
Host: ads.google.com
User-Agent: Internet-Explorer-6
Cookie: [redacted]
__ar={"X":"kCTeqs1F4ME","Y":"12240","3":"387719230"}
Параметр X
в строке __ar
— это ID целевого ролика, а Y
— временная метка отмеченного момента в миллисекундах. В ответ на этот запрос сервер возвращает закодированную в Base64 картинку — миниатюру кадра, который выбран в рекламном кабинете с помощью инструмента «Моменты». Заменив в запросе параметр X
на ID своего «приватного» видео, Давид неожиданно для себя получил ответ с закодированным кадром. Таким образом, он обнаружил ошибку IDOR (Insecure Direct Object Reference), позволяющую получить доступ к защищенным настройками приватности данным. Но с использованием уязвимости исследователю удалось раздобыть всего лишь один кадр из частного видео. Хороший результат, но явно недостаточный.
Давид решил написать скрипт на Python, который автоматически отправляет соответствующие запросы, сохраняет кадры из откликов сервера, а затем заново собирает из них полноценное видео. Несложные подсчеты показали: если ролик демонстрирует 24 кадра в секунду, то каждый кадр присутствует на экране 33 мс. Таким образом, скрипт должен последовательно сохранять кадры с интервалом в 33 мс, а потом скомпоновать из полученных изображений какое‑нибудь интересное кино.
Работу созданного им proof of concept Давид Шюц запечатлел в видеоролике, который выложил на том же YouTube. Скрипт вытаскивает кадры из предварительно сохраненного Шюцем приватного ролика и воссоздает его первый трехсекундный отрезок. Изображение получается мелким, размытым и не слишком качественным, но детали на нем рассмотреть все равно можно. Это успех!
Очевидно, что использование этого метода имеет три серьезных ограничения. Во‑первых, необходимо знать ID приватного видео, которое злоумышленник пытается стянуть. Во‑вторых, способ не позволяет сохранять аудиодорожку, только видеоряд, да и то сомнительного качества. В‑третьих, мелкие детали на таком видео рассмотреть решительно невозможно.
Давид Шюц немедленно сообщил в Google о найденной им ошибке. Через месяц «Корпорация добра» выплатила ему за находку 5000 долларов, а функция «Моменты» в рекламном кабинете была исправлена — теперь движок проверяет права доступа к видео при обработке запросов из рекламного кабинета Google. Но Давид решил не останавливаться на достигнутом и вскоре обнаружил в сервисах YouTube еще один интересный баг.
Я ЗНАЮ, ЧТО ТЫ СМОТРИШЬ!
Второй найденный Давидом Шюцем баг в гугловских сервисах работает еще забавнее. Достаточно отправить потенциальной жертве ссылку на специально созданную веб‑страницу, и все тайное внезапно становится явным. Злоумышленнику открывается доступ к истории просмотра видео на YouTube, ссылкам на приватные ролики, а также он получает в свое распоряжение список понравившихся клипов и содержимое раздела «Посмотреть позже» данного пользователя. Как работает эта уязвимость и как Давид ее нашел? Чтобы понять принцип действия бага, нужно разобраться в некоторых особенностях работы YouTube.
Невидимые плей-листы
Даже если ты заходил на YouTube лишь пару раз в жизни, чтобы посмотреть на котиков, будь уверен: в твоем аккаунте все равно имеется несколько автоматически созданных плей‑листов. Первый из них, с идентификатором HL
(History List), содержит историю просмотра. Туда добавляются ссылки на все ролики, которые ты когда‑либо просматривал на YouTube. Другой плей‑лист, озаглавленный WL
(Watch Later), — это «избранное», в которое помещаются ссылки на видео при нажатии пользователем кнопки «Посмотреть позже».
Еще один плей‑лист, содержащий ссылки на понравившиеся пользователю видео, требует отдельного описания. Давид обнаружил его в ходе экспериментов с URL собственного канала на YouTube. Идентификатор канала представляет собой строку длиной 24 символа: его можно узнать, внимательно посмотрев на адрес страницы канала. Например, если URL имеет вид https://www.youtube.com/channel/UCOvX9uEO0a3fZNCK12MAabc
, то идентификатор представляет собой строку UCOvX9uEO0a3fZNCK12MAabc
.
После ряда опытов со своими аккаунтами Давид пришел к выводу, что доступ к списку понравившегося видео открывается, если заменить первые три символа в идентификаторе канала значением LLB
или LLD
. Иными словами, если идентификатор канала имеет значение UCOvX9uEO0a3fZNCK12MAgug
, то плей‑лист понравившихся клипов будет иметь имя LLBvX9uEO0a3fZNCK12MAgug
или LLDvX9uEO0a3fZNCK12MAgug
.
Еще один плей‑лист содержит ссылки на все загруженное пользователем видео вне зависимости от настроек конфиденциальности. То есть все общедоступные ролики, видео с ограниченным доступом и приватное видео хранятся в одном и том же списке. Принцип именования этого плей‑листа такой же, как в предыдущем случае, только используемый префикс имеет вид UUB
или UUD
.
Казалось бы, достаточно набрать в адресной строке браузера URL вида https://www.youtube.com/playlist?list=xxx
, где xxx
— идентификатор плей‑листа с загруженными пользователем роликами, и мы получим ID его видео, включая приватные. Однако все не так просто. Владелец канала, которому принадлежит аккаунт, действительно увидит полный список загруженного им контента. А вот другой пользователь сможет просмотреть только ссылки на публичные видео, в то время как приватные ролики и ролики с доступом по ссылке останутся для него скрытыми.
Очевидно, что эти плей‑листы содержат большой объем информации, которая может быть интересна злоумышленникам. И на первый взгляд кажется, что добраться до нее не так‑то просто.
Встраиваемый плеер
Как известно, добавленные на видеохостинг ролики можно просматривать не только на самом YouTube, но и на сторонних сайтах, если загрузивший видео пользователь не запретил подобное действие в настройках. Специально для этого существует приложение YouTube Iframe Player. Добавить плеер на сайт очень просто: для этого достаточно лишь вставить в веб‑страницу специальный HTML-код, содержащий тег iframe
.
Несмотря на кажущуюся простоту, проигрыватель имеет собственный API, позволяющий делать с ним всякие полезные вещи: например, автоматически останавливать и возобновлять воспроизведение видео или начинать показ с определенного момента. Реализуется это с использованием JavaScript, а для вызова функций API разработчик должен подключить на своем сайте специальную JS-библиотеку.
Для взаимодействия между веб‑страницей и проигрывателем используется встроенная в браузер технология PostMessageAPI. Она позволяет обмениваться данными сайтам с айфреймами, содержимое которых загружается с другого домена. Работает это так. В плеер встроен специальный слушатель PostMessage, ожидающий входящих команд. Команды отправляет по защищенному каналу подключенная к странице JS-библиотека, когда пользователь выполняет то или иное действие, например ставит видео на паузу. Но эта связь двусторонняя: плеер также может передавать библиотеке сообщения о событиях. Давид Шюц утверждает, что YouTube Iframe Player болтает без умолку, даже когда его никто ни о чем не спрашивает, и рассказывает всем и каждому о своем внутреннем мире. Это, в свою очередь, позволяет поместить на веб‑странице слушатели, настроенные на различные события. Например, слушатель может вызываться, когда пользователь открывает веб‑страницу и запускает воспроизведение ролика.
Давид Шюц приводит такой пример подобного взаимодействия. Когда в айфрейме на веб‑странице встроенный плеер начинает воспроизведение видео, вызывается команда API player.playVideo()
. При этом сайт передает в айфрейм такое сообщение:
{
"event":"command",
"func":"playVideo",
"args":[],
"id":1,
"channel":"widget"
}
В ответ плеер возвращает сайту целую кучу информации:
{
"event":"infoDelivery",
"info":{
"playerState":-1,
"currentTime":0,
"duration":1344,
"videoData":{
"video_id":"M7lc1UVf-VE",
"author":"",
"title":"YouTube Developers Live: Embedded Web Player Customization"
},
"videoStartBytes":0,
"videoBytesTotal":1,
"videoLoadedFraction":0,
"playbackQuality":"unknown",
"availableQualityLevels":[],
"currentTimeLastUpdated_":1610191891.79,
"playbackRate":1,
"mediaReferenceTime":0,
"videoUrl":"https://www.youtube.com/watch?v=M7lc1UVf-VE",
"playlist":null,
"playlistIndex":-1
},
"id":1
,"channel":"widget"
}
{
"event":"onStateChange",
"info":-1,
"id":1,
"channel":"widget"
}
JS-библиотека от YouTube служит своего рода уровнем абстракции, позволяющим веб‑разработчику просто поместить проигрыватель на странице своего сайта и больше ни о чем не беспокоиться. Весь упомянутый выше «обмен любезностями» происходит скрыто, «под капотом», и многие пользователи о существовании подобных вещей даже не догадываются. Но при желании можно вызвать нужные функции API без всяких библиотек, напрямую, с использованием обычного JavaScript. Что и попытался сделать Давид Шюц.
Например, чтобы перехватывать все сообщения PostMessageAPI, которые плеер передает веб‑странице, и журналировать их в консоли, можно использовать такой слушатель:
window.addEventListener("message", function(event){console.log(event.data)})
Между тем API проигрывателя от YouTube позволяет делать много других любопытнейших вещей, кроме управления воспроизведением ролика. Скажем, с помощью встроенного в библиотеку метода player.loadPlaylist(playlist_id)
можно загружать во встраиваемый проигрыватель плей‑листы с определенным ID. После этого достаточно вызвать функцию playVideo()
, и в айфрейме начнет воспроизводиться первое видео из плей‑листа, по окончании которого будет автоматически загружено следующее, и так далее по списку.
В JS-библиотеке имеется еще один полезный метод, позволяющий получить данные видеороликов из определенного плей‑листа, — он называется player.getPlaylist()
. Он возвращает массив, состоящий из ID всех видео в загруженном плей‑листе, например так:
> player.getPlaylist()
Array(20) [ "KxgcVAuem8g", "U_OirTVxiFE", "rbez_1MEhdQ", "VpC9qeKUJ00",
"LnDjm9jhkoc", "BQIOEdkivao", "layKyzA1ABc", "-Y9gdQnt7zs",
"U_OX5vQ567Y", "ghOqpVet1uQ", ...]
При воспроизведении видео iframe со встроенным проигрывателем передает веб‑странице большой объем данных, среди которых особого внимания заслуживает объект videoData
. В нем содержится массив данных о проигрываемом в данный момент ролике, включая его название, имя автора и ID:
> player.getVideoData()
Object { video_id: "KxgcVAuem8g", author: "LiveOverflow2",
title: "Astable 555 timer - Clock Module", video_quality: "medium",
video_quality_features: [], list: "PLGPckJAmiZCTyI72iI2KaJxkp-vUKBlTi" }
Примечательно, что сведения об этом объекте отсутствуют в официальной документации YouTube, поскольку некоторое время назад обрабатывающая его функция была удалена из JS-библиотеки. Тем не менее проигрыватель по запросу все равно отправляет данные VideoData()
на веб‑страницу. Соответственно, они могут быть получены и обработаны с помощью обычного JavaScript. Давид Шюц нашел информацию о том, что указанная функция все еще жива (хоть и давным‑давно выпилена из библиотеки), на Stack Overflow. Чем и решил воспользоваться в своих экспериментах.
А теперь самое интересное. Если пользователь авторизован на YouTube, когда он откроет любой сайт со встроенным плеером, этот плеер также автоматически авторизуется в его аккаунте. И все просмотренные юзером на стороннем сайте ролики тут же попадают в плей‑лист HL
(History List), о котором уже рассказывалось чуть раньше. Иными словами, проигрыватель имеет полный доступ к аккаунту юзера на видеохостинге. А создатель веб‑страницы имеет фактически полный доступ к самому плееру с использованием его внутреннего API. Смекаешь, к чему все это ведет?
Срывая покровы
В заметке на своем сайте Давид Шюц отмечал, что этот хак не требует использования каких‑то специальных «хакерских приемов», тут в ход идет исключительно сообразительность и умение обращаться с JavaScript. Действительно: доступ к автоматически генерируемым плей‑листам имеет только владелец аккаунта на YouTube, а встраиваемый плеер может работать на любом сайте с привилегиями этого владельца. Просто как дважды два.
Для проверки своей гипотезы Давид создал одностраничный сайт со встроенным YouTube Iframe Player и заставил его воспроизвести плей‑лист с историей просмотра HL
(History List) другого своего аккаунта. Все получилось! Когда плей‑лист загрузился, Давид вызвал функцию player.getPlaylist()
и получил список ID всех видео в этом плей‑листе. То есть любой злоумышленник, прислав кому‑нибудь ссылку на свою веб‑страничку, может получить список роликов, которые этот кто‑то просматривал на YouTube. Неплохо, но встраиваемый плеер позволяет без труда добывать и другую полезную информацию.
Давид создал proof of concept, позволяющий с помощью player.getPlaylist()
получить список видео, добавленных в плей‑лист WL
(Watch Later) — «Посмотреть позже» и HL — список просмотренных видео. Аналогичным образом плеер на его странице подгружал плей‑лист понравившихся видео, а из его названия восстанавливал ID канала пользователя YouTube, подменив первые три символа в 24-символьном идентификаторе. Зная ID канала, скрипт получал плей‑лист с загруженными пользователем видео и парсил его с помощью недокументированной функции player.getVideoData()
, получая заголовки роликов и прочую полезную информацию о них.
Некоторая заминка возникла у Давида Шюца с личными видео пользователя, закрытыми настройками приватности. Встраиваемый плеер позволяет получить плей‑лист «Загрузки», теоретически содержащий список всех роликов, которые юзер залил на YouTube. Но на сторонних сайтах ID приватных видео остаются скрытыми. Зато в этом списке отображается кое‑что другое. Если ты помнишь, помимо общедоступных и приватных видео, на YouTube еще имеются ролики, доступные исключительно по ссылке. Юзеры используют эту функцию, если не хотят, чтобы загруженное ими видео попало в паблик и индексировалось поисковиками, но вместе с тем желают поделиться им со своими знакомыми. Так вот, ID таких роликов тоже отображается в плей‑листе «Загрузки». А значит, можно просмотреть «полуприватные» видео юзера, ведь плеер обладает теми же привилегиями, что и владелец аккаунта! А еще, зная ID, можно сгенерировать ссылки на такие ролики и поделиться ими с общественностью. Неплохо, правда?
Давид сообщил о своих находках в Google и спустя некоторое время получил за свои изыскания вознаграждение в размере 1337 долларов. Теперь при загрузке плей‑листов встраиваемый плеер обращается за контентом к эндпойнту /list_ajax?list=[playlist-id]
. Если проигрыватель пытается загрузить приватный или «специальный» плей‑лист из перечисленных в этой статье, эндпойнт возвращает ошибку. Вместе с тем баг с недокументированной функцией player.getVideoData()
был актуален еще какое‑то время, но после повторного письма в Google разработчики залатали и эту дыру.
ВЫВОДЫ
Обнаруженные и описанные Давидом Шюцем баги показывают, что источником уязвимостей может стать взаимодействие нескольких продуктов, разработкой которых занимаются разные команды одной и той же компании — как это было в случае с YouTube и Google Ads. Это тот самый случай, когда правая рука не знает, что делает левая. В результате возможны утечки, которые злоумышленник может использовать в своих целях.
Сам Давид пишет: «После того как вы некоторое время протестируете продукт и поймете, как он работает внутри, становится более эффективной (и увлекательной) попытка выполнить какие‑нибудь неожиданные действия, которых разработчики, возможно, не предусмотрели. Чем лучше вы понимаете систему, тем больше у вас появляется идей, как ее сломать».
Неплохой подход — тестировать только что появившиеся фичи и технологии, которые создатели еще не успели толком отладить. «Но опять же, даже в самых надежных и хорошо протестированных системах есть шанс, что простая замена идентификатора в запросе приведет к критической ошибке», — говорит Давид.