Специалисты из Google Project Zero нашли несколько опасных уязвимостей в Ghostscript — популярной реализации PostScript. Правильно сформированный файл может позволить исполнять произвольный код в целевой системе. Уязвимости подвержена и библиотека Pillow, которую часто используют в проектах на Python, в том числе — на вебе. Как это эксплуатировать? Давай разбираться.
Python Imaging Library (PIL) и ее современный форк Pillow предназначены для работы с изображениями из Python. В общих чертах они напоминают модуль gd в PHP. Эти библиотеки используются во многих популярных фреймворках и модулях. Их вызовы можно встретить в самых разных примерах кода. В общем, Pillow нередко встречается в продакшене, если один из компонентов стека — это язык Python.
Для операций с файлами PIL и Pillow используют внешние утилиты, такие как Ghostscript. Ghostscript — это кросс-платформенный интерпретатор языка PostScript (PS). Он может обрабатывать файлы PostScript и конвертировать их в другие графические форматы, выводить содержимое и печатать на принтерах, не имеющих встроенной поддержки PostScript.
А PostScript, в свою очередь, — это не просто язык разметки, а полноценный язык программирования. В нем реализованы свои алгоритмы работы с текстом и изображениями.
Официальная документация Adobe на PostScript в данный момент насчитывает около 900 страниц текста и примеров. Так что развернуться тут есть где. Неудивительно, что настолько развесистая штуковина иногда позволяет проделывать вещи, которые не были предусмотрены разработчиками интерпретаторов.
На этот раз в интерпретаторе Ghostscript и была обнаружена пачка уязвимостей, которые снова нашел Тавис Орманди (Tavis Ormandy) из Google Project Zero. Он сообщил о своей находке осенью этого года. Найденные уязвимости — это, по сути, продолжение прошлогодней ошибки в Ghostscript, что получила название GhostButt.
Давай выясним, какие слабые места были обнаружены и каким образом их можно проэксплуатировать.
Стенд
Демонстрировать уязвимость я, как обычно, буду с помощью Docker и контейнера на основе Debian.
Если хочешь немного подебажить, то запускай контейнер с соответствующими ключами.
Обновляем репозитории и устанавливаем Python, менеджер пакетов pip и вспомогательные утилиты.
Теперь установим последнюю уязвимую версию Pillow.
Для удобства тестирования нам также понадобится Flask. Это популярный фреймворк для создания веб-приложений.
Теперь с его помощью напишем небольшой скриптик, который будет принимать пользовательские картинки и менять их размер. Довольно обычное поведение для современных веб-сервисов.
Осталось запустить этот скрипт и посмотреть на результат его работы в браузере.
Готовый стенд для тестирования уязвимости в PIL
Если не хочешь возиться со всеми предустановками вручную, то можешь воспользоваться готовым решением из репозитория Vulhub.
Также нам нужен собственно сам Ghostscript версии ниже 9.24. Я буду использовать две версии: 9.21 — для демонстрации уязвимости GhostButt и 9.23 — для тестирования текущего бага. Взять их можно на официальном сайте в разделе загрузок.
После распаковки в соответствующих папках ты найдешь бинарники gs-921-linux-x86_64 и gs-923-linux-x86_64. Я буду перемещать их в /usr/bin/gs по мере необходимости.
Еще я поставил вспомогательную утилиту для отладчика GDB — pwndbg.
И скачал исходники Ghostscript, чтобы скомпилировать дебаг-версии утилиты.
Готовые дебаг-бинарники будут лежать в папке debugbin. Вот теперь стенд готов.
Бинарник Ghostscript, скомпилированный с отладочной информацией
Оригинальный GhostButt (CVE-2017-8291) и причины уязвимости PIL
Прежде чем переходить к рассмотрению недавних уязвимостей, вернемся на год назад и посмотрим на их прародителя. Проблемные версии — 9.21 и ниже, поэтому берем 9.21.
Используем Ghostscript версии 9.21
Первым делом стоит обратить внимание на то, что PIL автоматически определяет тип передаваемого файла. По аналогии с ImageMagick библиотека смотрит на заголовок картинки и передает управление нужному участку кода.
При обработке файла отрабатывает функция _open_core. Она вызывает метод _accept из каждого класса, который отвечает за формат файла. В качестве аргументов передаются первые 16 байт обрабатываемого файла.