
Exploit for Exim Use-After-Free (CVE-2020-28018) achieving remote code execution via memory corruption primitives, including arbitrary read/write and heap leak, with optional local privilege escalation chain.
There exists a Use-after-free (UAF) vulnerability in tls-openssl.c that allow remote unauthenticated attackers to corrupt internal memory data, thus finally achieving remote code execution.
Primitives:
With the use of all those primitives chained together it is possible to fully bypass all the available exploit mitigations finally ending up on a remote code execution as the exim user.
This vulnerability has been released among a huge list of vulnerabilities, the official Qualys report chains the Use-After-Free with CVE-2020-28008 to perform a Local Privilege Escalation (LPE) once RCE has been achieved.
The exim, should be configured / compiled in the following way:
X_PIPE_CONNECT is disabledYou can use the checker.py script to check if a remote server is on a vulnerable version and has some needed requisites for it to be exploitable.
[!] checker.py does NOT trigger the vulnerability, just checks for vulnerable version, check if PIPELINING and TLS are enabled. This means this checker does not check for patch, which means that it can generate false positives.
As we already know, the vulnerability is located at 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() is the main function that performs the message reading from client.
On specific situations, smtp_reset() is called, which performs a clean up of all the buffers
and values.
This can happen in situations like:
HELO/EHLO is receivedSTARTTLS is receivedRSET is receivedsmtp_setup_msg()At the end of smtp_reset(), a call to store_reset() is performed.
store_reset is a macro wrapping for store_reset_3() function.
The store functions are just functions that manage the dynamic memory.
Exim uses a pool allocator on blocks that are received from malloc.
There is also an interesting functionality which is a growable string implementation.
gstring struct:
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;
When needing more space for concatenating a new string, it calls gstring_grow().
That function first tries to call store_extend_3(), that function tries to extend the memory
within the same pool block.
It can be useful when the length of the input is not known, but if more memory was allocated after it we won't be able to extend it.
Then gstring_grow() calls store_newblock_3() which just returns a new memory and copies the
bytes already present in the past one to the new one.
Then the g->s pointer is restored from gstring_catn().
In the function tls_write(), we can see there is a BOOL called more.
It indicates if there is more stuff to be copied into the string buffer before returning the data back to the user.
If so, the pointer is not NULL'ed.
If not, then the data contained in the string buffer is returned to the user.
This functionality opens some interesting ways trigger a Use-After-Free.
First, the pointer to the gstring struct is stored at a static variable,
this means on future calls to tls_write() we will be able to use it.
How can we free the buffer and then be able to use it?
We need to make smtp_setup_msg() call smtp_reset() after one
of our buffers is still on server_corked (not NULL'ed).
After the reset, if we call tls_write() somehow, the pointer will
still be there, thus allowing us to use it after the memory has been freed.
smtp_reset() frees all the memory of POOL_MAIN, in which our buffer is contained.
To control the Use-After-Free we need first to initialize a new connection.
As we want to exploit the tls_write() we need first to start a new TLS session.
So first we send a EHLO command, followed by a STARTTLS to start the TLS connection.
Then to make more be 1 we pipeline a command, and the final one will be the half of a NOOP.
We close the TLS connection and send the rest of the NOOP command.
We now send EHLO again, which will make smtp_reset be called and free our buffer.
Now we need to start another TLS connection to be able to use tls_write() again.
We send STARTTLS.
Now sending any command to the server will end up calling tls_write() for returning a response.
But... server_corked still contains a pointer to somewhere on the freed memory.
And that data might be used by another functions as it is freed...so our gstring struct will be corrupted
with random binary data.
This is the result of triggering the UAF:
gef➤ p *corked
$1 = {
size = 0x54595c9c,
ptr = 0xa7e800ba,
s = 0x7e35043433160bd3 <error: Cannot access memory at address 0x7e35043433160bd3>
}
gef➤ p corked
$2 = (gstring *) 0x555ad3be1b58
gef➤
This struct is in this way just when entering tls_write() for our command following the STARTTLS.
Obviously, once the corked->s is tried to be accessed results on a SIGSEGV interruption.
As mentioned by Qualys, they use three steps to exploit the vulnerability: