
Exploit für Exim Use-After-Free (CVE-2020-28018), der über Memory-Corruption-Primitive Remote-Codeausführung erreicht, einschließlich beliebigem Lesen/Schreiben und Heap-Leak, mit optionaler Kette zur lokalen Privilegienausweitung.
Es existiert eine Use-After-Free (UAF)-Schwachstelle in tls-openssl.c, die es entfernten, nicht authentifizierten Angreifern ermöglicht, interne Speicherdaten zu korrumpieren und schließlich eine entfernte Codeausführung (Remote Code Execution, RCE) zu erreichen.
Primitive:
Durch die Verkettung all dieser Primitive ist es möglich, alle verfügbaren Exploit-Mitigations vollständig zu umgehen und schließlich eine entfernte Codeausführung als der Exim-Benutzer zu erreichen.
Diese Schwachstelle wurde zusammen mit einer großen Liste weiterer Schwachstellen veröffentlicht. Der offizielle Qualys-Bericht verkettet den Use-After-Free mit CVE-2020-28008, um eine lokale Privilegienerweiterung (Local Privilege Escalation, LPE) durchzuführen, nachdem RCE erreicht wurde.
Exim muss wie folgt konfiguriert/kompiliert sein:
X_PIPE_CONNECT ist deaktiviertSie können das Skript checker.py verwenden, um zu prüfen, ob ein entfernter Server eine verwundbare Version hat und einige notwendige Voraussetzungen für die Ausnutzbarkeit erfüllt.
[!] checker.py löst die Schwachstelle NICHT aus, sondern prüft lediglich auf eine verwundbare Version und ob PIPELINING und TLS aktiviert sind. Das bedeutet, dass dieses Prüfprogramm nicht auf den Patch prüft, was zu Fehlalarmen führen kann.
Wie bereits bekannt, befindet sich die Schwachstelle 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() ist die Hauptfunktion, die das Lesen von Nachrichten vom Client durchführt.
In bestimmten Situationen wird smtp_reset() aufgerufen, das eine Bereinigung aller Puffer und Werte durchführt.
Dies kann in Situationen wie den folgenden passieren:
HELO/EHLO wird empfangenSTARTTLS wird empfangenRSET wird empfangensmtp_setup_msg()Am Ende von smtp_reset() wird ein Aufruf von store_reset() durchgeführt.
store_reset ist ein Makro, das die Funktion store_reset_3() umschließt.
Die Store-Funktionen sind lediglich Funktionen, die den dynamischen Speicher verwalten.
Exim verwendet einen Pool-Allokator für Blöcke, die von malloc empfangen werden.
Es gibt auch eine interessante Funktionalität: eine implementierung von wachsenden Strings (growable string).
gstring-Struktur:
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;
Wenn mehr Platz zum Verketten eines neuen Strings benötigt wird, wird gstring_grow() aufgerufen.
Diese Funktion versucht zunächst, store_extend_3() aufzurufen, das versucht, den Speicher innerhalb des gleichen Pool-Blocks zu erweitern.
Dies kann nützlich sein, wenn die Länge der Eingabe unbekannt ist, aber wenn nach dem zu erweiternden Speicher mehr Speicher allokiert wurde, kann eine Erweiterung nicht durchgeführt werden.
Dann ruft gstring_grow() store_newblock_3() auf, das einfach neuen Speicher zurückgibt und die bereits vorhandenen Bytes vom alten in den neuen Speicher kopiert.
Dann wird der Zeiger g->s von gstring_catn() wiederhergestellt.
In der Funktion tls_write() sehen wir einen BOOL namens more.
Er gibt an, ob noch mehr Daten in den String-Puffer kopiert werden müssen, bevor die Daten an den Benutzer zurückgegeben werden.
Wenn ja, wird der Zeiger nicht auf NULL gesetzt.
Wenn nicht, werden die im String-Puffer enthaltenen Daten an den Benutzer zurückgegeben.
Diese Funktionalität eröffnet einige interessante Wege, einen Use-After-Free auszulösen.
Zunächst wird der Zeiger auf die gstring-Struktur in einer statischen Variable gespeichert. Das bedeutet, dass wir ihn bei zukünftigen Aufrufen von tls_write() verwenden können.
Wie können wir den Puffer freigeben und ihn dann trotzdem nutzen?
Wir müssen erreichen, dass smtp_setup_msg() smtp_reset() aufruft, nachdem sich einer unserer Puffer noch in server_corked befindet (nicht auf NULL gesetzt).
Nach dem Zurücksetzen wird der Zeiger, wenn wir tls_write() irgendwie aufrufen, immer noch vorhanden sein, sodass wir ihn nutzen können, nachdem der Speicher freigegeben wurde.
smtp_reset() gibt den gesamten Speicher von POOL_MAIN frei, in dem sich unser Puffer befindet.
Um den Use-After-Free zu kontrollieren, müssen wir zunächst eine neue Verbindung initialisieren.
Da wir tls_write() ausnutzen wollen, müssen wir zuerst eine neue TLS-Sitzung starten.
Also senden wir zuerst einen EHLO-Befehl, gefolgt von einem STARTTLS, um die TLS-Verbindung zu starten.
Um dann more auf 1 zu setzen, pipelinieren wir einen Befehl, und der letzte wird die Hälfte eines NOOP sein.
Wir schließen die TLS-Verbindung und senden den Rest des NOOP-Befehls.
Wir senden erneut EHLO, was dazu führt, dass smtp_reset aufgerufen wird und unseren Puffer freigibt.
Jetzt müssen wir eine weitere TLS-Verbindung starten, um tls_write() wieder nutzen zu können.
Wir senden STARTTLS.