本帖最后由 ench4nt3r 于 2016-7-11 12:48 编辑
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: 13
nseg : 5
现在来到了最重要的时刻,第二个for君要上场工作了。先看下它长啥样。
[C] 纯文本查看 复制代码 for (j = i = 0; i < nseg; i++, j++) {
sp = segments[i];
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[i];
}
}
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' // 长度 0
segments[1]: 'hello' // 长度 5
segments[2]: 'world' // 长度 5
segments[3]: 'world' // 长度 5
segments[4]: '.x' // 长度 2
segments[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[i]);
len = (int) slen(segments[i]);
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
|