Openfind Mail2000电子邮件系统RCE漏洞分析
邮件系统作为大部分企业主要的信息传递方式,在各大组织都占有举足轻重的地位。攻击者一旦掌控了邮件服务器,不仅可以窃取邮件,还能为进一步的渗透做好准备。本篇文章将介绍在Openfind Mail2000这套软件上所发现的内存漏洞,以及相关的攻击手法。
此漏洞是在2018年被发现,已通报给Openfind并迅速被修补,相关用户也得到了更新帮助。
Openfind Mail2000
Mail2000 是由台湾地区厂商Openfind开发的简单易用的电子邮件系统,被广泛使用于政府机关、教育机构,如台北市教育局、中科院,以及台湾科技大学都是Mail2000的用户。常见登录界面如下所示:
这次所讲述的漏洞,便是从这个Web界面,攻陷整台服务器!
服务器架构
Mail2000提供的Web界面使用了CGI(Common Gateway Interface)技术来实现。而大多数Web服务器实现CGI的方式如下图:
首先由httpd接受客户端请求,然后根据对应的CGI路径,执行相对应的CGI文件。大多数的开发者会根据需求,将常见的共用功能编写成library,供CGI调用。
从底层来看,可发现虽然称为“Web服务器”,但仍有许多组件和二进制相关!例如httpd为了运行效率,多是由C/C++所编写,而其它像是库、扩充的模块、各页面的CGI也经常如此。因此,二进制相关的漏洞,便是我们这次的主体!
漏洞
这个漏洞位于Openfind中的libm2kc
库中,其中包含了各种CGI通用函数,如参数解析和文件处理等等,而此次漏洞就发生在参数解析的部分。由于参数处理是很底层且基本的功能,因此影响的范围非常的大,就连Openfind的其它产品也同样受影响!
这个漏洞的触发条件如下:
攻击者通过
multipart
形式发送HTTP的POST请求POST请求传输的文件内容超过200项
multipart
是HTTP协议中用来处理文件传输的一种格式,示例如下:
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="files"; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--AaB03x--
而在libm2kc
中,使用了数组来储存参数:
g_stCGIEnv.param_cnt = 0;
while(p=next_param())
{
g_stCGIEnv.param[param_cnt] = p;
g_stCGIEnv.param_cnt++;
}
这个数组为全局变量g_stCGIEnv
中的param
,在存入param
数组时,代码并没有检查是否已超过声明的数组大小,造成了越界写入。
需要注意的是,param
数组所储存的为指向字符串位置的指针,而非字符串本身。
struct param
{
char *key;
char *value;
int flag;
};
因此当触发越界写入时,写入内存的值也是一个个指向字符串的指针,而被指向的字符串内容则是造成溢出的参数。
漏洞利用
要利用越界写入的漏洞,就要先了解利用这个溢出可以做什么,发生溢出的全局变量结构如下:
00000000 CGIEnv struc ; (sizeof=0x6990, mappedto_95)
00000000 buf dd ?; offset
00000004 length dd ?
00000008 field_8 dd 6144 dup(?) ; offset
00006008 param_arr param 200 dup(?)
00006968 file_vecdd ?; offset
0000696C vec_len dd ?
00006970 vec_cur_len dd ?
00006974 arg_cnt dd ?
00006978 field_6978 dd ?
0000697C errcode dd ?
00006980 method dd ?
00006984 is_multipartdd ?
00006988 read_func dd ?
0000698C field_698C dd ?
00006990 CGIEnv ends
溢出的数组为其中的param_arr
,因此在其之后的变量都可能被覆盖。包括post_files
、vec_len
、vec_cur_len
、arg_cnt
等等。其中最吸引我注意的是file_vec
这个变量,这是一个用来管理POST上传文件的vector
,大部分的vector结构如下所示:
使用size
记录数组的总长度,end
记录目前空间已用到哪里,这样就可以在容量不够的时候进行扩充。因此我们若能利用漏洞覆盖vector
的指针,就有可能成功利用漏洞!借由覆盖这个vector
指针,我们可以达到伪造一个上传文件及其所有相关变量的效果,而这个上传文件的结构里包含了各种常见的文件相关变数,像是路径、文件名,和Linux中用来管理文件的FILE
结构。而FILE
结构便是此次攻击的关键!
FILE Structure Exploit
这次的攻击使用了FILE structure exploit
,这是近几年刚发现的攻击手法,由angelboy在HITCON CMT公开:
FILE结构是Linux中用来做文件处理的结构,像是STDIN
、STDOUT
、STDERR
,或者是调用fopen后回传的结构都属于FILE
。而这个结构之所以能成为漏洞利用的突破口主要在于它所包含的vtable
指针:
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
vtable
中记录了各种函数指针,对应各种文件处理相关的功能:
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
/* ... */
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
/* ... */
};
因此,如果我们可以篡改、伪造这个vtable
,就可以在程序处理文件时候,劫持处理流程!至此,我们制定出以下的攻击步骤:
1.建立连接,调用CGI
2.使用大量参数,覆盖vector指针
3.伪造上传文件中的FILE*,指向伪造的FILE结构
4.在CGI流程中调用FILE相关的操作fread,fwrite,fclose, …
5.劫持程序流程
现在我们已经知道终点是调用一个FILE操作
,那么就可以开始往回找哪个函数是CGI常用的FILE操作,又有哪一些CGI可以作为入口点,这样才能串起攻击链!我们首先对使用到POST file
的相关函数进行研究,并选定了目标MCGI_VarClear()
。
MCGI_VarClear()
在许多用到FILE的CGI中都有被调用,它用于在程序结束前将g_stCGIEnv
清空,包括将动态配置的内存释放掉,以及将所有FILE关闭,也就是调用fclose()
,这也意味着可以通过vtable
进行劫持!我们可以使用这个越界写入漏洞覆盖file_vec
,而指向的内容就是request请求的参数,这样便可以伪造为上传的文件!结构如下所示:
我们最终目标就是将FILE*
指向伪造的结构,借由伪造的vtable
劫持程序流程!因此,我们需要将FILE*
这个指针指向一个内容可控的位置,但是其实我们并不知道该指到哪里去,因为Linux上的有一个内存防御机制-ASLR 。
Address Space Layout Randomization
ASLR使得每次程序在执行并载入内存时,会随机使用不同的位置,我们可以尝试使用cat /proc/self/maps
观察每一次执行时的内存位置是否相同:
ASLR在大部分的环境中都是默认开启的,因此在编写exploit时,我们不知道该指到哪里。
而且这个机制在CGI的架构下会造成更大的阻碍,一般的服务器的攻击流程可能是这样:
貌似可以先窃取地址,再进一步攻击。但在CGI架构中却是这样:
在这个情况下,得到的地址是无法在后续攻击中使用的!一个CGI执行完就结束了,下一个请求又是全新的CGI!
为了应对这个问题,我们最后编写了两个exploit,攻击的手法根据CGI的二进制文件而有不同。
Post-Auth RCE - /cgi-bin/msg_read
第一个exploit的入口点是一个需要登入的页面,该程序较大、功能也较多。在这个exploit中,我们使用了堆喷射的手法来解决ASLR,也就是在堆中填入大量重复的数据,这样我们就有很高的机率可以猜到它的位置。
而喷射的内容就是大量伪造好的FILE结构,包含伪造的vtable。从这个二进制文件中,我们找到了一个十分实用的gadget:
xchg eax, esp; ret
这个gadget的作用在于,我们可以改变栈的位置,而刚好此时的eax
指向内容是可控的,因此整个栈的内容都可以伪造,也就是说我们可以使用ROP(Return-oriented programming)来攻击!于是我们在伪造的vtable中设置了栈移动的gadget以及后续利用的ROP攻击链,进行ROP攻击!
现在可以拿shell了嘛?不,其实还有一个大问题,由于前面提到的防御机制ASLR,我们找不到system
的位置!这个二进制文件本身提供的代码并不足以反弹一个shell,因此我们希望可以直接利用libc
当中的system
函数来达成目的,但正如前面所提到的,每次载入内存位置都是随机的,我们并不知道system
的确切位置!
经过我们再次仔细研究,发现了一件非常特别的事,这个程序理论上是打开了NX
,也就是可写区域不可执行的保护机制。
但是实际执行的时候,栈的执行权限没有被禁止!
不论原因为何,我们可以利用这个可执行区域,将shellcode放上去执行!
然而,这个攻击是需要登入的,对于追求完美的DEVCORE研究组来说,这并不足够!因此我们研究了其它的攻击路径!
Pre-Auth RCE - /cgi-bin/cgi_api
在搜索了所有CGI入口点以后,我们找到了一个不需要登入,同时又会呼叫MCGI_VarClear()
的CGI——/cgi-bin/cgi_api
。正如其名,它就是一只调用API的接口,因此程序本身非常的小,几乎是调用完库就结束了,所以也不再有和栈相关的gadget可以利用。
这时,由于我们已经得知栈是可执行的,因此其实我们是可以跳过ROP这个步骤,直接将shellcode放置在栈上。这里利用到一个CGI的特性——HTTP的相关变量会放在环境变量中,就像以下常见变量:
- HTTP_HOST
- REQUEST_METHOD
- QUERY_STRING
而环境变量事实上就是被放置在栈的最末端,也就是可执行区域,因此我们只要伪造vtable,直接调用shellcode就可以了!
当然这时候同样出现了一个问题:我们仍旧没有栈的内存位置。这个时候有些人可能会陷入一个死胡同,觉得攻击就是要一次到位,像个狙击手一样一击必杀,但实际上可能是拿导弹枪把敌人炸飞:
换个角度思考,这个二进制是32 bits的,因此这个位置有1.5字节是随机的,总共有16的3次方个可能的组合,所以平均只要4096次请求就可以撞对一次!这对于现在的电脑、网络来说其实也就是几分钟之间的事情,因此直接暴力破解就可!于是我们最终的exploit流程就是:
1.发送POST请求至cgi_api
QUERY_STRING
中放入shellcode
2.触发越界写入,覆盖file_vec
越界参数准备好伪造的FILE
和vtable
3.cgi_api
结束前调用MCGI_VarClear
4.跳转至vtable
上的shellcode位置,弹回shell
最后我们成功写出了不用认证的RCE攻击链,并且这个exploit不会因为版本不同而受影响!而在实际遇到的案例中也证明了这个exploit的通用性,我们曾在一次的演练当中借由Mail2000的这个1day作为突破口,成功获取目标的VPN数据,进一步往内网渗透!
漏洞修复
此漏洞已在2018/05/08 发布的Mail2000 V7 Patch 050版本中完成了修复。修复编号为OF-ISAC-18-002、OF-ISAC-18-003。
本文由白帽汇整理,不代表白帽汇任何观点和立场
来源:https://devco.re/blog/2019/12/23/how-binary-dog-survives-in-web-world/
最新评论