## # $Id$ ## ## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # Framework web site for more information on licensing and terms of use. # http://metasploit.com/framework/ ## require 'msf/core' class Metasploit3 < Msf::Auxiliary include Msf::Auxiliary::Scanner include Msf::Exploit::Remote::Tcp def initialize super( 'Name' => 'Simple FTP Fuzzer', 'Description' => %q{ This module will connect to a FTP server and perform pre- and post-authentication fuzzing }, 'Author' => [ 'corelanc0d3r', 'jduck' ], 'License' => MSF_LICENSE, 'Version' => '$Revision$' ) register_options( [ Opt::RPORT(21), OptInt.new('STARTATSTAGE', [ false, "Start at this test stage",1]), OptInt.new('STEPSIZE', [ false, "Increase string size each iteration with this number of chars",10]), OptInt.new('DELAY', [ false, "Delay between connections in seconds",1]), OptInt.new('STARTSIZE', [ false, "Fuzzing string startsize",10]), OptInt.new('ENDSIZE', [ false, "Fuzzing string endsize",20000]), OptInt.new('STOPAFTER', [ false, "Stop after x number of consecutive errors",2]), OptString.new('USER', [ false, "Username",'anonymous']), OptString.new('PASS', [ false, "Password",'mozilla@example.com']), OptBool.new('FASTFUZZ', [ false, "Only fuzz with cyclic pattern",true]), OptBool.new('CONNRESET', [ false, "Break on CONNRESET error",true]), ], self.class) deregister_options('RHOST') @evilchars = [ 'A','a','%s','%d','%n','%x','%p','-1','0','0xfffffffe','0xffffffff','A/','//','/..','//..', 'A%20','./A','.A',',A','A:','!A','&A','?A','\A','../A/','..?','//A:','\\A','{A','$A','A*', 'cmd','A@a.com','#A','A/../','~','~A','~A/','A`/','>A',' Fuzzing size set to #{count} (#{prepend}#{evilstr})") initial_cmds.each do |cmd| send_pkt(cmd, true) end pkt = prepend + evil + "\r\n" send_pkt(pkt, true) sock.put("QUIT\r\n") select(nil, nil, nil, datastore['DELAY']) disconnect count += datastore['STEPSIZE'] rescue ::Exception => e @error_cnt += 1 print_status("Exception #{@error_cnt} of #{@nr_errors}") if (e.class.name == 'Rex::ConnectionRefused') or (e.class.name == 'EOFError') or (e.class.name == 'Errno::ECONNRESET' and datastore['CONNRESET']) or (e.class.name == 'Errno::EPIPE') if datastore['ExpandCrash'] print_status("Crash string : #{prepend}#{evil}") else print_status("Crash string : #{prepend}#{evilstr} x #{count}") end if @error_cnt >= @nr_errors print_status("System does not respond - exiting now\n") @stopprocess = true print_error("Error: #{e.class} #{e} #{e.backtrace}\n") return else print_status("Exception triggered, need #{@nr_errors - @error_cnt} more exception(s) before interrupting process") select(nil,nil,nil,3) #wait 3 seconds end end if @error_cnt >= @nr_errors count += datastore['STEPSIZE'] @error_cnt = 0 end end end end end end def ftp_commands if datastore['FtpCommands'].to_s.upcase == "DEFAULT" @commands else datastore['FtpCommands'].split(/[\s,]+/) end end def run_host(ip) startstage = datastore['STARTATSTAGE'] @nr_errors = datastore['STOPAFTER'] @error_cnt = 0 @stopprocess = false if datastore['FASTFUZZ'] @evilchars = [''] end print_status("Connecting to host " + ip + " on port " + datastore['RPORT']) if (startstage == 1) process_phase(1, "Fuzzing without command") startstage += 1 end if (startstage == 2) and (@stopprocess == false) process_phase(2, "Fuzzing USER", 'USER ') startstage += 1 end if (startstage == 3) and (@stopprocess == false) process_phase(3, "Fuzzing PASS", 'PASS ', [ "USER " + datastore['USER'] + "\r\n" ]) startstage += 1 end if (startstage == 4) print_status "[Phase 4] Fuzzing commands: #{ftp_commands.join(", ")}" ftp_commands().each do |cmd| if (@stopprocess == false) process_phase(4, "Fuzzing command: #{cmd}", "#{cmd} ", [ "USER " + datastore['USER'] + "\r\n", "PASS " + datastore['PASS'] + "\r\n" ]) end end # Don't progress into stage 5, it must be selected manually. #startstage += 1 end # Fuzz other commands, all command combinations in one session if (startstage == 5) print_status("[Phase 5] Fuzzing other commands (Part 2, #{Time.now.localtime}): #{ftp_commands.join(", ")}") ftp_commands().each do |cmd| if (@stopprocess == false) ecount = 1 count = datastore['STARTSIZE'] print_status("Fuzzing command #{cmd} - #{Time.now.localtime}" ) connect pkt = "USER " + datastore['USER'] + "\r\n" send_pkt(pkt, true) pkt = "PASS " + datastore['PASS'] + "\r\n" send_pkt(pkt, true) while count <= datastore['ENDSIZE'] print_status(" -> Fuzzing size set to #{count}") begin @evilchars.each do |evilstr| if datastore['FASTFUZZ'] evilstr = "Cyclic" evil = Rex::Text.pattern_create(count) @emax = 1 ecount = 1 else evil = evilstr * count end print_status(" Command : #{cmd}, Character : #{evilstr} (#{ecount}/#{@emax})") ecount += 1 pkt = cmd + " " + evil + "\r\n" send_pkt(pkt, true) select(nil, nil, nil, datastore['DELAY']) @error_cnt = 0 end rescue ::Exception => e @error_cnt += 1 print_status("Exception #{@error_cnt} of #{@nr_errors}") if (e.class.name == 'Rex::ConnectionRefused') or (e.class.name == 'EOFError') or (e.class.name == 'Errno::ECONNRESET' and datastore['CONNRESET']) or (e.class.name == 'Errno::EPIPE') if @error_cnt >= @nr_errors print_status("System does not respond - exiting now\n") @stopprocess = true print_error("Error: #{e.class} #{e} #{e.backtrace}\n") return else print_status("Exception triggered, need #{@nr_errors - @error_cnt} more exception(s) before interrupting process") select(nil,nil,nil,3) #wait 3 seconds end end if @error_cnt >= @nr_errors @error_cnt = 0 end end count += datastore['STEPSIZE'] end sock.put("QUIT\r\n") select(nil, nil, nil, datastore['DELAY']) disconnect end end end end end