Следующая новость
Предыдущая новость

Злая картинка. Разбираем уязвимость в GhostScript, чтобы эксплуатировать Pillow и ImageMagick

11.02.2019 13:03
Злая картинка. Разбираем уязвимость в GhostScript, чтобы эксплуатировать Pillow и ImageMagick

Содержание статьи

  • Стенд
  • Оригинальный GhostButt (CVE-2017-8291) и причины уязвимости PIL
  • Новые проблемы в Ghostscript и уязвимость CVE-2018-16509
  • Демонстрация уязвимости (видео)
  • Выводы

Специалисты из 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.

Давай выясним, какие слабые места были обнаружены и каким образом их можно проэксплуатировать.

INFO

  • CVE-2017-8291 — GhostButt Ghostscript.
  • CVE-2018-16509 — новая уязвимость.

Стенд

Демонстрировать уязвимость я, как обычно, буду с помощью Docker и контейнера на основе Debian.

$ docker run --rm -p80:80 -ti --name=pilrce --hostname=pilrce debian /bin/bash 

Если хочешь немного подебажить, то запускай контейнер с соответствующими ключами.

$ docker run --rm -p80:80 -ti --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --name=pilrce --hostname=pilrce debian /bin/bash 

Обновляем репозитории и устанавливаем Python, менеджер пакетов pip и вспомогательные утилиты.

$ apt update && apt install -y nano wget strace python python-pip gdb git 

Теперь установим последнюю уязвимую версию Pillow.

$ pip install "Pillow==5.3.0" 

Для удобства тестирования нам также понадобится Flask. Это популярный фреймворк для создания веб-приложений.

$ pip install flask 

Теперь с его помощью напишем небольшой скриптик, который будет принимать пользовательские картинки и менять их размер. Довольно обычное поведение для современных веб-сервисов.

