
Эксплойт для Exim Use-After-Free (CVE-2020-28018), обеспечивающий удаленное выполнение кода с помощью примитивов повреждения памяти, включая произвольное чтение/запись и утечку кучи, с опциональной цепочкой локального повышения привилегий.
Существует уязвимость 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.