##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < 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 <peter.ve[at]corelan.be>', 'jduck' ],
      'License'        => MSF_LICENSE
      )

    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]),
      ])

    @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','<A','A%n','A../','.././','A../',
      '....//','~?*/','.\../','\.//A','-%A','%Y','%H','/1','!','@','%','&','/?(*','*','(',')',
      '`',',','~/','/.','\$:','/A~%n','=','=:;)}','1.2.','41414141','-1234','999999,','%00','+A',
      '+123','..\'','??.','..\.\'','.../','1234123+',
      '%Y%%Y%/','%FC%80%80%80%80%AE%FC%80%80%80%80%AE/','????/','\uff0e/','%%32%65%%32%65/',
      '+B./','%%32%65%%32%65/','..%c0%af','..%e0%80%af','..%c1%9c'
    ]
    @commands = [
      'ABOR','ACCT','ALLO','APPE','AUTH','CWD','CDUP','DELE','FEAT','HELP','HOST','LANG','LIST',
      'MDTM','MKD','MLST','MODE','NLST','NLST -al','NOOP','OPTS','PASV','PORT','PROT','PWD','REIN',
      'REST','RETR','RMD','RNFR','RNTO','SIZE','SITE','SITE CHMOD','SITE CHOWN','SITE EXEC','SITE MSG',
      'SITE PSWD','SITE ZONE','SITE WHO','SMNT','STAT','STOR','STOU','STRU','SYST','TYPE','XCUP',
      'XCRC','XCWD','XMKD','XPWD','XRMD'
    ]
    @emax = @evilchars.length

    register_advanced_options(
      [
        OptString.new('FtpCommands', [ false, "Commands to fuzz at stages 4 and 5",@commands.join(" ")]),
        OptBool.new('ExpandCrash', [ false, "Expand any crash strings",false]),
    ])
  end


  def get_pkt
    buf = sock.get_once(-1, 10)
    vprint_status("[in ] #{buf.inspect}")
    buf
  end

  def send_pkt(pkt, get_resp = false)
    vprint_status("[out] #{pkt.inspect}")
    sock.put(pkt)
    get_pkt if get_resp
  end


  def process_phase(phase_num, phase_name, prepend = '', initial_cmds = [])
    print_status("[Phase #{phase_num}] #{phase_name} - #{Time.now.localtime}")
    ecount = 1
    @evilchars.each do |evilstr|

      if datastore['FASTFUZZ']
        evilstr = "Cyclic"
        @emax = 1
      end

      if (@stopprocess == false)
        count = datastore['STARTSIZE']
        print_status(" Character : #{evilstr} (#{ecount}/#{@emax})")
        ecount += 1
        while count <= datastore['ENDSIZE']
          begin
            connect
            if datastore['FASTFUZZ']
              evil = Rex::Text.pattern_create(count)
            else
              evil = evilstr * count
            end
            print_status("  -> 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'].to_s)

    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
