Специалисты из 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
Теперь с его помощью напишем небольшой скриптик, который будет принимать пользовательские картинки и менять их размер. Довольно обычное поведение для современных веб-сервисов.
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
. Вот теперь стенд готов.
Прежде чем переходить к рассмотрению недавних уязвимостей, вернемся на год назад и посмотрим на их прародителя. Проблемные версии — 9.21 и ниже, поэтому берем 9.21.
$ cp ~/ghostscript-9.21-linux-x86_64/gs-921-linux-x86_64 /usr/bin/gs
Первым делом стоит обратить внимание на то, что PIL автоматически определяет тип передаваемого файла. По аналогии с ImageMagick библиотека смотрит на заголовок картинки и передает управление нужному участку кода.
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 статей в месяц |
Уже подписан?
Читайте также
Последние новости