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!
- Vagrant
- Docker (only if you decide to run Docker on your host and not inside the Vagrant VM)
Download Exim's source code by executing
$ git submodule update --init
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/
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.
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.
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.
.
├── 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
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!
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
# ...