
Exploit per la Use-After-Free di Exim (CVE-2020-28018) che consente l'esecuzione remota di codice tramite primitive di corruzione della memoria, incluse lettura/scrittura arbitraria e leak dell'heap, con una catena opzionale di escalation dei privilegi locali.
Esiste una vulnerabilità Use-after-free (UAF) in tls-openssl.c che consente a un attaccante remoto non autenticato di corrompere i dati interni della memoria, portando infine all'esecuzione di codice remoto.
Primitive:
Utilizzando tutte queste primitive concatenate, è possibile bypassare completamente tutte le mitigazioni disponibili per gli exploit, ottenendo infine l'esecuzione di codice remoto come utente exim.
Questa vulnerabilità è stata rilasciata tra un'enorme lista di vulnerabilità; il rapporto ufficiale di Qualys concatena la Use-After-Free con CVE-2020-28008 per effettuare un escalation di privilegi locale (LPE) una volta ottenuta la RCE.
Exim deve essere configurato/compilato nel seguente modo:
X_PIPE_CONNECT disabilitatoPuoi usare lo script checker.py per verificare se un server remoto è in una versione vulnerabile e possiede alcuni requisiti necessari per essere sfruttabile.
[!] checker.py NON attiva la vulnerabilità, controlla solo la versione vulnerabile, verifica se PIPELINING e TLS sono abilitati. Questo significa che il checker non controlla la patch, quindi può generare falsi positivi.
Come già sappiamo, la vulnerabilità si trova in 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() è la funzione principale che esegue la lettura del messaggio dal client.
In situazioni specifiche, viene chiamata smtp_reset(), che esegue una pulizia di tutti i buffer e i valori.
Ciò può accadere in situazioni come:
HELO/EHLOSTARTTLSRSETsmtp_setup_msg()Alla fine di smtp_reset(), viene eseguita una chiamata a store_reset().
store_reset è una macro che avvolge la funzione store_reset_3().
Le funzioni di store sono solo funzioni che gestiscono la memoria dinamica.
Exim utilizza un allocatore a pool su blocchi ricevuti da malloc.
C'è anche una funzionalità interessante che è un'implementazione di stringhe espandibili.
Struttura 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 serve più spazio per concatenare una nuova stringa, chiama gstring_grow().
Quella funzione prima prova a chiamare store_extend_3(), che tenta di estendere la memoria all'interno dello stesso blocco del pool.
Può essere utile quando la lunghezza dell'input non è nota, ma se dopo di esso è stata allocata altra memoria, non saremo in grado di estenderlo.
Poi gstring_grow() chiama store_newblock_3() che restituisce una nuova memoria e copia i byte già presenti in quella vecchia nella nuova.
Quindi il puntatore g->s viene ripristinato da gstring_catn().
Nella funzione tls_write(), possiamo vedere che c'è un BOOL chiamato more.
Indica se ci sono altri dati da copiare nel buffer della stringa prima di restituire i dati all'utente.
Se è così, il puntatore non viene azzerato.
Altrimenti, i dati contenuti nel buffer della stringa vengono restituiti all'utente.
Questa funzionalità apre alcuni modi interessanti per attivare una Use-After-Free.
Innanzitutto, il puntatore alla struttura gstring è memorizzato in una variabile statica, questo significa che nelle future chiamate a tls_write() saremo in grado di usarlo.
Come possiamo liberare il buffer e poi essere in grado di usarlo?
Dobbiamo fare in modo che smtp_setup_msg() chiami smtp_reset() dopo che uno dei nostri buffer è ancora in server_corked (non azzerato).
Dopo il reset, se chiamiamo tls_write() in qualche modo, il puntatore sarà ancora lì, permettendoci di usarlo dopo che la memoria è stata liberata.
smtp_reset() libera tutta la memoria di POOL_MAIN, in cui è contenuto il nostro buffer.
Per controllare la Use-After-Free dobbiamo prima inizializzare una nuova connessione.
Poiché vogliamo sfruttare tls_write(), dobbiamo prima avviare una nuova sessione TLS.
Quindi per prima cosa inviamo un comando EHLO, seguito da STARTTLS per avviare la connessione TLS.
Poi per fare in modo che more sia 1, inviamo in pipeline un comando, e l'ultimo sarà la metà di un NOOP.
Chiudiamo la connessione TLS e inviamo il resto del comando NOOP.
Ora inviamo di nuovo EHLO, che farà chiamare smtp_reset e libererà il nostro buffer.
Ora dobbiamo avviare un'altra connessione TLS per poter usare di nuovo tls_write().
Inviamo STARTTLS.
Ora l'invio di qualsiasi comando al server finirà per chiamare tls_write() per restituire una risposta.
Ma... server_corked contiene ancora un puntatore a qualche punto nella memoria liberata.
E quei dati potrebbero essere usati da altre funzioni mentre sono liberati...quindi la nostra struttura gstring verrà corrotta con dati binari casuali.
Questo è il risultato dell'attivazione della UAF: