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

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

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

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

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

aLLy

Специалист по информационной безопасности в ONsec. Research, ethical hacking and Photoshop.

Оставить мнение

Check Also

Мальтийский банк приостановил работу из-за атаки

Один из крупнейших банков Мальты, Bank of Valletta, пострадал от кибератаки и был вынужден…