
Exploit pour la faille use-after-free d'Exim (CVE-2020-28018) permettant une exécution de code à distance via des primitives de corruption mémoire, incluant lecture/écriture arbitraire et fuite de tas, avec une chaîne optionnelle d'élévation de privilèges locaux.
Il existe une vulnérabilité de use-after-free (UAF) dans tls-openssl.c qui permet à des attaquants distants non authentifiés de corrompre des données mémoire internes, aboutissant ainsi à une exécution de code à distance.
Primitives :
En enchaînant toutes ces primitives, il est possible de contourner entièrement toutes les mitigations d'exploitation disponibles et d'aboutir finalement à une exécution de code à distance en tant qu'utilisateur exim.
Cette vulnérabilité a été publiée dans une vaste liste de vulnérabilités ; le rapport officiel de Qualys chaîne le Use-After-Free avec CVE-2020-28008 pour effectuer une élévation de privilèges locale (LPE) une fois la RCE obtenue.
Exim doit être configuré / compilé de la manière suivante :
X_PIPE_CONNECT est désactivéVous pouvez utiliser le script checker.py pour vérifier si un serveur distant exécute une version vulnérable et possède certains prérequis nécessaires pour être exploitable.
[!] checker.py ne déclenche PAS la vulnérabilité, il vérifie simplement la version vulnérable, et teste si PIPELINING et TLS sont activés. Cela signifie que ce vérificateur ne vérifie pas le correctif, et peut donc générer des faux positifs.
Comme nous le savons déjà, la vulnérabilité se situe dans 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() est la fonction principale qui lit le message provenant du client.
Dans certaines situations, smtp_reset() est appelée, ce qui nettoie tous les tampons et valeurs.
Cela peut se produire dans des situations comme :
HELO/EHLOSTARTTLSRSETsmtp_setup_msg()À la fin de smtp_reset(), un appel à store_reset() est effectué.
store_reset est une macro qui encapsule la fonction store_reset_3().
Les fonctions store sont simplement des fonctions qui gèrent la mémoire dynamique.
Exim utilise un allocateur par pool sur des blocs obtenus via malloc.
Il existe également une fonctionnalité intéressante : une implémentation de chaîne extensible (growable string).
Structure 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;
Lorsqu'il a besoin de plus d'espace pour concaténer une nouvelle chaîne, il appelle gstring_grow().
Cette fonction essaie d'abord d'appeler store_extend_3(), qui tente d'étendre la mémoire dans le même bloc du pool.
Cela peut être utile lorsque la longueur de l'entrée n'est pas connue, mais si de la mémoire supplémentaire a été allouée après, nous ne pourrons pas l'étendre.
Ensuite, gstring_grow() appelle store_newblock_3(), qui retourne simplement une nouvelle zone mémoire et copie les octets déjà présents dans l'ancienne vers la nouvelle.
Ensuite, le pointeur g->s est restauré à partir de gstring_catn().
Dans la fonction tls_write(), on peut voir un BOOL appelé more.
Il indique s'il y a d'autres données à copier dans le tampon de chaîne avant de renvoyer les données à l'utilisateur.
Si c'est le cas, le pointeur n'est pas mis à NULL.
Sinon, les données contenues dans le tampon de chaîne sont renvoyées à l'utilisateur.
Cette fonctionnalité ouvre des pistes intéressantes pour déclencher un Use-After-Free.
Premièrement, le pointeur vers la structure gstring est stocké dans une variable statique ; cela signifie que lors de futurs appels à tls_write(), nous pourrons l'utiliser.
Comment pouvons-nous libérer le tampon puis être en mesure de l'utiliser ?
Nous devons faire en sorte que smtp_setup_msg() appelle smtp_reset() alors que l'un de nos tampons se trouve encore dans server_corked (non mis à NULL).
Après le reset, si nous appelons tls_write() d'une manière ou d'une autre, le pointeur sera toujours là, ce qui nous permet de l'utiliser après que la mémoire a été libérée.
smtp_reset() libère toute la mémoire de POOL_MAIN, dans laquelle notre tampon est contenu.
Pour contrôler le Use-After-Free, nous devons d'abord initialiser une nouvelle connexion.
Comme nous voulons exploiter tls_write(), nous devons d'abord démarrer une nouvelle session TLS.
Nous envoyons donc d'abord une commande EHLO, suivie d'un STARTTLS pour démarrer la connexion TLS.
Ensuite, pour que more soit à 1, nous pipelinons une commande, et la dernière sera la moitié d'un NOOP.
Nous fermons la connexion TLS et envoyons le reste de la commande NOOP.
Nous envoyons maintenant à nouveau EHLO, ce qui déclenchera l'appel de smtp_reset et libérera notre tampon.
Nous devons maintenant démarrer une autre connexion TLS pour pouvoir utiliser à nouveau tls_write().
Nous envoyons STARTTLS.
Désormais, l'envoi de n'importe quelle commande au serveur aboutira à l'appel de tls_write() pour renvoyer une réponse.
Mais... server_corked contient toujours un pointeur vers une zone de la mémoire libérée.