CVE-2019-5096

Catalpa 网络安全爱好者

好久没写博客了,一直在忙考试之类的事情,这个月初(2019-12-02) Cisco 的安全专家披露了 GoAhead 开源 web 服务中存在两个漏洞,CVE 编号 5096 和 5097,5096 是一枚 UAF 漏洞(号称能够达到 RCE),5097 是一枚 DDoS 漏洞。我们简单分析一下 5096.

环境准备

  1. 首先去 GitHub clone 下来 GoAhead 的源代码
1
git clone https://github.com/embedthis/goahead
  1. 查找最近的更新版本信息
1
git log --pretty=oneline

  1. 和此漏洞有关的是 FIX Issue #287,那么我们选择签出其之前的一个版本
1
git checkout f42afd767f90358908f5ef46b4e5bee8803d5ac5
  1. 修改源代码,打开 osdep.c 文件,将函数 websTempFile 修改为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PUBLIC char *websTempFile(cchar *dir, cchar *prefix)
{
static int count = 0;
char sep;

sep = '/';
if (!dir || *dir == '\0') {
#if WINCE
dir = "/Temp";
sep = '\\';
#elif ME_WIN_LIKE
dir = getenv("TEMP");
sep = '\\';
#elif VXWORKS
dir = ".";
#else
dir = "/tmp";
#endif
}
if (!prefix) {
prefix = "tmp";
}
return sfmt("/tmp%c%s-%d.tmp", sep, prefix, count++);
}
  1. 编译程序,用 VSCode 打开 src 文件夹准备分析。

漏洞分析

编译好的二进制文件在 build 目录下,我们首先分析一下源代码的大致逻辑。

PS:VSCode 安装了 C/C++ 插件的话可以很方便的查找函数和变量的交叉引用

根据披露信息 ,漏洞发生的位置是 freeUploadFile 函数,在文章中还给出了相关的崩溃信息:

1
2
3
4
5
6
7
8
9
10
11
12
double free or corruption (fasttop)

