Path: blob/master/modules/auxiliary/fuzzers/ftp/client_ftp.rb
19670 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45##6# Fuzzer written by corelanc0d3r - <peter.ve [at] corelan.be>7# http://www.corelan.be:8800/index.php/2010/10/12/death-of-an-ftp-client/8#9##1011class MetasploitModule < Msf::Auxiliary12include Exploit::Remote::TcpServer1314def initialize15super(16'Name' => 'Simple FTP Client Fuzzer',17'Description' => %q{18This module will serve an FTP server and perform FTP client interaction fuzzing19},20'Author' => [ 'corelanc0d3r <peter.ve[at]corelan.be>' ],21'License' => MSF_LICENSE,22'References' => [23[ 'URL', 'http://www.corelan.be:8800/index.php/2010/10/12/death-of-an-ftp-client/' ],24],25'Notes' => {26'Stability' => [CRASH_SERVICE_DOWN],27'SideEffects' => [],28'Reliability' => []29}30)31register_options(32[33OptPort.new('SRVPORT', [ true, 'The local port to listen on.', 21 ]),34OptString.new('FUZZCMDS', [ true, 'Comma separated list of commands to fuzz (Uppercase).', 'LIST,NLST,LS,RETR', nil, /(?:[A-Z]+,?)+/ ]),35OptInt.new('STARTSIZE', [ true, 'Fuzzing string startsize.', 1000]),36OptInt.new('ENDSIZE', [ true, 'Max Fuzzing string size.', 200000]),37OptInt.new('STEPSIZE', [ true, 'Increment fuzzing string each attempt.', 1000]),38OptBool.new('RESET', [ true, 'Reset fuzzing values after client disconnects with QUIT cmd.', true]),39OptString.new('WELCOME', [ true, 'FTP Server welcome message.', 'Evil FTP Server Ready']),40OptBool.new('CYCLIC', [ true, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),41OptBool.new('ERROR', [ true, 'Reply with error codes only', false]),42OptBool.new('EXTRALINE', [ true, "Add extra CRLF's in response to LIST", true])43]44)45end4647# Not compatible today48def support_ipv6?49false50end5152def setup53super54@state = {}55end5657def run58@fuzzsize = datastore['STARTSIZE'].to_i59exploit60end6162# Handler for new FTP client connections63def on_client_connect(client)64@state[client] = {65name: "#{client.peerhost}:#{client.peerport}",66ip: client.peerhost,67port: client.peerport,68user: nil,69pass: nil70}71# set up an active data port on port 2072print_status("Client connected : #{client.peerhost}")73active_data_port_for_client(client, 20)74send_response(client, '', 'WELCOME', 220, ' ' + datastore['WELCOME'])75# from this point forward, on_client_data() will take over76end7778def on_client_close(client)79@state.delete(client)80end8182# Active and Passive data connections83def passive_data_port_for_client(client)84@state[client][:mode] = :passive85if !(@state[client][:passive_sock])86s = Rex::Socket::TcpServer.create(87'LocalHost' => '0.0.0.0',88'LocalPort' => 0,89'Context' => { 'Msf' => framework, 'MsfExploit' => self }90)91dport = s.getsockname[2]92@state[client][:passive_sock] = s93@state[client][:passive_port] = dport94print_status(" - Set up passive data port #{dport}")95end96@state[client][:passive_port]97end9899def active_data_port_for_client(client, port)100@state[client][:mode] = :active101connector = proc do102host = client.peerhost.dup103Rex::Socket::Tcp.create(104'PeerHost' => host,105'PeerPort' => port,106'Context' => { 'Msf' => framework, 'MsfExploit' => self }107)108end109@state[client][:active_connector] = connector110@state[client][:active_port] = port111print_status(" - Set up active data port #{port}")112end113114def establish_data_connection(client)115print_status(" - Establishing #{@state[client][:mode]} data connection")116begin117Timeout.timeout(20) do118if (@state[client][:mode] == :active)119return @state[client][:active_connector].call120end121if (@state[client][:mode] == :passive)122return @state[client][:passive_sock].accept123end124end125print_status(' - Data connection active')126rescue StandardError => e127print_error("Failed to establish data connection: #{e.class} #{e}")128end129nil130end131132# FTP Client-to-Server Command handlers133def on_client_data(client)134# get the client data135data = client.get_once136return if !data137138# split data into command and arguments139cmd, arg = data.strip.split(/\s+/, 2)140arg ||= ''141142return if !cmd143144# convert commands to uppercase and strip spaces145case cmd.upcase.strip146147when 'USER'148@state[client][:user] = arg149send_response(client, arg, 'USER', 331, ' User name okay, need password')150return151152when 'PASS'153@state[client][:pass] = arg154send_response(client, arg, 'PASS', 230, "-Password accepted.\r\n230 User logged in.")155return156157when 'QUIT'158if datastore['RESET']159print_status('Resetting fuzz settings')160@fuzzsize = datastore['STARTSIZE']161@stepsize = datastore['STEPSIZE']162end163print_status('** Client disconnected **')164send_response(client, arg, 'QUIT', 221, ' User logged out')165return166167when 'SYST'168send_response(client, arg, 'SYST', 215, ' UNIX Type: L8')169return170171when 'TYPE'172send_response(client, arg, 'TYPE', 200, " Type set to #{arg}")173return174175when 'CWD'176send_response(client, arg, 'CWD', 250, ' CWD Command successful')177return178179when 'PWD'180send_response(client, arg, 'PWD', 257, ' "/" is current directory.')181return182183when 'REST'184send_response(client, arg, 'REST', 200, ' OK')185return186187when 'XPWD'188send_response(client, arg, 'PWD', 257, ' "/" is current directory')189return190191when 'SIZE'192send_response(client, arg, 'SIZE', 213, ' 1')193return194195when 'MDTM'196send_response(client, arg, 'MDTM', 213, " #{Time.now.strftime('%Y%m%d%H%M%S')}")197return198199when 'CDUP'200send_response(client, arg, 'CDUP', 257, ' "/" is current directory')201return202203when 'PORT'204port = arg.split(',')[4, 2]205if !port && (port.length == 2)206client.put("500 Illegal PORT command.\r\n")207return208end209port = port.map(&:to_i).pack('C*').unpack('n')[0]210active_data_port_for_client(client, port)211send_response(client, arg, 'PORT', 200, ' PORT command successful')212return213214when 'PASV'215print_status("Handling #{cmd.upcase} command")216daddr = Rex::Socket.source_address(client.peerhost)217dport = passive_data_port_for_client(client)218@state[client][:daddr] = daddr219@state[client][:dport] = dport220pasv = (daddr.split('.') + [dport].pack('n').unpack('CC')).join(',')221dofuzz = fuzz_this_cmd('PASV')222code = 227223if datastore['ERROR']224code = 557225end226if (dofuzz == 1)227print_status(" * Fuzzing response for PASV, payload length #{@fuzzdata.length}")228send_response(client, arg, 'PASV', code, " Entering Passive Mode (#{@fuzzdata},1,1,1,1,1)\r\n")229incr_fuzzsize230else231send_response(client, arg, 'PASV', code, " Entering Passive Mode (#{pasv})")232end233return234235when /^(LIST|NLST|LS)$/236# special case - requires active/passive connection237print_status("Handling #{cmd.upcase} command")238conn = establish_data_connection(client)239if !conn240client.put("425 Can't build data connection\r\n")241return242end243print_status(' - Data connection set up')244code = 150245if datastore['ERROR']246code = 550247end248client.put("#{code} Here comes the directory listing.\r\n")249code = 226250if datastore['ERROR']251code = 550252end253client.put("#{code} Directory send ok.\r\n")254strfile = 'passwords.txt'255strfolder = 'Secret files'256dofuzz = fuzz_this_cmd('LIST')257if (dofuzz == 1)258strfile = @fuzzdata + '.txt'259strfolder = @fuzzdata260paylen = @fuzzdata.length261print_status("* Fuzzing response for LIST, payload length #{paylen}")262incr_fuzzsize263end264print_status(' - Sending directory list via data connection')265if datastore['EXTRALINE']266extra = "\r\n"267else268extra = ''269end270dirlist = "drwxrwxrwx 1 100 0 11111 Jun 11 21:10 #{strfolder}\r\n" + extra271dirlist << "-rw-rw-r-- 1 1176 1176 1060 Aug 16 22:22 #{strfile}\r\n" + extra272conn.put("total 2\r\n" + dirlist)273conn.close274return275276when 'RETR'277# special case - requires active/passive connection278print_status("Handling #{cmd.upcase} command")279conn = establish_data_connection(client)280if !conn281client.put("425 Can't build data connection\r\n")282return283end284print_status(' - Data connection set up')285strcontent = 'blahblahblah'286dofuzz = fuzz_this_cmd('LIST')287if (dofuzz == 1)288strcontent = @fuzzdata289paylen = @fuzzdata.length290print_status("* Fuzzing response for RETR, payload length #{paylen}")291incr_fuzzsize292end293client.put("150 Opening BINARY mode data connection #{strcontent}\r\n")294print_status(' - Sending data via data connection')295conn.put(strcontent)296client.put("226 Transfer complete\r\n")297conn.close298return299300when /^(STOR|MKD|REM|DEL|RMD)$/301send_response(client, arg, cmd.upcase, 500, ' Access denied')302return303304when 'FEAT'305send_response(client, arg, 'FEAT', '', "211-Features:\r\n211 End")306return307308when 'HELP'309send_response(client, arg, 'HELP', 214, " Syntax: #{arg} - (#{arg}-specific commands)")310311when 'SITE'312send_response(client, arg, 'SITE', 200, ' OK')313return314315when 'NOOP'316send_response(client, arg, 'NOOP', 200, ' OK')317return318319when 'ABOR'320send_response(client, arg, 'ABOR', 225, ' Abor command successful')321return322323when 'ACCT'324send_response(client, arg, 'ACCT', 200, ' OK')325return326327when 'RNFR'328send_response(client, arg, 'RNRF', 350, ' File.exist')329return330331when 'RNTO'332send_response(client, arg, 'RNTO', 350, ' File.exist')333return334335else336send_response(client, arg, cmd.upcase, 200, ' Command not understood')337return338end339340return341end342343# Fuzzer functions344345# Do we need to fuzz this command ?346def fuzz_this_cmd(cmd)347@fuzzcommands = datastore['FUZZCMDS'].split(',')348349fuzzme = 0350@fuzzcommands.each do |thiscmd|351if ((cmd.upcase == thiscmd.upcase) || (thiscmd == '*')) && (fuzzme == 0)352fuzzme = 1353break354end355end356357if fuzzme == 1358# should we use a cyclic pattern, or just A's ?359if datastore['CYCLIC']360@fuzzdata = Rex::Text.pattern_create(@fuzzsize)361else362@fuzzdata = 'A' * @fuzzsize363end364end365366return fuzzme367end368369def incr_fuzzsize370@stepsize = datastore['STEPSIZE'].to_i371@fuzzsize += @stepsize372print_status("(i) Setting next payload size to #{@fuzzsize}")373if (@fuzzsize > datastore['ENDSIZE'].to_i)374@fuzzsize = datastore['ENDSIZE'].to_i375end376end377378# Send data back to the server379def send_response(client, arg, cmd, code, msg)380if arg.length > 40381showarg = arg[0, 40] + '...'382else383showarg = arg384end385386if cmd.length > 40387showcmd = cmd[0, 40] + '...'388else389showcmd = cmd390end391392print_status("Sending response for '#{showcmd}' command, arg #{showarg}")393dofuzz = fuzz_this_cmd(cmd)394395## Fuzz this command ? (excluding PASV, which is handled in the command handler)396if (dofuzz == 1) && (cmd.upcase != 'PASV')397paylen = @fuzzdata.length398print_status("* Fuzzing response for #{cmd.upcase}, payload length #{paylen}")399if datastore['ERROR']400code = '550 '401end402if cmd == 'FEAT'403@fuzzdata = "211-Features:\r\n " + @fuzzdata + "\r\n211 End"404end405if cmd == 'PWD'406@fuzzdata = ' "/' + @fuzzdata + '" is current directory'407end408cmsg = code.to_s + ' ' + @fuzzdata409client.put("#{cmsg}\r\n")410print_status('* Fuzz data sent')411incr_fuzzsize412else413# Do not fuzz414cmsg = code.to_s + msg415cmsg = cmsg.strip416client.put("#{cmsg}\r\n")417end418end419end420421422