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

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

$ 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

Готовый стенд для тестирования уязвимости в 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, скомпилированный с отладочной информацией
 

Оригинальный 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 версии 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 байт обрабатываемого файла.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *