Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

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

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

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

工具目录

分类

查看所有分类
Loading categories
工具/GitHubGitHub/dorkerdevil/cve-2020-28018
权限提升内存取证漏洞分析漏洞利用远程访问工具Payload 开发二进制利用
GitHubdorkerdevil/cve-2020-28018

CVE-2020-28018

针对Exim Use-After-Free(CVE-2020-28018)漏洞的利用程序,通过内存破坏原语实现远程代码执行,包括任意读写和堆泄露,并可选地包含本地权限提升链。

查看仓库
71125年前尚未审核

最受欢迎

查看全部 →

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

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

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。

/*************************************************
*         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 结构体:

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 的结果:

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 之后使用一个大的参数就足以覆盖结构体 :)

结果:

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 长度遍历堆。

	[+] 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 失败?我使用一个错误的发件人,因为发件人需要一个域,如果没有指定域,错误消息将包含客户端发送的数据。

下载工具