好久没写博客了,一直在忙考试之类的事情,这个月初(2019-12-02) Cisco 的安全专家披露了 GoAhead 开源 web 服务中存在两个漏洞,CVE 编号 5096 和 5097,5096 是一枚 UAF 漏洞(号称能够达到 RCE),5097 是一枚 DDoS 漏洞。我们简单分析一下 5096.
环境准备
- 首先去 GitHub clone 下来 GoAhead 的源代码
1 | git clone https://github.com/embedthis/goahead |
- 查找最近的更新版本信息
1 | git log --pretty=oneline |
- 和此漏洞有关的是 FIX Issue #287,那么我们选择签出其之前的一个版本
1 | git checkout f42afd767f90358908f5ef46b4e5bee8803d5ac5 |
- 修改源代码,打开 osdep.c 文件,将函数 websTempFile 修改为
1 | PUBLIC char *websTempFile(cchar *dir, cchar *prefix) |
- 编译程序,用 VSCode 打开 src 文件夹准备分析。
漏洞分析
编译好的二进制文件在 build 目录下,我们首先分析一下源代码的大致逻辑。
PS:VSCode 安装了 C/C++ 插件的话可以很方便的查找函数和变量的交叉引用
根据披露信息,漏洞发生的位置是 freeUploadFile 函数,在文章中还给出了相关的崩溃信息:
1 | double free or corruption (fasttop) |
大致的函数调用流程是 complete -> reuseConn -> websFreeUpload -> freeUploadFile
但是按照这样的函数流程找下去的话你会感觉很疑惑,因为上述的函数调用链并不包括漏洞的核心代码。
我在分析这个漏洞的时候最初也是按照这个漏洞顺序找下去的,结果一无所获,网上寥寥几篇分析文章只有只言片语,无奈只能从头了解和这个漏洞有关的一些知识点。
首先,根据披露信息我们知道这个漏洞和 multi-part/form-data 类型的数据有关,引用其中的几句话
1 | When processing a multi-part/form-data HTTP request with multiple Content-Disposition headers in the same request, a use-after-free condition can occur while cleaning up the heap structures used for storing the different parts of the request. |
大概意思是说当程序处理 multi-part/form-data 格式数据的时候可能会发生 UAF,那么什么是 multi-part/form-data 呢?
百度简单了解了一下,它是 HTML 三种表单 enctype 类型之一,主要用于解决向服务器传输二进制数据的问题,其具体定义在 RFC2388 中。详细信息:https://blog.csdn.net/wyn126/article/details/96451357
简单来说它是一种特殊的数据传输格式,其基本结构是
1 | --banner |
利用 banner 分割开不同的数据块,每个数据块表示一个 HTML 表单。数据块基本格式又包括 header 和数据本体。处理起来的基本思路就是 读取 分割行banner –> 读取 data header –> 读取 data –> 寻找下一个 banner… 循环处理直到 banner 结束为止。
了解基本信息之后我们来看一下 GoAhead 在处理请求的时候到底做了什么?
主入口函数位于 http.c 中,我们暂且不谈 socket 部分,那么入口函数就是 websPump(),这个函数将处理请求的过程简单分为 5 个步骤,摘录源代码如下
1 | PUBLIC void websPump(Webs *wp) // 根据不同的读取标志,调用不同的处理函数 |
5 个阶段分别是 BEGIN、CONTENT、READY、RUNNING、COMPLETE。从字面上可理解为 开始->接受数据->准备完成->处理阶段->处理完成收尾。
首先来看 parseIncoming 函数,大概通读一下这个函数实现了一些初始化操作,解析请求,构造 Webs 结构体等。
processContent 函数是我们的主要关注点,因为它和我们传入的数据息息相关,源代码如下
1 | static bool processContent(Webs *wp) |
此函数接受 Webs 结构体作为参数,这个结构体由用户传入的请求抽象而成,之后的大部分操作都围绕着这个结构体进行,在 VSCode 中可以看到此结构体的具体定义。
主要关注此函数的
1 |
|
我们可以在 VSCode 中搜索 multipart/form-data 字符串,得到以下代码
1 | else if (strcmp(key, "content-type") == 0) { |
如果 content-type 字段的内容是 multipart/form-data,则 flags 会被置为 WEBS_UPLOAD。
和上面的代码相呼应,如果在 flags 中判断到了 WEBS_UPLOAD 表示请求的类型是 multipart/form-data。那么接下来需要分析 websProcessUploadData 函数。
1 | PUBLIC bool websProcessUploadData(Webs *wp) // 可能的漏洞入口 |
此函数位于 upload.c 中,第 106 行。通读下来,函数主要的功能是定位 banner 以及(如上所述)处理 data 等。
其处理流程和我们之前提到的类似,先找到分割行然后根据匹配到的不同 token 调用不同的处理函数,相关的注释在代码中已经给出。
先来看处理分块数据头部的部分
1 | static void processUploadHeader(Webs *wp, char *line) // 处理分块数据头部 |
此函数大致的功能是匹配不同字段的值,我这里简单分为三种情况。
其一:匹配 Content-Disposition 字段,它的值默认为 form-data
其二:匹配 name 字段,它的值为 HTML 表单中变量的名字,此字段无论是上传 file 或是提交普通表单都会存在。
其三:匹配 filename,这个字段仅仅当上传 file 的时候包含,它是客户端上传的文件的名字。
关键代码就是匹配到 file 的情况,此时函数会过滤文件名,确保不包含非法的字符,之后在临时目录创建文件(利用 websTempFile 函数实现,也就是我们一开始修改的函数),尝试打开这个临时文件,并将 FILE 指针传递给 wp->upfd 字段。
我们主要关注第 257 行,这是第一个关键点。此处将 wp->currentFile 指向的结构 free,freeUploadFile 函数就是在披露信息中给出的存在问题的函数。实际上这个函数本身没什么问题,只不过它的调用者没有做好相关的保护。
currentFile 保存的是我们之前提到的临时文件结构,第一次执行 processUploadHeader 函数的时候这个字段为空,通过 walloc(sizeof(WebsUpload)) 分配新的空间用于保存临时文件的名字和客户端传递过来的真实的文件名。
紧接着来到处理 data 本身的位置,第 145 行调用了函数 processContentData,代码如下
1 | static bool processContentData(Webs *wp) |
此函数主要实现的功能是找到 currentFile 字段(也就是上一个函数生成的结构),然后继续搜索请求体直到找到下一个 banner 为止,将找到的数据通过 writeToFile 函数写入临时文件中。
之后来到关键点 2。这里会将之前取出的 currentFile 通过 hashEnter 函数加入一个 hashtable,可以理解为一个链表或者数组。
程序会按照上述流程逐步处理每个数据块。这里我简单画一张图来表示这种处理流程:
发现什么问题了嘛?当数据包中只有一个 data 块的时候,这样处理没什么问题,但是当存在两个或者更多数据包的时候,第一次“循环”将 currentFile 指向的内存加入 hashtable,当再次返回 processUploadHeader 函数时,会对上一次处理的那个 currentFile 调用 freeUploadFile ,此时存在于 hashtable 中的内存已经处于 free 状态,为之后的触发埋下伏笔。
还记得之前说的 5 个状态?当一个请求结束的时候会调用 complete 清理环境,源代码如下
1 | static int complete(Webs *wp, int reuse) // 请求处理结束 |
当请求头包含 Connection: keep-alive 字样的时候,程序尝试重用链接调用 reuseConn 函数。
1 | static void reuseConn(Webs *wp) // 重用链接? |
接着调用 termWebs 函数
1 | static void termWebs(Webs *wp, int reuse) |
此函数执行一系列的内存清理操作,前面的大部分代码没有问题,最后一部分代码出现纰漏
1 |
|
wp->files 表示 hashtable 中元素的个数,正常上传文件这个字段会大于 0 ,此时调用函数 websFreeUpload,代码如下
1 | PUBLIC void websFreeUpload(Webs *wp) |
利用 hashFirst 函数从 hashTable 中取出元素,并对它调用 freeUploadFile,也就是所谓的存在问题的函数。
到这里漏洞产生的原因已经很明显了,为了更加直观的理解,我修改一下上面的图片:
由于 hashtable 中保存的元素都是处于 free 状态的,所以在 websFreeUpload 函数中再次对它们执行 free 操作就会导致 doble free。
漏洞复现
编译好程序,用命令启动
1 | sudo goahead -v --home /etc/goahead /var/www/goahead |
确认浏览器访问没有问题之后,修改位于 /var/www/goahead 目录下的 index.html 文件内容
1 |
|
之后用浏览器访问
选择三个文件之后点击 submit
此时程序已经由于 double free 崩溃。
对此我们可以简单调试分析一下,首先启动 goahead 程序,然后启动 gdb attach 上去,在 websFreeUpload 函数下断点
1 | b websFreeUpload |
在浏览器中正常操作,点击 submit 之后 gdb 会断下,单步运行,第一次执行完 hashFirst 取得的元素地址是 0xdeb940,取得的 up 地址为 0xdef300
再次取得的元素地址依旧是 0xdef300
继续执行程序崩溃,这里我给出完整的函数崩溃信息
可以看到和我们前文的分析相同。
简单的总结
第一次分析这种开源软件,如果有任何问题还请各位大佬指正。
本文只分析了该漏洞的触发流程以及函数调用流程,至于披露者所称的可造成 RCE 我简单想了一下,感觉可利用空间不是特别大,不过还是看服务运行在哪个系统上面,如果是 2.27 等含有 tcache 的 libc 可能存在利用空间,但是 goahead 主要应用在嵌入式系统中,至于在这些系统能否 RCE 还有待商榷,希望有思路的大佬能交流一番。
- 本文作者: CataLpa
- 本文链接: https://wzt.ac.cn/2019/12/23/CVE-2019-5096/
-
版权声明:
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。