
Exploit para Exim Use-After-Free (CVE-2020-28018) que logra ejecución remota de código mediante primitivas de corrupción de memoria, incluyendo lectura/escritura arbitraria y fuga de heap, con una cadena opcional de escalada de privilegios local.
Existe una vulnerabilidad de use-after-free (UAF) en tls-openssl.c que permite a atacantes remotos no autenticados corromper datos de memoria interna, logrando así, finalmente, ejecución remota de código.
Primitivas:
Con el uso de todas esas primitivas encadenadas, es posible evadir por completo todas las mitigaciones de explotación disponibles, terminando finalmente en una ejecución remota de código como el usuario exim.
Esta vulnerabilidad ha sido publicada dentro de una enorme lista de vulnerabilidades; el informe oficial de Qualys encadena el Use-After-Free con CVE-2020-28008 para realizar una escalada local de privilegios (LPE) una vez que se ha conseguido RCE.
Exim debe estar configurado / compilado de la siguiente manera:
X_PIPE_CONNECT está deshabilitadoPuedes usar el script checker.py para comprobar si un servidor remoto tiene una versión vulnerable y cumple algunos de los requisitos necesarios para que sea explotable.
[!] checker.py NO dispara la vulnerabilidad; solo comprueba si la versión es vulnerable y verifica si PIPELINING y TLS están habilitados. Esto significa que este comprobador no comprueba si el servidor está parcheado, por lo que puede generar falsos positivos.
Como ya sabemos, la vulnerabilidad se encuentra en 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() es la función principal que realiza la lectura del mensaje desde el cliente.
En determinadas situaciones, se llama a smtp_reset(), que realiza una limpieza de todos los búferes y valores.
Esto puede ocurrir en situaciones como:
HELO/EHLOSTARTTLSRSETsmtp_setup_msg()Al final de smtp_reset(), se realiza una llamada a store_reset().
store_reset es una macro que envuelve la función store_reset_3().
Las funciones store son simplemente funciones que gestionan la memoria dinámica.
Exim utiliza un asignador de pools sobre bloques que recibe de malloc.
También hay una funcionalidad interesante: una implementación de cadenas extensibles.
Estructura 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;
Cuando necesita más espacio para concatenar una nueva cadena, llama a gstring_grow().
Esa función primero intenta llamar a store_extend_3(); esta función intenta extender la memoria dentro del mismo bloque del pool.
Puede ser útil cuando no se conoce la longitud de la entrada, pero si se asignó más memoria después, no podremos extenderlo.
Entonces gstring_grow() llama a store_newblock_3(), que simplemente devuelve una nueva memoria y copia los bytes ya presentes en la anterior a la nueva.
Luego el puntero g->s se restaura desde gstring_catn().
En la función tls_write(), podemos ver que hay un BOOL llamado more.
Indica si hay más datos que copiar en el búfer de cadena antes de devolver los datos al usuario.
Si es así, el puntero no se pone a NULL.
Si no, entonces los datos contenidos en el búfer de cadena se devuelven al usuario.
Esta funcionalidad abre algunas vías interesantes para desencadenar un Use-After-Free.
Primero, el puntero a la estructura gstring se almacena en una variable estática; esto significa que en futuras llamadas a tls_write() podremos usarlo.
¿Cómo podemos liberar el búfer y luego poder usarlo?
Necesitamos hacer que smtp_setup_msg() llame a smtp_reset() después de que uno de nuestros búferes siga en server_corked (sin poner a NULL).
Después del reset, si de algún modo llamamos a tls_write(), el puntero seguirá allí, lo que nos permite usarlo después de que la memoria haya sido liberada.
smtp_reset() libera toda la memoria de POOL_MAIN, en la que se encuentra nuestro búfer.
Para controlar el Use-After-Free, primero necesitamos inicializar una nueva conexión.
Como queremos explotar tls_write(), primero debemos iniciar una nueva sesión TLS.
Así que primero enviamos un comando EHLO, seguido de STARTTLS para iniciar la conexión TLS.
Luego, para que more sea 1, hacemos pipelining de un comando, y el último será la mitad de un NOOP.
Cerramos la conexión TLS y enviamos el resto del comando NOOP.
Ahora enviamos EHLO de nuevo, lo que hará que se llame a smtp_reset y libere nuestro búfer.
Ahora necesitamos iniciar otra conexión TLS para poder usar tls_write() de nuevo.
Enviamos STARTTLS.
Ahora, enviar cualquier comando al servidor terminará llamando a tls_write() para devolver una respuesta.
Pero... server_corked todavía contiene un puntero a algún lugar de la memoria liberada.
Y esos datos pueden ser utilizados por otras funciones al estar liberados... así que nuestra estructura gstring se corromperá con datos binarios aleatorios.
Este es el resultado de desencadenar el UAF: