
Exploit para Exim Use-After-Free (CVE-2020-28018) que permite execução remota de código através de primitivas de corrupção de memória, incluindo leitura/escrita arbitrária e vazamento de heap, com cadeia opcional de escalonamento de privilégio local.
Existe uma vulnerabilidade de Use-after-free (UAF) em tls-openssl.c que permite que atacantes remotos não autenticados corrompam dados internos da memória, eventualmente alcançando execução remota de código.
Primitivas:
Com o uso de todas essas primitivas encadeadas, é possível contornar completamente todas as mitigações de exploração disponíveis, resultando em execução remota de código como o usuário exim.
Esta vulnerabilidade foi divulgada em uma grande lista de vulnerabilidades; o relatório oficial da Qualys encadeia o Use-After-Free com CVE-2020-28008 para realizar uma Escalação de Privilégio Local (LPE) uma vez que a RCE foi alcançada.
O exim deve ser configurado/compilado da seguinte forma:
X_PIPE_CONNECT está desabilitadoVocê pode usar o script checker.py para verificar se um servidor remoto está em uma versão vulnerável e possui alguns requisitos necessários para ser explorável.
[!] checker.py NÃO aciona a vulnerabilidade, apenas verifica a versão vulnerável, verifica se PIPELINING e TLS estão ativados. Isso significa que este verificador não verifica o patch, o que pode gerar falsos positivos.
Como já sabemos, a vulnerabilidade está localizada em 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() é a função principal que realiza a leitura da mensagem do cliente.
Em situações específicas, smtp_reset() é chamada, que realiza uma limpeza de todos os buffers e valores.
Isso pode acontecer em situações como:
HELO/EHLO é recebidoSTARTTLS é recebidoRSET é recebidosmtp_setup_msg()No final de smtp_reset(), uma chamada a store_reset() é realizada.
store_reset é uma macro que envolve a função store_reset_3().
As funções de store são apenas funções que gerenciam a memória dinâmica.
O Exim usa um alocador de pool em blocos recebidos do malloc.
Existe também uma funcionalidade interessante que é uma implementação de string expansível.
Struct 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;
Quando precisa de mais espaço para concatenar uma nova string, ele chama gstring_grow().
Essa função primeiro tenta chamar store_extend_3(), que tenta estender a memória dentro do mesmo bloco do pool.
Isso pode ser útil quando o comprimento da entrada não é conhecido, mas se mais memória foi alocada depois dela, não conseguiremos estendê-la.
Então gstring_grow() chama store_newblock_3() que apenas retorna uma nova memória e copia os bytes já presentes na anterior para a nova.
Em seguida, o ponteiro g->s é restaurado a partir de gstring_catn().
Na função tls_write(), podemos ver que há um BOOL chamado more.
Ele indica se há mais coisas a serem copiadas para o buffer de string antes de retornar os dados ao usuário.
Se sim, o ponteiro não é anulado.
Se não, então os dados contidos no buffer de string são retornados ao usuário.
Essa funcionalidade abre algumas maneiras interessantes de desencadear um Use-After-Free.
Primeiro, o ponteiro para a struct gstring é armazenado em uma variável estática, o que significa que em futuras chamadas para tls_write() poderemos usá-lo.
Como podemos liberar o buffer e depois ser capazes de usá-lo?
Precisamos fazer com que smtp_setup_msg() chame smtp_reset() depois que um de nossos buffers ainda estiver em server_corked (não anulado).
Após o reset, se chamarmos tls_write() de alguma forma, o ponteiro ainda estará lá, permitindo-nos usá-lo após a memória ter sido liberada.
smtp_reset() libera toda a memória de POOL_MAIN, na qual nosso buffer está contido.
Para controlar o Use-After-Free, primeiro precisamos inicializar uma nova conexão.
Como queremos explorar o tls_write(), primeiro precisamos iniciar uma nova sessão TLS.
Então, primeiro enviamos um comando EHLO, seguido de um STARTTLS para iniciar a conexão TLS.
Em seguida, para fazer more ser 1, enviamos um pipeline de comandos, e o último será a metade de um NOOP.
Fechamos a conexão TLS e enviamos o restante do comando NOOP.
Agora enviamos EHLO novamente, o que fará com que smtp_reset seja chamado e libere nosso buffer.
Agora precisamos iniciar outra conexão TLS para poder usar tls_write() novamente.
Enviamos STARTTLS.
Agora, enviar qualquer comando para o servidor resultará na chamada de tls_write() para retornar uma resposta.
Mas... server_corked ainda contém um ponteiro para algum lugar na memória liberada.
E esses dados podem ser usados por outras funções, pois estão liberados... então nossa struct gstring será corrompida com dados binários aleatórios.
Este é o resultado de acionar o 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➤