Dead.Letter (CVE-2026-45185) – XBOW 如何在 Exim 中发现未认证的远程代码执行漏洞

Hacker News Top 新闻

摘要

XBOW 披露了 CVE-2026-45185,这是一个存在于 Exim 邮件服务器中的严重未认证远程代码执行漏洞,由 TLS 处理中的释放后使用错误引起。本文详细阐述了技术漏洞的开发过程以及人工智能模型在漏洞发现中的作用。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/13 00:25

# XBOW - Dead.Letter (CVE-2026-45185) XBOW 如何在 Exim 中发现未授权 RCE 漏洞 来源: https://xbow.com/blog/dead-letter-cve-2026-45185-xbow-found-rce-exim XBOW 发现了 CVE-2026-45185,这是 Exim 中一个关键的未授权远程代码执行(RCE)漏洞,并利用披露窗口期测试了人类与自主漏洞开发能力究竟能达到何种深度。 ## CVE-2026-45185 亲爱的读者, 接下来呈现的,首先是一个故事。是那种古老而陈旧的故事类型。关于邂逅与错失、心碎与无声背叛的故事,关于那些曾以为会永恒的爱最终却变成完全其他样子的故事。只是这一次,它发生在一个通常不讲述这类故事的环境中。 这些页面是我们正在构建的产品早期测试阶段的副产品。该产品专注于在原生代码中寻找和检测漏洞。因此,你即将阅读的既是关于一个我们找到并报告的全局性严重漏洞的技术报告,同时也更安静地记录了我如何尝试与我们如今生活的这个世界的新形态达成和解的心路历程。 我从事专业漏洞编写工作已近十年,在安全领域从业二十年。近年来,大型语言模型(LLM)的出现改变了范式,在此之前,每当涉及使用它们来编写漏洞利用代码时,我总是让自己保持旁观者姿态。这是我第一次放下戒备,让其中一个模型进入那个直到现在只有我亲手触碰过的领域。 此外,在我的整个职业生涯中,我从未阅读过 Exim 的任何一行源代码。我隐约记得几年前 Qualys 的一份分析报告(https://www.qualys.com/2021/05/04/21nails/21nails.txt),当时让我印象深刻,但我从未坐下来深入研究其代码本身。 我希望以下内容能吸引两类读者:那些寻求技术深度的读者,以及那些寻求故事性的读者。如果它同时吸引了这两类读者,我将倍感荣幸。 ## 漏洞详情 该漏洞是一个释放后使用(Use-After-Free, UAF)错误,当 GnuTLS(许多基于 Debian 的发行版,包括 Ubuntu 的默认 TLS 库)处理 TLS 连接时触发。在 TLS 关闭期间,Exim 会释放其 TLS 传输缓冲区——但嵌套的 BDAT 接收包装器仍可能处理传入字节,并最终调用 `ungetc()`,向已释放的区域写入单个字符(`\n`)。这一字节的写入落在 Exim 的分配器元数据上,破坏了分配器的内部结构;随后漏洞利用利用这种损坏来获取进一步的利用原语。 这里的关键在于,**触发此漏洞几乎不需要服务器进行任何特殊配置。** 这一点,加上破坏本身的技术形态,使其成为迄今为止在 Exim 中发现的最高等级的漏洞之一。 乍看之下,写入原语可能显得相当微弱:它仅向已释放的内存区域写入单个换行符。但正如本帖后续内容所示,这单个字节足以将权限提升至完全远程代码执行。 以下是 Exim 工作原理及其内部漏洞所在的技术细节 walkthrough。如果你只是为了故事而来,请随意跳转到 [设定挑战](https://xbow.com/blog/dead-letter-cve-2026-45185-xbow-found-rce-exim#setting) 部分。 *注意:本帖中所有代码均来自 Debian 基于系统(包括 Ubuntu 24.04 LTS)默认安装的 **Exim 4.97**。* ### Exim 基础 当客户端在明文 SMTP 会话中发出 STARTTLS 命令时,Exim 的命令调度器运行以下处理程序: `` #File: src/smtp_in.c int smtp_setup_msg(void) { // ... switch (smtp_read_command(...)) { // ... case STARTTLS_CMD: HAD(SCH_STARTTLS); if (!fl.tls_advertised) { done = synprot_error(L_smtp_protocol_error, 503, NULL, US"STARTTLS command used when not advertised"); break; } /* 如果定义了 ACL 检查则应用 */ if ( acl_smtp_starttls && (rc = acl_check(ACL_WHERE_STARTTLS, NULL, acl_smtp_starttls, &user_msg, &log_msg)) != OK) { done = smtp_handle_acl_fail(ACL_WHERE_STARTTLS, rc, user_msg, log_msg); break; } // ... s = NULL; if ((rc = tls_server_start(&s)) == OK) //[1] { // ... DEBUG(D_tls) debug_printf("TLS active\n"); break; /* 成功的 STARTTLS */ } // ... } // ... } `` \[1\] 调用 `tls_server_start()`,进而调用 **`tls_init()`**,然后分配一个新的 GnuTLS 服务器会话 `` #File: src/tls-gnu.c static int tls_init( const host_item *host, smtp_transport_options_block * ob, const uschar * require_ciphers, exim_gnutls_state_st **caller_state, tls_support * tlsp, uschar ** errstr) { //... state = &state_server; state->tlsp = tlsp; DEBUG(D_tls) debug_printf("initialising GnuTLS server session\n"); rc = gnutls_init(&state->session, GNUTLS_SERVER); //... } `` `state->session` 是一个 `gnutls_session_t`,它是 GnuTLS 句柄,拥有该 TLS 连接的所有加密状态:协商的密码套件、记录层密钥、读写序列号、ALPN 选择等。重要的是,它还处理将 GnuTLS 桥接到 Exim 的传输回调。 由于它用于处理 Exim 执行的后续所有 TLS 操作,因此被视为一种处理 TLS 状态的对象,并作为参数传递给任何操作。例如: `` #File: src/tls-gnu.c rc = gnutls_handshake(state->session); //... inbytes = gnutls_record_recv(state->session, state->xfer_buffer, ...); //... outbytes = gnutls_record_send(state->session, buff, left); //... gnutls_bye(state->session, GNUTLS_SHUT_WR); //... gnutls_deinit(state->session); `` 回到 `tls_server_start()`,一旦会话建立,Exim 配置证书验证,注册 SNI post-client-hello 回调,回复 220 TLS go ahead,将 GnuTLS 传输层连接到 SMTP 输入和输出文件描述符,最后运行握手过程。 `` #File: src/tls-gnu.c int tls_server_start(uschar ** errstr) { int rc; exim_gnutls_state_st * state = NULL; // ... 已激活检查、tls_init() 调用、ALPN/恢复设置 ... if (verify_check_host(&tls_verify_hosts) == OK) { state->verify_requirement = VERIFY_REQUIRED; gnutls_certificate_server_set_request(state->session, GNUTLS_CERT_REQUIRE); } // ... gnutls_handshake_set_post_client_hello_function(state->session, exim_sni_handling_cb); if (!state->tlsp->on_connect) { smtp_printf("220 TLS go ahead\r\n", FALSE); fflush(smtp_out); } gnutls_transport_set_ptr2(state->session, (gnutls_transport_ptr_t)(long) fileno(smtp_in), (gnutls_transport_ptr_t)(long) fileno(smtp_out)); state->fd_in = fileno(smtp_in); state->fd_out = fileno(smtp_out); //... } `` 握手成功后,Exim 使用 `store_malloc()` 分配传输缓冲区,并用 TLS 包装函数替换 SMTP 接收函数。这些包装器的目的是抽象接收 `receive_*` 函数的调用者与其底层连接类型之间的关系。 `` #File: exim-gnutls-noasan/src/tls-gnu.c int tls_server_start(uschar ** errstr) { //... state->xfer_buffer = store_malloc(ssl_xfer_buffer_size); receive_getc = tls_getc; receive_getbuf = tls_getbuf; receive_get_cache = tls_get_cache; receive_hasc = tls_hasc; receive_ungetc = tls_ungetc; receive_feof = tls_feof; receive_ferror = tls_ferror; return OK; } `` `xfer_buffer` 是一个 4096 字节的明文区域。每当解析器调用 `tls_getc()` 且缓冲区为空时,`tls_refill()` 会解密一条记录到其中;随后字节通过低水位标记 `xfer_buffer_lwm` 逐个消耗。值得注意的细节是,该缓冲区是直接的 `store_malloc()`,它是 `internal_store_malloc()` 的包装器,而后者又是 `malloc` 的包装器。 `` File: src/store.c void store_malloc_3(size_t size, const char func, int linenumber) { if (n_nonpool_blocks++ > max_nonpool_blocks) max_nonpool_blocks = n_nonpool_blocks; return internal_store_malloc(size, func, linenumber); //[1] } `` 一旦 `tls_server_start()` 返回,整个 SMTP I/O 路径都将通过安装的 TLS 感知回调进行。Exim 不直接调用 `tls_getc()`——它调用我们之前提到的间接函数指针(`receive_getc`、`receive_getbuf` 等)。 ### BDAT 分块 BDAT(RFC 3030 CHUNKING)是 SMTP 语法的一部分,客户端在此声明“线路上的下一个 N 个八位组是正文字节;不要将它们解释为 SMTP 命令”。与将正文作为以 CRLF 终止的流并以 `.` 结尾的 DATA 不同,BDAT N \[LAST\] 预先声明大小,接收方仅逐字读取 N 个字节。 对于 Exim 的解析器来说,这带来了一个小小的实现挑战。解析器是由间接函数指针驱动的状态机——`receive_getc`、`receive_getbuf`、`receive_hasc`、`receive_ungetc`。如前所述,在明文会话中,该层是 smtp;在成功的 STARTTLS 之后,整行都被重写以指向 `tls_*`。但 BDAT 是一种*模态*操作:它不替换底层传输,而是在上面组合用于有限数量的字节,然后退出。 为了处理这一点,Exim 保留了一组相同形状的第二行,即 `lwr_receive_*`。当 BDAT 块开始时,`bdat_push_receive_functions()` 将当前顶行复制到下行,并用 BDAT 包装器覆盖顶行: \#File: src/smtp\_in\.c `` #File: src/smtp_in.c static inline void bdat_push_receive_functions(void) { /* 将当前的 receive_* 函数压入“栈”,并用 bdat_getc() 替换它们,后者将使用 lwr_receive_* 函数来完成实际工作。 */ if (!lwr_receive_getc) { lwr_receive_getc = receive_getc; lwr_receive_getbuf = receive_getbuf; lwr_receive_hasc = receive_hasc; lwr_receive_ungetc = receive_ungetc; } else { DEBUG(D_receive) debug_printf("chunking double-push receive functions\n"); } receive_getc = bdat_getc; receive_getbuf = bdat_getbuf; receive_hasc = bdat_hasc; receive_ungetc = bdat_ungetc; } `` BDAT 包装器本身不解析 SMTP 命令。它们所做的是将所有实际 I/O 委托给刚刚保存的下层: `` int bdat_getc(unsigned lim) { // ... for (;;) { if (chunking_data_left > 0) return lwr_receive_getc(chunking_data_left--); bdat_pop_receive_functions(); } } uschar * bdat_getbuf(unsigned * len) { uschar * buf; if (chunking_data_left == 0) { *len = 0; return NULL; } if (*len > chunking_data_left) *len = chunking_data_left; buf = lwr_receive_getbuf(len); /* 要么是 smtp_getbuf 要么是 tls_getbuf */ chunking_data_left -= *len; return buf; } int bdat_ungetc(int ch) { chunking_data_left++; bdat_push_receive_functions(); /* 尚未完成,调用 push 是安全的,因为它在推送任何内容之前检查状态 */ return lwr_receive_ungetc(ch); } `` 因此,当 BDAT 处于活动状态时,消息读取器拉取的每个正文字节都流经以下路径: `` bdat_getc lwr_receive_getc tls_getc gnutls_record_recv xfer_buffer `` 当块被消耗完毕时,BDAT 应该通过 `bdat_pop_receive_functions()` 自行出栈,该函数将下行复制回顶行并将下行清除为 NULL: `` #File: src/smtp_in.c static inline void dat_pop_receive_functions(void) { if (!lwr_receive_getc) { DEBUG(D_receive) debug_printf("chunking double-pop receive functions\n"); return; } receive_getc = lwr_receive_getc; receive_getbuf = lwr_receive_getbuf; receive_hasc = lwr_receive_hasc; receive_ungetc = lwr_receive_ungetc; lwr_receive_getc = NULL; lwr_receive_getbuf = NULL; lwr_receive_hasc = NULL; lwr_receive_ungetc = NULL; } `` 下行由 BDAT 拥有,因此一旦 `bdat_push_receive_functions()` 将 `tls_*`(或 `smtp_*`)放入 `lwr_receive_*`,只有 `bdat_pop_receive_functions()` 应该将其取回。Exim 中没有其他代码路径读取或写入下行。 ### TLS 接收路径内部:xfer\_buffer 和低水位标记 既然我们知道 BDAT 将调用保存的 `tls_*` 回调,下一个问题是:这些回调实际上做了什么?其中两个对接下来的故事至关重要,即 `tls_getc()` 和 `tls_ungetc()`: \#File: src/tls\-gnu\.c `` #File: src/tls-gnu.c /************************************************* * TLS 版本的 getc * *************************************************/ /* 从 TLS 输入缓冲区获取下一个字节。如果缓冲区为空,它通过 GnuTLS 读取函数重新填充缓冲区。仅由服务器端 TLS 使用。这用于喂养 DKIM 并应用于所有消息正文读取。 参数: lim 要读取/缓冲的最大数量 返回:下一个字符或 EOF */ int tls_getc(unsigned lim) { exim_gnutls_state_st * state = &state_server; if (state->xfer_buffer_lwm >= state->xfer_buffer_hwm) if (!tls_refill(lim)) return state->xfer_error ? EOF : smtp_getc(lim); } `` `` /************************************************* * TLS 版本的 ungetc * *************************************************/ /* 将字符放回输入缓冲区。仅调用一次。仅由服务器端 TLS 使用。 参数: ch 字符 返回:该字符 */ int tls_ungetc(int ch) { if (ssl_xfer_buffer_lwm <= 0) log_write_die(0, LOG_MAIN, "buffer underflow in tls_ungetc"); ssl_xfer_buffer[--ssl_xfer_buffer_lwm] = ch; return ch; } `` 这两个函数都操作相同的三个全局变量: \- `ssl_xfer_buffer` — `tls_server_start()` 使用 `store_malloc()` 分配的 4096 字节传输缓冲区。 \- `ssl_xfer_buffer_lwm` — **低水位标记**,下一个要消耗的字节的索引。 \- `ssl_xfer_buffer_hwm` — **高水位标记**,最后缓冲字节的下一个位置。 缓冲区由 `tls_refill()` 填充,该函数调用 `gnutls_record_recv()` 将下一个 TLS 记录解密到 `xfer_buffer` 中。重新填充后,`lwm` 重置为 0,`hwm` 设置为解密记录的大小。 所有这一切意味着我们可以通过发送 SMTP 命令来控制 `ssl_xfer_buffer_lwm`。 ### 触发点 释放和使用发生在从 `receive_msg()` 调用的 `read_message_bdat_smtp()` 内部。`read_message_bdat_smtp_` 负责拉取 BDAT 块的正文字节。一旦 BDAT 正文解析器开始运行,它就不会返回给调用者,直到块完全消耗完毕。这就是所有事情发生的窗口——`xfer_buffer` 在循环中间被释放,而同一循环在几次迭代后通过它进行写入。 `` /* 上述 read_message_data_smtp() 的变体,专为 RFC 3030 CHUNKING 设计。接受由 CRLF 或 CR 或 LF 分隔的输入行,并写入以 LF 分隔的 spoolfile。在我们拥有 wireformat spoolfiles 之前,我们需要用于正确重新扩展 wire 的 body_linecount 会计,因此使用上述状态机的简化版本;我们不需要执行前导点检测和取消填充。 参数: fout 写入消息的文件;如果跳过则为 NULL;必须同时打开用于写入和读取。 返回:指示停止读取原因的 END_xxx 值之一 */ static int read_message_bdat_smtp(FILE * fout) { int linelength = 0, ch; enum CH_STATE ch_state = LF_SEEN; BOOL fix_nl = FALSE; for(;;) { switch ((ch = bdat_getc(GETC_BUFFER_UNLIMITED))) //[1] { case EOF: return END_EOF; case ERR: return END_PROTOCOL; case EOD: // ... if (linelength == -1) /* \r 已见 */ { bdat_ungetc('\n'); // 使用点:这是 UAF 写入触发的地方 continue; } bdat_ungetc('\r'); // ('\r' 分支是相同原语, fix_nl = TRUE; // 但具有不同的字节值) continue; case '\0': body_zerocount++; break; } // ... } } `` 循环的每次迭代都调用 `bdat_getc`(`[1]`)。当块中还有正文字节剩余时,`bdat_getc` 将工作委托给保存的下层: `` #File: src/smtp_in.c int bdat_getc(unsigned lim) { // ... for (;;) { if (chunking_data_left > 0) return lwr_receive_getc(chunking_data_left--);//[1] } } `` `[1]` 调用 `tls_getc`,在我们的案例中 `` #File: src/tls-gnu.c int tls_getc(unsigned lim) { exim_gnutls_state_st * s

相似文章

CVE-2026-45257:通过kTLS-RX在FreeBSD中的本地权限提升

Lobsters Hottest

FreeBSD中存在一个严重的本地权限提升漏洞(CVE-2026-45257),允许无特权用户将任意数据写入任何可读文件的页面缓存,绕过文件权限和标志,最终导致完全获取root权限。该漏洞影响FreeBSD 13.0及更高版本的默认安装,通过sendfile、KTLS和内核内AES-GCM解密的不安全组合实现。

Linux内核中因单个错误字符导致的高危漏洞

Ars Technica

Linux内核中一个错误的字符引入了一个use-after-free漏洞(CVE-2026-53111),允许非特权用户在Debian和Ubuntu系统上将权限提升至root;该漏洞已修复并移植回旧版本。

AMD不愿修复的远程代码执行漏洞

Hacker News Top

一名研究人员发现AMD的AutoUpdate软件存在远程代码执行漏洞,原因在于不安全的HTTP下载链接和缺乏证书验证。AMD最初以超出范围为由不予理会,但在公众关注后同意发布CVE并修复。