app.py
01: from flask import Flask, flash, get_flashed_messages, make_response,  redirect, render_template_string, request 02: from os import path, unlink 03: from PIL import Image 04: 05: import tempfile 06: 07: app = Flask(__name__) 08: 09: @app.route('/', methods=['GET', 'POST']) 10: def upload_file(): 11:     if request.method == 'POST': 12:         file = request.files.get('image', None) 13: 14:         if not file: 15:             flash('No image found') 16:             return redirect(request.url) 17: 18:         filename = file.filename 19:         ext = path.splitext(filename)[1] 20: 21:         if (ext not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp']): 22:             flash('Invalid extension') 23:             return redirect(request.url) 24: 25:         tmp = tempfile.mktemp("test") 26:         img_path = "{}.{}".format(tmp, ext) 27: 28:         file.save(img_path) 29: 30:         img = Image.open(img_path) 31:         w, h = img.size 32:         ratio = 256.0 / max(w, h) 33: 34:         resized_img = img.resize((int(w * ratio), int(h * ratio))) 35:         resized_img.save(img_path) 36: 37:         r = make_response() 38:         r.data = open(img_path, "rb").read() 39:         r.headers['Content-Disposition'] = 'attachment; filename=resized_{}'.format(filename) 40: 41:         unlink(img_path) 42: 43:         return r 44: 45:     return render_template_string(''' 46:     <!doctype html> 47:     <title>Image Resizer</title> 48:     <h1>Upload an Image to Resize</h1> 49:     {% with messages = get_flashed_messages() %} 50:     {% if messages %} 51:         <ul class=flashes> 52:         {% for message in messages %} 53:         <li>{{ message }}</li> 54:         {% endfor %} 55:         </ul> 56:     {% endif %} 57:     {% endwith %} 58:     <form method=post enctype=multipart/form-data> 59:       <p><input type=file name=image> 60:          <input type=submit value=Upload> 61:     </form> 62:     ''') 63: 64: if __name__ == '__main__': 65:     app.run(threaded=True, port=80, host="0.0.0.0") 

Осталось запустить этот скрипт и посмотреть на результат его работы в браузере.

$ python app.py 
Злая картинка. Разбираем уязвимость в GhostScript, чтобы эксплуатировать Pillow и ImageMagick
Готовый стенд для тестирования уязвимости в PIL

Если не хочешь возиться со всеми предустановками вручную, то можешь воспользоваться готовым решением из репозитория Vulhub.

Также нам нужен собственно сам Ghostscript версии ниже 9.24. Я буду использовать две версии: 9.21 — для демонстрации уязвимости GhostButt и 9.23 — для тестирования текущего бага. Взять их можно на официальном сайте в разделе загрузок.

$ wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs923/ghostscript-9.23-linux-x86_64.tgz $ wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs921/ghostscript-9.21-linux-x86_64.tgz $ tar xvzf ghostscript-9.23-linux-x86_64.tgz && tar xvzf ghostscript-9.21-linux-x86_64.tgz 

После распаковки в соответствующих папках ты найдешь бинарники gs-921-linux-x86_64 и gs-923-linux-x86_64. Я буду перемещать их в /usr/bin/gs по мере необходимости.

Еще я поставил вспомогательную утилиту для отладчика GDB — pwndbg.

$ git clone https://github.com/pwndbg/pwndbg $ cd pwndbg $ ./setup.sh 

И скачал исходники Ghostscript, чтобы скомпилировать дебаг-версии утилиты.

$ cd ~ $ wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs921/ghostscript-9.21.tar.gz $ wget https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs923/ghostscript-9.23.tar.gz $ tar xvf ghostscript-9.21.tar.gz $ tar xvf ghostscript-9.23.tar.gz $ cd ~/ghostscript-9.21 && ./configure && make debug $ cd ~/ghostscript-9.23 && ./configure && make debug 

Готовые дебаг-бинарники будут лежать в папке debugbin. Вот теперь стенд готов.

Злая картинка. Разбираем уязвимость в GhostScript, чтобы эксплуатировать Pillow и ImageMagick
Бинарник Ghostscript, скомпилированный с отладочной информацией

Оригинальный GhostButt (CVE-2017-8291) и причины уязвимости PIL

Прежде чем переходить к рассмотрению недавних уязвимостей, вернемся на год назад и посмотрим на их прародителя. Проблемные версии — 9.21 и ниже, поэтому берем 9.21.

$ cp ~/ghostscript-9.21-linux-x86_64/gs-921-linux-x86_64 /usr/bin/gs 
Злая картинка. Разбираем уязвимость в GhostScript, чтобы эксплуатировать Pillow и ImageMagick
Используем Ghostscript версии 9.21

Первым делом стоит обратить внимание на то, что PIL автоматически определяет тип передаваемого файла. По аналогии с ImageMagick библиотека смотрит на заголовок картинки и передает управление нужному участку кода.

/src/PIL/Image.py
2618:     prefix = fp.read(16) ... 2642:     im = _open_core(fp, filename, prefix) ... 2644:     if im is None: 2645:         if init(): 2646:             im = _open_core(fp, filename, prefix) ... 2623:     def _open_core(fp, filename, prefix): 2624:         for i in ID: 2625:             try: 2626:                 factory, accept = OPEN[i] 2627:                 result = not accept or accept(prefix) 2628:                 if type(result) in [str, bytes]: 2629:                     accept_warnings.append(result) 2630:                 elif result: 2631:                     fp.seek(0) 2632:                     im = factory(fp, filename) 2633:                     _decompression_bomb_check(im.size) 2634:                     return im 2635:             except (SyntaxError, IndexError, TypeError, struct.error): 2636:                 # Leave disabled by default, spams the logs with image 2637:                 # opening failures that are entirely expected. 2638:                 # logger.debug("", exc_info=True) 2639:                 continue 2640:         return None 

При обработке файла отрабатывает функция _open_core. Она вызывает метод _accept из каждого класса, который отвечает за формат файла. В качестве аргументов передаются первые 16 байт обрабатываемого файла.

Продолжение доступно только подписчикам

Материалы из последних выпусков можно покупать отдельно только через два месяца после публикации. Чтобы продолжить чтение, необходимо купить подписку.

Подпишись на «Хакер» по выгодной цене!

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

1 год

7190 р.

Экономия 1400 рублей!

1 месяц

720 р.

25-30 статей в месяц

Уже подписан?

Источник

Последние новости