[#0] 0x7ffff6dc1e97 → __GI_raise(sig=0x6)
[#1] 0x7ffff6dc3801 → __GI_abort()
[#2] 0x7ffff6e0c897 → __libc_message(action=do_abort, fmt=0x7ffff6f39b9a "%s\n")
[#3] 0x7ffff6e1390a → malloc_printerr(str=0x7ffff6f3b828 "double free or corruption (fasttop)")
[#4] 0x7ffff6e1b004 → _int_free(have_lock=0x0, p=0x611a00, av=0x7ffff716ec40 <main_arena>)
[#5] 0x7ffff6e1b004 → __GI___libc_free(mem=0x611a10)
[#6] 0x7ffff7b6e897 → freeUploadFile(up=0x611a50)
[#7] 0x7ffff7b6e7fc → websFreeUpload(wp=0x60d680)
[#8] 0x7ffff7b62274 → reuseConn(wp=0x60d680)
[#9] 0x7ffff7b5e438 → complete(wp=0x60d680, reuse=0x1)

大致的函数调用流程是 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
2
3
4
5
6
7
8
9
--banner
data header...

data
--banner
data header...

data
--banner--

利用 banner 分割开不同的数据块,每个数据块表示一个 HTML 表单。数据块基本格式又包括 header 和数据本体。处理起来的基本思路就是 读取 分割行banner –> 读取 data header –> 读取 data –> 寻找下一个 banner… 循环处理直到 banner 结束为止。

了解基本信息之后我们来看一下 GoAhead 在处理请求的时候到底做了什么?

主入口函数位于 http.c 中,我们暂且不谈 socket 部分,那么入口函数就是 websPump(),这个函数将处理请求的过程简单分为 5 个步骤,摘录源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
PUBLIC void websPump(Webs *wp)   // 根据不同的读取标志,调用不同的处理函数
{
bool canProceed;

for (canProceed = 1; canProceed; ) {
switch (wp->state) {
case WEBS_BEGIN: // 处理请求头
canProceed = parseIncoming(wp);
break;
case WEBS_CONTENT: // 处理请求体
canProceed = processContent(wp);
break;
case WEBS_READY:
if (!websRunRequest(wp)) {
/* Reroute if the handler re-wrote the request */
websRouteRequest(wp);
wp->state = WEBS_READY;
canProceed = 1;
continue;
}
canProceed = (wp->state != WEBS_RUNNING);
break;
case WEBS_RUNNING:
/* Nothing to do until websDone is called */
return;
case WEBS_COMPLETE: // 请求完成
canProceed = complete(wp, 1);
break;
}
}
}

5 个阶段分别是 BEGIN、CONTENT、READY、RUNNING、COMPLETE。从字面上可理解为 开始->接受数据->准备完成->处理阶段->处理完成收尾。

首先来看 parseIncoming 函数,大概通读一下这个函数实现了一些初始化操作,解析请求,构造 Webs 结构体等。

processContent 函数是我们的主要关注点,因为它和我们传入的数据息息相关,源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static bool processContent(Webs *wp)
{
bool canProceed;

canProceed = filterChunkData(wp); // 处理分块的数据?
if (!canProceed || wp->finalized) {
return canProceed;
}
#if ME_GOAHEAD_UPLOAD
if (wp->flags & WEBS_UPLOAD) { // 如果请求的类型是 multipart/form-data
canProceed = websProcessUploadData(wp); // 根据披露信息来看,可能的漏洞入口
if (!canProceed || wp->finalized) {
return canProceed;
}
}
#endif
#if !ME_ROM
if (wp->putfd >= 0) {
canProceed = websProcessPutData(wp);
if (!canProceed || wp->finalized) {
return canProceed;
}
}
#endif
#if ME_GOAHEAD_CGI
if (wp->cgifd >= 0) {
canProceed = websProcessCgiData(wp);
if (!canProceed || wp->finalized) {
return canProceed;
}
}
#endif
if (wp->eof) {
wp->state = WEBS_READY;
/*
Prevent reading content from the next request
The handler may not have been created if all the content was read in the initial read. No matter.
*/
socketDeleteHandler(wp->sid);
}
return canProceed;
}

此函数接受 Webs 结构体作为参数,这个结构体由用户传入的请求抽象而成,之后的大部分操作都围绕着这个结构体进行,在 VSCode 中可以看到此结构体的具体定义。

主要关注此函数的

1
2
3
4
5
6
7
8
#if ME_GOAHEAD_UPLOAD
if (wp->flags & WEBS_UPLOAD) { // 如果请求的类型是 multipart/form-data
canProceed = websProcessUploadData(wp); // 根据披露信息来看,可能的漏洞入口
if (!canProceed || wp->finalized) {
return canProceed;
}
}
#endif

我们可以在 VSCode 中搜索 multipart/form-data 字符串,得到以下代码

1
2
3
4
5
6
7
8
9
10
else if (strcmp(key, "content-type") == 0) {
wfree(wp->contentType);
wp->contentType = sclone(value);
if (strstr(value, "application/x-www-form-urlencoded")) {
wp->flags |= WEBS_FORM;
} else if (strstr(value, "application/json")) {
wp->flags |= WEBS_JSON;
} else if (strstr(value, "multipart/form-data")) {
wp->flags |= WEBS_UPLOAD;
}

如果 content-type 字段的内容是 multipart/form-data,则 flags 会被置为 WEBS_UPLOAD。

和上面的代码相呼应,如果在 flags 中判断到了 WEBS_UPLOAD 表示请求的类型是 multipart/form-data。那么接下来需要分析 websProcessUploadData 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
PUBLIC bool websProcessUploadData(Webs *wp)    // 可能的漏洞入口
{
char *line, *nextTok;
ssize nbytes;
bool canProceed;

line = 0;
canProceed = 1;
while (canProceed && !wp->finalized && wp->uploadState != UPLOAD_CONTENT_END) {
if (wp->uploadState == UPLOAD_BOUNDARY || wp->uploadState == UPLOAD_CONTENT_HEADER) {
/*
Parse the next input line
*/
line = wp->input.servp;
if ((nextTok = memchr(line, '\n', bufLen(&wp->input))) == 0) { // 找到分割边界这一行的结尾换行符
/* Incomplete line */
canProceed = 0; // 找不到的话说明这个请求不对劲
break;
}
*nextTok++ = '\0'; // 换行符变成 \x00
nbytes = nextTok - line; // 计算分割行的长度
assert(nbytes > 0); // 必须大于 0
websConsumeInput(wp, nbytes);
strim(line, "\r", WEBS_TRIM_END); // 从字符串中去掉某个字符?
}
switch (wp->uploadState) {
case 0:
initUpload(wp);
break;

case UPLOAD_BOUNDARY:
processContentBoundary(wp, line);
break;

case UPLOAD_CONTENT_HEADER: // 分割行的下一行(数据内容头部)
processUploadHeader(wp, line);
break;

case UPLOAD_CONTENT_DATA:
canProceed = processContentData(wp);
if (bufLen(&wp->input) < wp->boundaryLen) {
/* Incomplete boundary - return to get more data */
canProceed = 0;
}
break;

case UPLOAD_CONTENT_END:
break;
}
}
bufCompact(&wp->input);
return canProceed;
}

此函数位于 upload.c 中,第 106 行。通读下来,函数主要的功能是定位 banner 以及(如上所述)处理 data 等。

其处理流程和我们之前提到的类似,先找到分割行然后根据匹配到的不同 token 调用不同的处理函数,相关的注释在代码中已经给出。

先来看处理分块数据头部的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
static void processUploadHeader(Webs *wp, char *line)    // 处理分块数据头部
{
WebsUpload *file;
char *key, *headerTok, *rest, *nextPair, *value;

if (line[0] == '\0') {
wp->uploadState = UPLOAD_CONTENT_DATA;
return;
}
trace(7, "Header line: %s", line);

headerTok = line;
stok(line, ": ", &rest);

if (scaselesscmp(headerTok, "Content-Disposition") == 0) {
/*
The content disposition header describes either a form variable or an uploaded file.
头部的 Content-Disposition 字段表明了这块数据到底是表单变量,还是上传的文件

Content-Disposition: form-data; name="field1"
>>blank line
Field Data
---boundary

Content-Disposition: form-data; name="field1" filename="user.file"
>>blank line
File data
---boundary
*/
key = rest;
wfree(wp->uploadVar);
wfree(wp->clientFilename);
wp->uploadVar = wp->clientFilename = 0;
while (key && stok(key, ";\r\n", &nextPair)) {

key = strim(key, " ", WEBS_TRIM_BOTH);
ssplit(key, "= ", &value);
value = strim(value, "\"", WEBS_TRIM_BOTH); // 去掉多余的字符

if (scaselesscmp(key, "form-data") == 0) { // 匹配到 form-data 通用字段
/* Nothing to do */

} else if (scaselesscmp(key, "name") == 0) { // 匹配到 name 字段,表单和 file 都有
wfree(wp->uploadVar);
wp->uploadVar = sclone(value); // 把 name 的内容交给 wp 结构体

} else if (scaselesscmp(key, "filename") == 0) { // 匹配到 file 情况
if (wp->uploadVar == 0) {
websError(wp, HTTP_CODE_BAD_REQUEST, "Bad upload state. Missing name field");
return;
}
value = websNormalizeUriPath(value); // Normalize a URI path to remove "./", "../" and redundant separators.

if (*value == '.' || !websValidUriChars(value) || strpbrk(value, "\\/:*?<>|~\"'%`^\n\r\t\f")) {
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Bad upload client filename"); // 过滤不合法的文件名
wfree(value);
return;
}
wfree(wp->clientFilename);
wp->clientFilename = value; // 文件名给 wp

/*
Create the file to hold the uploaded data
*/
wfree(wp->uploadTmp);
if ((wp->uploadTmp = websTempFile(uploadDir, "tmp")) == 0) { // 创建临时文件
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR,
"Cannot create upload temp file %s. Check upload temp dir %s", wp->uploadTmp, uploadDir);
return;
}
trace(5, "File upload of: %s stored as %s", wp->clientFilename, wp->uploadTmp);

if ((wp->upfd = open(wp->uploadTmp, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0600)) < 0) { // 打开临时文件
websError(wp, HTTP_CODE_INTERNAL_SERVER_ERROR, "Cannot open upload temp file %s", wp->uploadTmp);
return;
}
/*
Create the files[id]
*/
freeUploadFile(wp->currentFile); // 关键点1
file = wp->currentFile = walloc(sizeof(WebsUpload));
memset(file, 0, sizeof(WebsUpload));
file->clientFilename = sclone(wp->clientFilename); // 每一项都对应着一个 chunk
file->filename = sclone(wp->uploadTmp);
}
key = nextPair;
}

} else if (scaselesscmp(headerTok, "Content-Type") == 0) {
if (wp->clientFilename) {
trace(5, "Set files[%s][CONTENT_TYPE] = %s", wp->uploadVar, rest);
wp->currentFile->contentType = sclone(rest);
}
}
}

此函数大致的功能是匹配不同字段的值,我这里简单分为三种情况。

其一:匹配 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
static bool processContentData(Webs *wp)
{
WebsUpload *file;
WebsBuf *content;
ssize size, nbytes, len;
char *data, *bp;

content = &wp->input;
file = wp->currentFile; // 之前处理过的 WebsUpload 结构体

size = bufLen(content);
if (size < wp->boundaryLen) {
/* Incomplete boundary. Return and get more data */
return 0;
}
if ((bp = getBoundary(wp, content->servp, size)) == 0) { // 寻找分割行
trace(7, "uploadFilter: Got boundary filename %x", wp->clientFilename);
if (wp->clientFilename) {
/*
No signature found yet. probably more data to come. Must handle split boundaries.
*/
data = content->servp;
nbytes = ((int) (content->endp - data)) - (wp->boundaryLen - 1);
if (writeToFile(wp, content->servp, nbytes) < 0) {
/* Proceed to handle error */
return 1;
}
websConsumeInput(wp, nbytes);
/* Get more data */
return 0;
}
}
data = content->servp;
nbytes = (bp) ? (bp - data) : bufLen(content);

if (nbytes > 0) {
/*
This is the CRLF before the boundary
*/
len = nbytes;
if (len >= 2 && data[len - 2] == '\r' && data[len - 1] == '\n') {
len -= 2;
}
if (wp->clientFilename) {
/*
Write the last bit of file data and add to the list of files and define environment variables
*/
if (writeToFile(wp, data, len) < 0) { // 写入文件错误
/* Proceed to handle error */
websConsumeInput(wp, nbytes);
return 1;
}
hashEnter(wp->files, wp->uploadVar, valueSymbol(file), 0); // 关键点2
defineUploadVars(wp);

} else if (wp->uploadVar) {
/*
Normal string form data variables
*/
data[len] = '\0';
trace(5, "uploadFilter: form[%s] = %s", wp->uploadVar, data);
websDecodeUrl(wp->uploadVar, wp->uploadVar, -1);
websDecodeUrl(data, data, -1);
websSetVar(wp, wp->uploadVar, data);
}
websConsumeInput(wp, nbytes);
}
if (wp->clientFilename) {
/*
Now have all the data (we've seen the boundary)
*/
close(wp->upfd);
wp->upfd = -1;
wfree(wp->clientFilename);
wp->clientFilename = 0;
wfree(wp->uploadTmp);
wp->uploadTmp = 0;
}
wp->uploadState = UPLOAD_BOUNDARY;
return 1;
}

此函数主要实现的功能是找到 currentFile 字段(也就是上一个函数生成的结构),然后继续搜索请求体直到找到下一个 banner 为止,将找到的数据通过 writeToFile 函数写入临时文件中。

之后来到关键点 2。这里会将之前取出的 currentFile 通过 hashEnter 函数加入一个 hashtable,可以理解为一个链表或者数组。

程序会按照上述流程逐步处理每个数据块。这里我简单画一张图来表示这种处理流程:

发现什么问题了嘛?当数据包中只有一个 data 块的时候,这样处理没什么问题,但是当存在两个或者更多数据包的时候,第一次“循环”将 currentFile 指向的内存加入 hashtable,当再次返回 processUploadHeader 函数时,会对上一次处理的那个 currentFile 调用 freeUploadFile ,此时存在于 hashtable 中的内存已经处于 free 状态,为之后的触发埋下伏笔。

还记得之前说的 5 个状态?当一个请求结束的时候会调用 complete 清理环境,源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int complete(Webs *wp, int reuse)    // 请求处理结束
{
assert(wp);
assert(websValid(wp));
assert(wp->state == WEBS_BEGIN || wp->state == WEBS_COMPLETE);

if (reuse && wp->flags & WEBS_KEEP_ALIVE && wp->rxRemaining == 0) { // 如果存在 keep alive 情况
reuseConn(wp); // 重用链接?
socketCreateHandler(wp->sid, SOCKET_READABLE, socketEvent, wp);
trace(5, "Keep connection alive");
return 1;
}
trace(5, "Close connection");
wp->state = WEBS_BEGIN;
wp->flags |= WEBS_CLOSED;
return 0;
}

当请求头包含 Connection: keep-alive 字样的时候,程序尝试重用链接调用 reuseConn 函数。

1
2
3
4
5
6
7
8
9
10
11
12
static void reuseConn(Webs *wp)   // 重用链接?
{
assert(wp);
assert(websValid(wp));

bufCompact(&wp->rxbuf);
if (bufLen(&wp->rxbuf)) {
socketReservice(wp->sid);
}
termWebs(wp, 1); // 终止链接?
initWebs(wp, wp->flags & (WEBS_KEEP_ALIVE | WEBS_SECURE | WEBS_HTTP11), 1);
}

接着调用 termWebs 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
static void termWebs(Webs *wp, int reuse)
{
assert(wp);

/*
Some of this is done elsewhere, but keep this here for when a shutdown is done and there are open connections.
*/
bufFree(&wp->input);
bufFree(&wp->output);
bufFree(&wp->chunkbuf);
if (!reuse) {
bufFree(&wp->rxbuf);
if (wp->sid >= 0) {
#if ME_COM_SSL
sslFree(wp);
#endif
socketDeleteHandler(wp->sid);
socketCloseConnection(wp->sid);
wp->sid = -1;
}
}
#if !ME_ROM
if (wp->putfd >= 0) {
close(wp->putfd);
wp->putfd = -1;
assert(wp->putname && wp->filename);
if (rename(wp->putname, wp->filename) < 0) {
error("Cannot rename PUT file from %s to %s", wp->putname, wp->filename);
}
}
#endif
#if ME_GOAHEAD_CGI
if (wp->cgifd >= 0) {
close(wp->cgifd);
wp->cgifd = -1;
}
wfree(wp->cgiStdin);
#endif
#if ME_GOAHEAD_UPLOAD
wfree(wp->clientFilename);
#endif
websPageClose(wp);
if (wp->timeout >= 0 && !reuse) {
websCancelTimeout(wp);
}
wfree(wp->authDetails);
wfree(wp->authResponse);
wfree(wp->authType);
wfree(wp->contentType);
wfree(wp->cookie);
wfree(wp->decodedQuery);
wfree(wp->digest);
wfree(wp->ext);
wfree(wp->filename);
wfree(wp->host);
wfree(wp->method);
wfree(wp->password);
wfree(wp->path);
wfree(wp->protoVersion);
wfree(wp->putname);
wfree(wp->query);
wfree(wp->realm);
wfree(wp->referrer);
wfree(wp->url);
wfree(wp->userAgent);
wfree(wp->username);
#if ME_GOAHEAD_UPLOAD
wfree(wp->boundary);
wfree(wp->uploadTmp);
wfree(wp->uploadVar);
#endif
#if ME_GOAHEAD_DIGEST
wfree(wp->cnonce);
wfree(wp->digestUri);
wfree(wp->opaque);
wfree(wp->nc);
wfree(wp->nonce);
wfree(wp->qop);
#endif
hashFree(wp->vars);
hashFree(wp->responseCookies);

#if ME_GOAHEAD_UPLOAD
if (wp->files >= 0) { // 之前处理过的所有文件对象
websFreeUpload(wp);
}
#endif
}

此函数执行一系列的内存清理操作,前面的大部分代码没有问题,最后一部分代码出现纰漏

1
2
3
4
5
#if ME_GOAHEAD_UPLOAD
if (wp->files >= 0) { // 之前处理过的所有文件对象
websFreeUpload(wp);
}
#endif

wp->files 表示 hashtable 中元素的个数,正常上传文件这个字段会大于 0 ,此时调用函数 websFreeUpload,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PUBLIC void websFreeUpload(Webs *wp)
{
WebsUpload *up;
WebsKey *s;

if (wp->files >= 0) {
for (s = hashFirst(wp->files); s; s = hashNext(wp->files, s)) {
up = s->content.value.symbol;
freeUploadFile(up);
if (up == wp->currentFile) {
wp->currentFile = 0;
}
}
hashFree(wp->files);
}
if (wp->currentFile) {
freeUploadFile(wp->currentFile);
wp->currentFile = 0;
}
if (wp->upfd >= 0) {
close(wp->upfd);
wp->upfd = -1;
}
}

利用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>

<h1>hello worlds</h1>
<form action="/" method="post" enctype="multipart/form-data">
<p><input type="file" name="upload"></p>
<p><input type="file" name="upload2"></p>
<p><input type="file" name="upload3"></p>
<p><input type="submit" value="submit"></p>
</form>

</body>
</html>

之后用浏览器访问

选择三个文件之后点击 submit

此时程序已经由于 double free 崩溃。

对此我们可以简单调试分析一下,首先启动 goahead 程序,然后启动 gdb attach 上去,在 websFreeUpload 函数下断点

1
b websFreeUpload 

在浏览器中正常操作,点击 submit 之后 gdb 会断下,单步运行,第一次执行完 hashFirst 取得的元素地址是 0xdeb940,取得的 up 地址为 0xdef300

再次取得的元素地址依旧是 0xdef300

继续执行程序崩溃,这里我给出完整的函数崩溃信息

可以看到和我们前文的分析相同。

简单的总结

第一次分析这种开源软件,如果有任何问题还请各位大佬指正。

本文只分析了该漏洞的触发流程以及函数调用流程,至于披露者所称的可造成 RCE 我简单想了一下,感觉可利用空间不是特别大,不过还是看服务运行在哪个系统上面,如果是 2.27 等含有 tcache 的 libc 可能存在利用空间,但是 goahead 主要应用在嵌入式系统中,至于在这些系统能否 RCE 还有待商榷,希望有思路的大佬能交流一番。

  • Title: CVE-2019-5096
  • Author: Catalpa
  • Created at : 2019-12-23 00:00:00
  • Updated at : 2024-10-17 08:46:15
  • Link: https://wzt.ac.cn/2019/12/23/CVE-2019-5096/
  • License: This work is licensed under CC BY-NC-SA 4.0.