Содержание статьи
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
Если не хочешь возиться со всеми предустановками вручную, то можешь воспользоваться готовым решением из репозитория 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
. Вот теперь стенд готов.
Оригинальный GhostButt (CVE-2017-8291) и причины уязвимости PIL
Прежде чем переходить к рассмотрению недавних уязвимостей, вернемся на год назад и посмотрим на их прародителя. Проблемные версии — 9.21 и ниже, поэтому берем 9.21.
$ cp ~/ghostscript-9.21-linux-x86_64/gs-921-linux-x86_64 /usr/bin/gs
Первым делом стоит обратить внимание на то, что 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. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»