Skip to content

This repository provides a learning environment to understand how an Exim RCE exploit for CVE-2018-6789 works.

Notifications You must be signed in to change notification settings

martinclauss/exim-rce-cve-2018-6789

Repository files navigation

Exim RCE (CVE-2018-6789) Learning Environment

Description

This is a set of files, scripts, notes, ... to set up an environment to investigate the Exim RCE (CVE-2018-6789). It can be used to debug Exim, write exploits, trace Exim function calls, learn about Exim's custom memory management (storeblocks), find out how a real-world exploit works, ...

It should only be used for academic purposes!

Requirements

  • Vagrant
  • Docker (only if you decide to run Docker on your host and not inside the Vagrant VM)

Setup

Download Exim's source code by executing

$ git submodule update --init

VM

There is a Vagrantfile in the root directory that currently supports libvirt, virtualbox and vmware as virtualization providers.

# -*- mode: ruby -*-
# vi: set ft=ruby :

memory = 8192 # in MiB
cpus = 4

Vagrant.configure("2") do |config|
  config.vm.box = "fedora/39-cloud-base"
  config.vm.box_version = "39.20231031.1"

  config.vm.provider "libvirt" do |lv|
    lv.memory = memory
    lv.cpus = cpus
  end
  config.vm.provider "virtualbox" do |lv|
    lv.memory = memory
    lv.cpus = cpus
  end
  config.vm.provider "vmware_fusion" do |lv|
    lv.memory = memory
    lv.cpus = cpus
  end

  config.vm.provision "shell", inline: <<-SHELL
  	/vagrant/scripts/setup_vm.sh
  SHELL
end

You can change the configuration as you like but keep in mind that, for example, the setup_vm.sh script uses dnf to install packages. If you want to use Ubuntu you must replace the dnf install lines with apt-get install and adjust the package names accordingly. However, there is no guarantee that the setup will work correctly.

When you are happy with your configuration just run:

$ vagrant up

to set up the machine and after that

$ vagrant ssh

to connect to it. If you don't know how to use Vagrant have a look here: https://www.vagrantup.com/intro/getting-started/

Docker container

Vagrant maps the current directory (i.e. the repository you just cloned) as a shared directory to /vagrant. To create and run the Docker image for Exim enter the following commands inside your VM (vagrant ssh)

[vagrant@localhost ~]$ cd /vagrant
[vagrant@localhost vagrant]$ ./scripts/reset_docker.sh

The first time will take much longer because Exim will be built from source. If you modify debugging scripts or other files that will be copied into the docker container you can always use ./scripts/reset_docker.sh to rebuild the Docker image. Surely, you can also just cut out necessary lines from the script and run them as single commands.

When everything is done you should see a root console:

Successfully tagged exim:latest
787f310ef922a1e519cf8bb47f1c4fed5f510da705e7ceefd48f160c980e969c
root@787f310ef922:/opt#

The weird strings may look different on your machine but you are now in a Debian Docker container running in a Fedora VM on your host machine.

Usage of the VM and the container

First, you can create two SSH sessions with vagrant ssh in two terminal windows. One can be used to run exploits and interact with Exim via SMTP. The other one is used to start, run, debug, ... Exim within the Docker container. ASLR is disabled in the VM so you can set reliable breakpoints that do not change during debugging sessions.

Example session:

First terminal:

$ vagrant ssh
[vagrant@localhost vagrant]$

Second terminal:

$ vagrant ssh
[vagrant@localhost vagrant]$ cd /vagrant
[vagrant@localhost vagrant]$ ./scripts/reset_docker.sh
...
# now you are inside the Debian Docker container
root@99296cf63016:/opt# ./run_exim.sh
root@99296cf63016:/opt# ./attach_exim.sh

The run_exim.sh script exits and Exim runs in the background. The ./attach_exim.sh script should attach gdb to the running Exim daemon process and give you an output like this:

...
pwndbg: loaded 170 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)

