漏洞简介
漏洞位于邮件传输代理软件Exim中,Exim运行在邮件服务器上,负责在发送者和接受者中间进行邮件中继。台湾的安全研究人员Meh Chang 2月2日首先发现了本漏洞,并报告给Exim。Exim安全团队2月10日发布了4.90.1版本对远程代码执行漏洞进行了修复。
漏洞影响所有低于4.90.1的Exim版本。
漏洞分析
Base64解码中的1字节溢出
这是b64decode函数中的计算错误:
base64.c: 153 b64decode
b64decode(const uschar *code, uschar **ptr)
{
int x, y;
uschar *result = store_get(3*(Ustrlen(code)/4) + 1);*ptr = result;
// perform decoding
}
Exim会分配一个3*(len/4)+1 字节的缓冲区来存储解码的base64数据。当输入的数据是无效的base64字符串并且长度是4n+3时,exim会分配3n+2字节但是实际解码时会消耗3n+2字节。这就引起了一字节堆溢出。
一般来说,该漏洞是没有危害的,因为覆写的内存一般是不使用的。但是,当字符串长度达到某个特定值时这一字节就会覆写一些重要的数据。因为这一字节是可控的,所以让漏洞的利用更加容易。
Base64解码函数是一个基础函数,所以这一漏洞可以被很容易地触发,造成远程代码执行。
漏洞利用
用来获取预认证远程代码执行的利用机制描述如下。为了利用一字节溢出漏洞,首先要欺骗内存管理机制。
漏洞利用是基于下面的系统和软件的:
Debian(stretch) and Ubuntu(zesty) SMTP daemon of Exim4 package installed with apt-get (4.89/4.88) Config enabled (uncommented in default config) CRAM-MD5 authenticator (any other authenticator using base64 also works) Basic SMTP commands (EHLO, MAIL FROM/RCPT TO) and AUTH
内存分配
首先,查看源代码并寻找内存分配字段。Exim是用自定义的函数进行内存动态分配的。
extern BOOL store_extend_3(void *, int, int, const char *, int); /* The */
extern void store_free_3(void *, const char *, int); /* value of the */
extern void *store_get_3(int, const char *, int); /* 2nd arg is */
extern void *store_get_perm_3(int, const char *, int); /* __FILE__ in */
extern void *store_malloc_3(int, const char *, int); /* every call, */
extern void store_release_3(void *, const char *, int); /* so give its */
extern void store_reset_3(void *, const char *, int); /* correct type */
函数store_free()和store_malloc() 直接调用glibc的malloc()和 free()。Glibc用0x10字节的chunk来存储每次分配的前0x10字节的元数据,然后返回分配的数据的位置。下面是chunk的结构:
元数据含有之前chunk的大小,当前block的大小和一些标记。大小的前三个bit用来存储标记。在这个例子中,0x81的size表示当前chunk是0x80字节,并且前面的chunk在使用中。
Exim中大多数释放的chunk都放在双链表中,叫做无序二进制文件。Glibc根据flag标记来维护并将释放的chunk合并到更大的chunk中来避免分片。对于每个分配请求,glibc会以FIFO的顺序检查chunk并重用chunk。
对于一些性能问题,exim用链表结构store_get(), store_release(), store_extend()和 store_reset()进行维护。
Storeblock的主要特征是每个block至少有0x2000字节,这就限制了对其的利用。Storeblock也是chunk中的数据,如果我们分析内存:
下面是分配堆数据所用的函数:
EHLO hostname
对每个EHLO(or HELO)命令,exim在sender_host_name中存储hostname的指针
1839 /* Discard any previous helo name */ 1840 1841 if (sender_helo_name != NULL) 1842 { 1843 store_free(sender_helo_name); 1844 sender_helo_name = NULL; 1845 } ... 1884 if (yield) sender_helo_name = string_copy_malloc(start); 1885 return yield;
未识别的命令
对于每个有不能打印的字符的未识别的命令,exim会分配一个缓冲区来将其变成可打印的
5725 done = synprot_error(L_smtp_syntax_error, 500, NULL, 5726 US"unrecognized command");
AUTH认证
在大多数的认证步骤中,exim使用base64编码来与客户端通信。编码和解码的字符串存储在store_get()分配的缓冲区中。
当命令正确执行时,就会调用smtp_reset()。函数会调用store_reset()来重设block chain为reset point,也就意味着上个命令之后的store_get()分配的所有storeblock都被释放了。
3771 int 3772 smtp_setup_msg(void) 3773 { 3774 int done = 0; 3775 BOOL toomany = FALSE; 3776 BOOL discarded = FALSE; 3777 BOOL last_was_rej_mail = FALSE; 3778 BOOL last_was_rcpt = FALSE; 3779 void *reset_point = store_get(0); 3780 3781 DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n"); 3782 3783 /* Reset for start of new message. We allow one RSET not to be counted as a 3784 nonmail command, for those MTAs that insist on sending it between every 3785 message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of 3786 TLS between messages (an Exim client may do this if it has messages queued up 3787 for the host). Note: we do NOT reset AUTH at this point. */ 3788 3789 smtp_reset(reset_point);
利用步骤
为了利用这一漏洞,chunk之下的base64解码的数据应该被自由地释放和控制。在经过多次尝试后,发现sender_host_name是一个更好的选择。我们安排一个在sender_host_name之上预留一个空闲的chunk作为base64数据的堆布局。
1. 将大的chunk放入无序的二进制文件中
首先,发送带有hostname的EHLO消息来进行分配和回收,将一个0x6060长的chunk放入无序的二进制文件中。
2. 切割第一个storeblock
然后发送未识别的字符串来触发store_get(),并且在空闲的chunk中分配storeblock。
3. 切割第二个storeblock并释放第一个
发送EHLO消息来获取第二个storeblock。因为EHLO完成后调用smtp_reset,第一个block就被顺序释放了。
堆布局准备好后,可以使用off-by-one来覆写原来的chunk大小。将0x2021修改为0x20f1后,就讲chunk大小扩展了。
4. 发送base64数据并触发off by one
为了触发off-by-one,我们用AUTH命令发送base64数据。溢出的字节精确地覆写了下一chunk的第一个字节,并扩展了下一chunk。
5. 伪造一个合理的chunk大小
因为chunk后,下一chunk的开始点就在原来的chunk中了。因此,需要让这看起来正常一点来绕过glibc的检查。这里我们发送了另一个base64字符串,因为需要空字节和不能打印的字符来伪造chunk的大小。
6. 释放扩展的chunk
为了控制扩展的chunk的内容,我们首先要释放chunk因为我们不能直接编辑其中的内容。然后发送新的EHLO消息来释放旧的hostname。正常的EHLO消息在成功后会调用smtp_reset,这可能会让程序崩溃。为了避免这种情况,需要发送一个无效的hostname,比如a+。
7. 覆写重叠storeblock的下一个指针
Chunk释放后,我们可以用AUTH检索并覆写重叠的storeblock部分,然后进行部分写。这样就可以在不破坏ASLR的情况下修改指针了。我们部分修改了指向含有ACL字符串的storeblock的下一个指针。ACL字符串是指向全局指针的集合的,比如:
uschar *acl_smtp_auth;
uschar *acl_smtp_data;
uschar *acl_smtp_etrn;
uschar *acl_smtp_expn;
uschar *acl_smtp_helo;
uschar *acl_smtp_mail;
uschar *acl_smtp_quit;
uschar *acl_smtp_rcpt;
这些指针是在exim进程开始时初始化的,根据配置信息进行设置。比如,在配置中acl_smtp_mail = acl_check_mail,那么指针acl_smtp_mail就指向字符串acl_check_mail。当使用MAIL FROM时,exim执行ACL检查,而ACL检查首先会扩展acl_check_mail。在扩展时,如果遇到${run{cmd}},exim就尝试执行命令。所以,我们控制了ACL字符串就获取了代码执行权限。另外, 不需要直接劫持程序控制流图,而且可以绕过PIE,NX这样的缓解措施。
8. 重设storeblock并取回ACL storeblock
这样,ACL storeblock就在链接的列表链中了。一旦触发了smtp_reset()就会释放ACL storeblock,然后通过分配多个区块将其取回。
9. 覆写ACL字符串并触发ACL检查
最后,我们覆写了整个含有ACL字符串的区块。并发送了EHLO,MAIL,RCPT这影的命令来触发ACL检查。一旦我们打开配置中的acl,就获取了远程代码执行。
本文翻译自:https://www.bleepingcomputer.com/news/security/vulnerability-affects-half-of-the-internets-email-servers/ ,https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/,如若转载,请注明原文地址: http://www.4hou.com/info/news/10617.html