Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Специалисты из 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, скомпилированный с отладочной информацией
Прежде чем переходить к рассмотрению недавних уязвимостей, вернемся на год назад и посмотрим на их прародителя. Проблемные версии — 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