Attaching to process 14
Reading symbols from /usr/exim/bin/exim-4.89_1-1-fc6d6586-XX-1...done.
...
0x00007ffff6b7f5e3 in __select_nocancel () at ../sysdeps/unix/syscall-template.S:84
84	../sysdeps/unix/syscall-template.S: No such file or directory.
Breakpoint 1 at 0x5555555c03d2: file smtp_in.c, line 1762.
Breakpoint 2 at 0x5555555c051d: file smtp_in.c, line 1884.
Breakpoint 3 at 0x55555556a2c8: file base64.c, line 154.
Breakpoint 4 at 0x5555555c6aca: file smtp_in.c, line 3690.

Exim is running and waiting for requests. The breakpoints that were set come from the debugging/breakpoints file. You can use Ctrl+C to interrupt the process and give control to gdb. You could also run one of the provided exploit scripts to test if everything is working as expected:

First terminal:

[vagrant@localhost ~]$ cd /vagrant/sploits/
[vagrant@localhost sploits]$ ./sploit_0.py
[+] Opening connection to localhost on port 25: Done

Second terminal:

Thread 2.1 "exim" hit Breakpoint 2, smtp_reset (reset_point=reset_point@entry=0x555555843078) at smtp_in.c:1884
1884	{
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────
 RAX  0x555555843078 ◂— 0x0
 RBX  0x0
 RCX  0x555555824b40 (store_last_get) —▸ 0x555555843078 ◂— 0x0
 RDX  0x555555820b30 (yield_length) ◂— 0x15800001c38
 RDI  0x555555843078 ◂— 0x0
 RSI  0x0
 R8   0x3
 R9   0x52
 R10  0x73
 R11  0x246
 R12  0x5555555ec7fa ◂— 'daemon.c'
 R13  0x555555843078 ◂— 0x0
 R14  0x0
 R15  0x0
 RBP  0x5555555ee3db ◂— and    byte ptr [rax], ah /* '  %s\n' */
 RSP  0x7ffffffbe528 —▸ 0x5555555c31d1 (smtp_setup_msg+67) ◂— mov    dword ptr [rip + 0x260b6d], 0
 RIP  0x5555555c051d (smtp_reset) ◂— push   rbp
────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────
 ► 0x5555555c051d <smtp_reset>       push   rbp
   0x5555555c051e <smtp_reset+1>     push   rbx
   0x5555555c051f <smtp_reset+2>     sub    rsp, 8
   0x5555555c0523 <smtp_reset+6>     mov    rbp, rdi
   0x5555555c0526 <smtp_reset+9>     mov    qword ptr [rip + 0x263657], 0 <0x555555823b88>
   0x5555555c0531 <smtp_reset+20>    mov    dword ptr [rip + 0x263645], 0 <0x555555823b80>
   0x5555555c053b <smtp_reset+30>    mov    dword ptr [rip + 0x26364f], 0 <0x555555823b94>
   0x5555555c0545 <smtp_reset+40>    mov    dword ptr [rip + 0x263699], 0 <0x555555823be8>
   0x5555555c054f <smtp_reset+50>    mov    dword ptr [rip + 0x263687], 0 <0x555555823be0>
   0x5555555c0559 <smtp_reset+60>    mov    dword ptr [rip + 0x263679], 0 <0x555555823bdc>
   0x5555555c0563 <smtp_reset+70>    mov    dword ptr [rip + 0x263677], 0 <0x555555823be4>
────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────
In file: /opt/exim/src/src/smtp_in.c
   1879 Returns:    nothing
   1880 */
   1881
   1882 static void
   1883 smtp_reset(void *reset_point)
 ► 1884 {
   1885 recipients_list = NULL;
   1886 rcpt_count = rcpt_defer_count = rcpt_fail_count =
   1887   raw_recipients_count = recipients_count = recipients_list_max = 0;
   1888 cancel_cutthrough_connection("smtp reset");
   1889 message_linecount = 0;
────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────
00:0000│ rsp  0x7ffffffbe528 —▸ 0x5555555c31d1 (smtp_setup_msg+67) ◂— mov    dword ptr [rip + 0x260b6d], 0
01:0008│      0x7ffffffbe530 —▸ 0x7ffffffbe600 ◂— 0x0
02:0010│      0x7ffffffbe538 —▸ 0x7ffffffbe540 —▸ 0x555555605f1a ◂— add    byte ptr [rip + 0x25203a73], ah
03:0018│      0x7ffffffbe540 —▸ 0x555555605f1a ◂— add    byte ptr [rip + 0x25203a73], ah
04:0020│      0x7ffffffbe548 —▸ 0x555555843078 ◂— 0x0
05:0028│      0x7ffffffbe550 ◂— 0x0
06:0030│      0x7ffffffbe558 —▸ 0x7ffff6b7f5e3 (__select_nocancel+10) ◂— cmp    rax, -0xfff
07:0038│      0x7ffffffbe560 ◂— 0x7ffffffbe560
──────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────
 ► f 0     5555555c051d smtp_reset
   f 1     5555555c31d1 smtp_setup_msg+67
   f 2     55555556de43 daemon_go+10909
   f 3     55555556de43 daemon_go+10909
   f 4     555555583ca5 main+21601
   f 5     7ffff6abe2e1 __libc_start_main+241
──────────────────────────────────────────────────────────────────────────────────────────────────────────
Breakpoint smtp_reset
pwndbg>

You can delete all breakpoints with d and continue with c to let the sploit_0.py script run until it exists:

Second terminal:

Breakpoint smtp_reset
pwndbg> d
pwndbg> c
Continuing.
[Inferior 2 (process 42) exited with code 01]

First terminal:

...
220 787f310ef922 ESMTP Exim 4.89_1-1-fc6d6586-XX Mon, 02 Mar 2020 14:47:24 +0000
250-787f310ef922 Hello test.example.org [172.17.0.1]
250-SIZE 52428800
250-8BITMIME
250-PIPELINING
250-AUTH PLAIN
250-CHUNKING
250-PRDR
250 HELP

501 Invalid base64 data
[*] Closed connection to localhost port 25

If the forked process you just debugged (attached to) exits (e.g. [Inferior 2 (process 42) exited with code 01]) you can quit gdb and run ./attach_exim.sh again.

GDB Scripts

Under debugging you can find some scripts that could be useful. One of the scripts is showmem.py. It allows you to inspect Exims storeblocks and the corresponding heap chunks. You can run it inside gdb with the smem command:

pwndbg> smem
...
[SHOWMEM]: 0x5555558402b0: heap chunk of size 0x000004b0 (used) / data:
[SHOWMEM]: 0x555555840760: heap chunk of size 0x00000030 (used) / data: /lib/x86_64-linux-gnu
[SHOWMEM]: 0x555555840790: heap chunk of size 0x00000050 (used) / data: ...UUU
[SHOWMEM]: 0x5555558407e0: heap chunk of size 0x000000e0 (used) / data:
[SHOWMEM]: 0x5555558408c0: heap chunk of size 0x00000370 (free) / data: .~....
[SHOWMEM]: 0x555555840c30: heap chunk of size 0x00000040 (used) / data:
[SHOWMEM]: 0x555555840c70: heap chunk of size 0x00002020 (used) / data:
[SHOWMEM]:   0x555555840c80: storeblock of size 0x00002000      / data:
[SHOWMEM]: 0x555555842c90: heap chunk of size 0x00002020 (used) / data:
[SHOWMEM]:   0x555555842ca0: storeblock of size 0x00002000      / data: root
[SHOWMEM]: 0x555555844cb0: heap chunk of size 0x00008010 (used) / data:
[SHOWMEM]: 0x55555584ccc0: heap chunk of size 0x00002010 (used) / data:
[SHOWMEM]: 0x55555584ecd0: heap chunk of size 0x00001010 (used) / data: 220 99296cf63016 ESMTP Exim 4.89_1-1-fc6d6586-XX M
[SHOWMEM]: 0x55555584fce0: heap chunk of size 0x0001d320 (free) / data:

The indented memory regions are the storeblocks the other regions are heap chunks (glibc). Currently this is an approximation since I did not cross-check the in-use chunks with glibc's free lists, so there might be some wrong indications of used/free blocks. You can always use pwndbg's bins, heap, ... commands as an additional source of information!

Note: If you edit the scripts and you are using libvirt / KVM as a provider you should use vagrant rsync-auto to copy your changes from the host (not the VM and not the Docker container) to the VM. If you use, for example, VirtualBox you don't have to take care of this manually.

Structure

.
├── debugging                # GDB related scripts
│   ├── breakpoints
│   ├── gdbinit
│   ├── showmem.py
│   └── trace.py
├── Dockerfile               # Dockerfile to build and debug Exim
├── Exim                     # Source code for the vulnerable Exim version
├── exim_code_backup         # backup of the Exim's vulnerable source code
│   └── exim-fc6d65867e82009a6e0671771728d41d3423a790.zip
├── exim_files               # Patched Exim files to build Exim correctly
│   ├── configure
│   ├── eximon.conf
│   ├── Makefile
│   └── Makefile-Linux
├── README.md
├── scripts                  # Helper scripts to debug Exim
│   ├── attach_exim.sh
│   ├── reset_docker.sh
│   ├── run_exim.sh
│   └── setup_vm.sh
├── sploits                  # Incremental exploit scripts and a script to find the 0xf1 byte
│   ├── exim_0xf1.py
│   ├── smtp.py
│   ├── sploit_0.py
│   ├── sploit_1.py
│   ├── sploit_2.py
│   ├── sploit_3.py
│   ├── sploit_4.py
│   ├── sploit_5.py
│   ├── sploit_6.py
│   ├── sploit_7.py
│   ├── sploit_8.py
│   └── sploit_9.py
│   ├── sploit_10.py
└── Vagrantfile              # Vagrantfile to create the VM that hosts the Docker container

Exploit Scripts

The exploit scripts can be found under sploits. They are built incrementally to make it easier to understand the different steps. They are almost identical to the steps provided by @straightblast426 on medium.com.

sploit_10.py is the final script that should demo the RCE in one single script. This script does not spawn a reverse shell but create a file under /tmp. You can modify this by editing the following line:

cmd = '/bin/bash -c "touch /tmp/pwned"'

Please note that the provided sploit_10.py script is not the only way to exploit the vulnerability. There are, for example, other ways to arrange the heap!

Limitations

The next pointer will be changed with a constant previously known address (that should also work for you if you use the identical setup). If you would throw this exploit against another running Exim it won't work (chances are very small). You could bypass ASLR with some brute-forcing. This works quite good since Exim forks (clones) itself to handle client requests. That means that the overall memory layout stays the same and you get a realistic chance to brute-force the next pointer. If you don't know what the next pointer is you should read the references first.

If you build exim slightly different, the next pointer location may be different. You have to find the address of the storeblock that contains acl_smtp_rcpt. Follow these steps and adjust the pointer in the respective sploit_xx.py files:

# connect to VM with: vagrant ssh
./run_exim.sh
./attach_exim.sh

# pwndbg starts, then Ctrl-C
pwndbg> smem
[SHOWMEM]: 0x55555562e6b0: heap chunk of size 0x00002020 (used) / data: .2cUUU
[SHOWMEM]:   0x55555562e6c0: storeblock of size 0x00002000      / data: /usr/exim/configure
...

The storeblock with address 0x55555562e6c0 also contains acl_smtp_rcpt:

pwndbg> p acl_smtp_rcpt
$1 = (uschar *) 0x55555562e7f0 "acl_check_rcpt"

So in the exploit scripts, replace it with the address as displayed with the smem command:

# ...
# address of the ACL strings storeblock (not the chunk)
address_of_acl_storeblock = 0x55555562E6C0
# ...

References