Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2020-28018 — 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. | Kitploit
Tools/GitHubGitHub/dorkerdevil/cve-2020-28018
Privilege EscalationMemory ForensicsVulnerability AnalysisExploitationRemote Access ToolPayload DevelopmentBinary Exploitation
GitHubdorkerdevil/cve-2020-28018

CVE-2020-28018

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.

View Repository
71125 years agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2020-28018: Exim Use-after-free (UAF) leading to RCE

Introduction

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:

  • Memory Leakage
  • Arbitrary read primitive
  • Write-What-Where primitive

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.

Pre-requisites

The exim, should be configured / compiled in the following way:

  • TLS is enabled
  • OpenSSL is used (instead of GnuTLS)
  • Exim is one of the vulnerable versions
  • X_PIPE_CONNECT is disabled

You 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.

Vulnerable code

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 received
  • STARTTLS is received
  • RSET is received
  • At the starting of smtp_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.

Triggering Use-After-Free

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.

Exploitation

As mentioned by Qualys, they use three steps to exploit the vulnerability:

Download Tool