作者:启明星辰ADLab
国庆前,一位国外安全研究员在 The Fuzzing Project 网站上写了一篇 blog,文章描述了他发现的一个 Apache 信息泄漏漏洞(CVE-2017-9798),并给该漏洞起名“Optionsbleed”,意为发生在Options请求中的出血漏洞。尽管使用了“bleed”这个名字,但是作者也在文章中坦言,该漏洞的影响确实无法与心脏出血“Heartbleed”(CVE-2014-0160)漏洞媲美。首先,这个漏洞影响的目标并不像心脏出血那么多,其次该漏洞只能泄漏很少(甚至什么都泄漏不出来)内存。
漏洞作者仅仅给出了 ASAN 的分析截图和漏洞复现方法,并未纰漏更多细节,本文启明星辰ADLab安全研究员将对此漏洞进行详细深入地分析。
根据 blog 的描写,想要触发漏洞,需要在服务器目录下添加一个.htaccess
文件(这是一个配置文件),并且需要在该文件中添加一个没有被全局注册过的 Limit 指令。为此,ADLab研究员下载编译了存在漏洞的 Apache 版本2.4.27,修改了漏洞作者提供的poc并测试发包,在配置没有问题的服务器上结果是这样的。
接着在Apache服务器子目录下创建.htaccess
文件,并在文件中写入如下内容:
<Limit abcxyz>
</Limit>
证明漏洞存在(如下图“OPTIONS”后面多出来的“,”)。
事实上,通过漏洞作者提供的 poc 向 Apache 发消息后,Apache服务器一共处理了两次连接请求(之所以产生两次连接,涉及到 uri 重定向等一系列问题,细节和本漏洞无关所以不再赘述,感兴趣的可以参考mod_dir.c
及相关文件了解详情)。 每次处理连接请求时 Apache 都会为当前的连接分配一个 request 结构体,该结构体比较复杂,包括了各种连接相关的信息,这里我们只关心它的第一个变量 pool,即 request 的内存池。
Apache 服务器自行维护一套内存管理机制,这套内存管理机制最大的特点就是简单,它并不是按照大小将空闲的内存块连接到不同的链表上以期提高内存管理效率、减少内存碎片并节约内存,甚至不会频繁地释放无用内存。相反,Apache 会把下一块可用内存指针指向空闲内存的起始地址,不管需要分配多大的内存,都从空闲内存的起始地址开始分配,并将这个空闲地址的指针后移。相应的,当分配的内存不再被需要的时候,也不会主动的释放这块内存方便重复利用,而是选择在网络连接的收尾阶段将整块request_rec
的 pool 释放掉。
Apache 这么做可能主要基于服务器对每个网络连接处理的时间并不长,每个网络连接需要的内存池并不算太大这两个原因。所以,采用这种方式来管理内存不仅不会造成对内存的浪费反而提高了服务器的响应效率。
当服务器处理第一次连接的时候,Apache 为 request 分配的内存池地址为 0xaa102488。
Apache 根据请求的地址遍历其目录,当遍历到被请求目录时发现该目录下有.htaccess
文件,就会打开该文件读取并处理文件内容,当读到<Limit abcxyz>
这一行时,服务器会从request地址池里分配一块内存用于存放 abcxyz 字符串。
然后,流程进入ap_method_register
函数。
这个函数是非常重要的函数,我们来看一下其实现:
该函数中的methods_registry
是一个apr_hash_t
类型的结构体指针,这是一个用于描述服务器中注册方法的结构体,用 hash 表的方式来保存和查找注册的方法。实现如下:
其中,array是hash表数组,数组中的每一项都指向一个由若干个 hash 节点串联起来的单向链表。每个节点的数据结构如下:
从代码上看,服务器首先会调用apr_hash_get
来查找是否该方法(即 Limit 指令指定的方法)已经被注册,之前我们已经说过触发漏洞需要添加一个没有被全局注册过的 Limit 指令(本例中是 abcxyz),所以该查找一定会失败。而register_one_method
函数一定会被执行,这个函数主要是对apr_hash_set
的封装,所以重点看一下apr_hash_set
函数以及其子函数find_entry
。
apr_hash_set
函数功能是对 hash 表进行增删改操作,而函数的最后一个参数 val 就是用来确定到底要对 hash 表进行哪种操作的。如果 val 为0就对找到的hash节点进行删除操作,不为0就对 hash 表进行插入或者重新赋值操作,具体来说就是当这个 hash 节点不存在的时候就插入一个新的节点,否则对 val 重新赋值。find_entry
的解释请参见我们在代码中添加的注释。
在 hash 插入之前内存如下,我们看到在 array[3]中只有两个 hash 节点。
当节点被插入后内存如下,我们看到 hash 表中多了一个节点,这个节点就是我们在.htaccess
中定义的 Limit 指令 abcxyz。
当 Apache 将回应包通过 socket 发出后,request 地址池将会被释放(methods_registry
中 abcxyz 的 key 和 val 的地址也在这块内存池中)。但是,通过对图中内存的观察,我们发现methods_registry
中对 abcxyz 的引用还在。(Apache 的内存管理在释放内存之后并不会将内存中的数据覆盖,所以这时候我们在调试器里还能看到原有的数据)。
至此,第一次连接的主要流程已经走完,留下了一个对已经释放的地址有引用的 hash 节点。
第二次连接首先分配内存池。
继续往下进行,当服务器对配置文件中的新配置项进行添加的时候会执行如下指令,从 request 内存池里分配一块内存,该内存起始地址是 0xaa107718 大小为152字节,并通过 memcpy 函数对这块内存进行内存拷贝工作,拷贝之后 0xaa107758 和 0xaa107760(分别保存着释放前 hash 节点的 key 和 val,见第一次连接中 request 地址池释放图片中的蓝色方框)两处的内容都被覆盖成了0。
当服务器再次进行到注册的时候,由于 hash 节点 0x811b5d0 处的 key 和 val 都已经被拷贝成了新的数据,所以注册函数认为 abcxyz 的节点不存在,于是重新分配了一个节点。原来的节点就成了一个指向了空字符串且 val 为 0 的节点。
最后,当服务器开始对 Options 进行响应的时候,会进入一个叫做 make_allow 的函数,其作用就是生成 Options 返回的支持方法的字符串,这个函数中有一个很重要的变量 mask,它是一个选择子,用来判断 hash 表上哪些节点是符合要求的,在我们的调试器里这个值是37。
make_allow
具体实现如下,函数中的AP_METHOD_BIT
是 int64 型的1,当 mask 值为37(二进制100101)时,要满足 mask 的筛选条件 hash 节点的 val 值需要为0、2或5。0x811b5d0 这个节点(已经释放的abcxyz)本来是不符合要求的,但是由于其 val 已经被写成0了,所以在这里也符合了筛选条件,这就是我们之前看到 allow 中多了一个逗号的原因。其实真正的情况是 allow 里多出了一个字符串,不过这个字符串为空字符串,所以只是多了一个分割用的逗号。
在补丁中,我们看到服务器在注册methods_registry
前进行了验证,不让.htaccess
全局注册新方法,但并未真正地修补漏洞的 root cause,而是通过一个判断来阻止该漏洞执行到有问题的代码流程上。
这是一个由 UAF 导致的信息泄漏漏洞,正如漏洞作者所说这个漏洞的影响确实远小于心脏出血,原因如下:
第一,使用.htaccess
文件进行服务器配置本身并不被Apache官方推荐,因为这会极大的影响服务器的响应效率。理论上,只有对特定目录设置权限之类的特殊情况,.htaccess
文件才应该被使用,再加上想要触发漏洞还要满足.htaccess
文件中需定义一个未被全局注册过的Limit指令这一条件,使得符合情况的机器大幅减少。
第二,漏洞利用程序不一定能够稳定运行,只有当hash节点的val被覆盖成了0、2或5的情况下,字符串才能被当作是合法的方法加入到allow数组中。
第三,也是最重要的一点,这个漏洞泄漏的内存数据并不大。
Optionsbleed - HTTP OPTIONS method can leak Apache's server memory
启明星辰积极防御实验室(ADLab)
ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员。截止目前,ADLab通过CVE发布Windows、Linux、Unix等操作系统安全或软件漏洞近400个,持续保持国际网络安全领域一流水准。实验室研究方向涵盖操作系统与应用系统安全研究、移动智能终端安全研究、物联网智能设备安全研究、Web安全研究、工控系统安全研究、云安全研究。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。