|
0x00 介绍
Embedthis Software GoAhead是美国Embedthis Software公司的一款嵌入式Web服务器。
Embedthis Software GoAhead 3.0.0版本至3.4.1版本中存在安全漏洞,该漏洞源于程序没有正确处理以‘.’字符开始的路径部分。远程攻击者可借助特制的URI利用该漏洞实施目录遍历攻击,造成拒绝服务(基于堆的缓冲区溢出和崩溃),也可能执行任意代码。[1]
0x01 环境
Ubuntu 15.10(I686,关闭ASLR、NX)
Goahead 3.4.1
Glibc 2.19
0x02 漏洞产生分析
瞧瞧代码
[C] 纯文本查看 复制代码for (mark = sp = dupPath; *sp; sp++) { if (*sp == '/') { *sp = '\0'; while (sp[1] == '/') { sp++; } segments[nseg++] = mark; len += (int) (sp - mark); mark = sp + 1; }}segments[nseg++] = mark;len += (int) (sp - mark);
在函数websNormalizeUriPath中,第一个for代码块,会将URI以’/‘分割,放入数组,并且统计URI字符串长度(不包括’/‘)。
来,让我们举个栗子看下。
websNormalizeUriPath收到了一个字符串参数,内容是”/hello/./world/.x”。第一个for君勤勤恳恳地工作,将字符串以’/‘分割并且统计好长度。此时各个变量的内容是这样的:
[C] 纯文本查看 复制代码segments[0]: '\0'segments[1]: 'hello'segments[2]: '.'segments[3]: 'world'segments[4]: '.x'segments[5] : '\0'len: 13nseg : 5
现在来到了最重要的时刻,第二个for君要上场工作了。先看下它长啥样。
[C] 纯文本查看 复制代码for (j = i = 0; i < nseg; i++, j++) { sp = segments; if (sp[0] == '.') { if (sp[1] == '\0') { if ((i+1) == nseg) { segments[j] = ""; } else { j--; } } else if (sp[1] == '.' && sp[2] == '\0') { if (i == 1 && *segments[0] == '\0') { j = 0; } else if ((i+1) == nseg) { if (--j >= 0) { segments[j] = ""; } } else { j = max(j - 2, -1); } } } else { segments[j] = segments; }}
segments同时肩负输入和输出的重任,i控制输入流的偏移,j控制输出流的偏移。
此时有两种情况处理,当sp为 ‘.’ 时,做一些操作。当sp不为 ‘.’ 时,直接将输入复制到输出。
仔细瞧瞧当sp为’.’时的处理,它做了以下的动作:
- 当下一个字符为0时,如果输出流到了末尾时((i+1) == nseg),直接复制空字符串到输出流。否则输出流不变(j–,在for的循环表达式中j++,以保持不变)
- 当下一个字符为 ‘.’ 并且sp[2]为0时,也就是sp为 “..”时。做*操作。(这里不讲了,不是重点。)重点来了,如果sp不是上面两种情况,将会啥都不做,比如sp为”.x”的话,那么它啥也不做,并且在for的循环表达式中将i跟j自增。
继续举个栗子瞧瞧:
还是以上面的字符串为例“/hello/./world/.x”
- ‘hello’直接从输入复制到输出
- ‘.’,j - 1。以保持不变
- ‘world’,将输入复制到输出。注意,在第2步中因为j不变,所以j现在是2,也就是’.’的位置。
- ‘.x’,啥也不做,i++,j++。到这里已经结束了,nseg为5,现在i也是5了,j为4
看看调整后segments的内容:
[C] 纯文本查看 复制代码segments[0]: '\0' // 长度 0segments[1]: 'hello' // 长度 5segments[2]: 'world' // 长度 5segments[3]: 'world' // 长度 5segments[4]: '.x' // 长度 2segments[5] : '\0'
继续往下走
[C] 纯文本查看 复制代码nseg = j;assert(nseg >= 0);if ((path = walloc(len + nseg + 1)) != 0) { for (i = 0, dp = path; i < nseg; ) { strcpy(dp, segments); len = (int) slen(segments); dp += len; if (++i < nseg || (nseg == 1 && *segments[0] == '\0' && firstc == '/')) { *dp++ = '/'; } } *dp = '\0';}
len使用的还是分割时计算的(13)。nseg被改成了j(4)。
看看上面调整后segments内字符串的长度:0 + 5 + 5 + 5 = 15。(nseg为4)
path new时的长度是13 + 4 + 1 (len + nseg + 1),而复制到path的字符串长度将是15 + 3 + 1。
很明显,在这发生了溢出。只要稍微构造一下就能触发unlink了。
漏洞分析完毕。
0x03 目录遍历
来,我们准备了这么一个字符串”/../../../../../.x/.x/.x/.x/.x/.x/etc/passwd”,在第二个for君的处理中,遇到”..”并且没到末尾的话,会将j-1,或者置0。
在处理了一连串的”..”之后,遇到了”.x”,我们知道它只会将i、j加1。看看处理完之后的segments吧
[C] 纯文本查看 复制代码(gdb) p segments[0]$9 = 0x8055a30 ""(gdb) p segments[1]$10 = 0x8055a31 ".."(gdb) p segments[2]$11 = 0x8055a34 ".."(gdb) p segments[3]$12 = 0x8055a37 ".."(gdb) p segments[4]$13 = 0x8055a3a ".."(gdb) p segments[5]$14 = 0x8055a3d ".."(gdb) p segments[6]$15 = 0x8055a52 "etc"(gdb) p segments[7]$16 = 0x8055a56 "passwd"(gdb) p segments[8]$17 = 0x8055a46 ".x"(gdb) p nseg $23 = 8
详情请看参考[2]的Directory traversal
0x04 远程命令执行
当执行到wfree(dupPath);的时候,内存布局大概如下:
path是能通过url控制的区域,只要溢出并且覆盖top的size(重点是覆盖点p位,置为0),这样的话,当free(segments)时,就会判断path是否为空闲,由于前面被我们将top的p位置为0,所以此时会unlink(path)。通过在path准备点蛋糕,就可以让shellcode执行了。
蛋糕打造过程:
因为glibc 2.19在unlink判断了fd和bk,所以想要直接通过fd和bk来覆盖函数地址是不可能了。
[C] 纯文本查看 复制代码#define unlink(P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P); \ else { \ FD->bk = BK; \ BK->fd = FD; \ if (!in_smallbin_range (P->size) \ && __builtin_expect (P->fd_nextsize != NULL, 0)) { \ assert (P->fd_nextsize->bk_nextsize == P); \ assert (P->bk_nextsize->fd_nextsize == P); \ if (FD->fd_nextsize == NULL) { \ if (P->fd_nextsize == P) \ FD->fd_nextsize = FD->bk_nextsize = FD; \ else { \ FD->fd_nextsize = P->fd_nextsize; \ FD->bk_nextsize = P->bk_nextsize; \ P->fd_nextsize->bk_nextsize = FD; \ P->bk_nextsize->fd_nextsize = FD; \ } \ } else { \ P->fd_nextsize->bk_nextsize = P->bk_nextsize; \ P->bk_nextsize->fd_nextsize = P->fd_nextsize; \ } \ } \ } \}
glibc 2.19的unlink如上。
从代码可以看出,Relase模式下,是对fd_nextsize和bk_nextsize没有进行判断的,但是fd_nextsize和bk_nextsize是在large blocks才有的,所以需要构造一个大于512字节(32位系统)的块。
我构造的path:
当segments被释放时,path会被认为是已经释放了的块,所以会触发consolIDAte forward。
fd和bk都指向path的地址,以通过”corrupted double-linked list”检查。
exp执行结果:
0x05 参考
[1]SCAP中文社区
[2]Advisory: CVE-2014-9707
[3]Understanding glibc malloc
[4]EXP
</i></i></i></i>
来源:http://www.12558.net
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |
|