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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'HTTP Form Field Fuzzer',
        'Description' => %q{
          This module will grab all fields from a form,
          and launch a series of POST actions, fuzzing the contents
          of the form fields. You can optionally fuzz headers too
          (option is enabled by default)
        },
        'Author' => [
          'corelanc0d3r',
          'Paulino Calderon <calderon[at]websec.mx>' # Added cookie handling
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['URL', 'http://www.corelan.be:8800/index.php/2010/11/12/metasploit-module-http-form-field-fuzzer'],
        ],
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN],
          'SideEffects' => [],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        OptString.new('URL', [ false, 'The URL that contains the form', '/']),
        OptString.new('FORM', [ false, 'The name of the form to use. Leave empty to fuzz all forms', '' ]),
        OptString.new('FIELDS', [ false, 'Name of the fields to fuzz. Leave empty to fuzz all fields', '' ]),
        OptString.new('ACTION', [ false, 'Form action full URI. Leave empty to autodetect', '' ]),
        OptInt.new('STARTSIZE', [ true, 'Fuzzing string startsize.', 1000]),
        OptInt.new('ENDSIZE', [ true, 'Max Fuzzing string size.', 40000]),
        OptInt.new('STEPSIZE', [ true, 'Increment fuzzing string each attempt.', 1000]),
        OptInt.new('TIMEOUT', [ true, 'Number of seconds to wait for response on GET or POST', 15]),
        OptInt.new('DELAY', [ true, 'Number of seconds to wait between 2 actions', 0]),
        OptInt.new('STOPAFTER', [ false, 'Stop after x number of consecutive errors', 2]),
        OptBool.new('CYCLIC', [ true, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),
        OptBool.new('FUZZHEADERS', [ true, 'Fuzz headers', true]),
        OptString.new('HEADERFIELDS', [ false, 'Name of the headerfields to fuzz. Leave empty to fuzz all fields', '' ]),
        OptString.new('TYPES', [ true, 'Field types to fuzz', 'text,password,inputtextbox']),
        OptString.new('CODE', [ true, 'Response code(s) indicating OK', '200,301,302,303' ]),
        OptBool.new('HANDLECOOKIES', [ true, 'Appends cookies with every request.', false])
      ]
    )
  end

  def init_vars
    proto = 'http://'
    if datastore['SSL']
      proto = 'https://'
    end

    @send_data = {
      uri: '',
      version: '1.1',
      method: 'POST',
      headers: {
        'Content-Length' => 100,
        'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Language' => 'en-us,en;q=0.5',
        'Accept-Encoding' => 'gzip,deflate',
        'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
        'Keep-Alive' => '300',
        'Connection' => 'keep-alive',
        'Referer' => proto + datastore['RHOST'] + ':' + datastore['RPORT'].to_s,
        'Content-Type' => 'application/x-www-form-urlencoded'
      }
    }
    @get_data_headers = {
      'Referer' => proto + datastore['RHOST'] + ':' + datastore['RPORT'].to_s
    }
  end

  def init_fuzzdata
    @fuzzsize = datastore['STARTSIZE']
    @endsize = datastore['ENDSIZE']
    set_fuzz_payload
    @nrerrors = 0
  end

  def incr_fuzzsize
    @stepsize = datastore['STEPSIZE'].to_i
    @fuzzsize += @stepsize
  end

  def set_fuzz_payload
    if datastore['CYCLIC']
      @fuzzdata = Rex::Text.pattern_create(@fuzzsize)
    else
      @fuzzdata = 'A' * @fuzzsize
    end
  end

  def is_error_code(code)
    okcode = false
    checkcodes = datastore['CODE'].split(',')
    checkcodes.each do |testcode|
      testcode = testcode.upcase.gsub(' ', '')
      if testcode == code.to_s.upcase.gsub(' ', '')
        okcode = true
      end
    end
    return okcode
  end

  def fuzz_this_field(fieldname, fieldtype)
    fuzzcommands = datastore['FIELDS'].split(',')
    fuzzme = 0
    if !fuzzcommands.empty?
      fuzzcommands.each do |thiscmd|
        thiscmd = thiscmd.strip
        if ((fieldname.upcase == thiscmd.upcase) || (thiscmd == '')) && (fuzzme == 0)
          fuzzme = 1
        end
      end
    else
      fuzztypes = datastore['TYPES'].split(',')
      fuzztypes.each do |thistype|
        if (fieldtype.upcase.strip == thistype.upcase.strip)
          fuzzme = 1
        end
      end
    end
    if fuzzme == 1
      set_fuzz_payload
    end
    return fuzzme
  end

  def fuzz_this_headerfield(fieldname)
    fuzzheaderfields = datastore['HEADERFIELDS'].split(',')
    fuzzme = 0
    if !fuzzheaderfields.empty?
      fuzzheaderfields.each do |thisfield|
        thisfield = thisfield.strip
        if ((fieldname.upcase == thisfield.upcase) || (thisfield == '')) && (fuzzme == 0)
          fuzzme = 1
        end
      end
    else
      fuzzme = 1
    end
    if fuzzme == 1
      set_fuzz_payload
    end
    return fuzzme
  end

  def do_fuzz_headers(form, headers)
    headercnt = 0
    datastr = ''
    form[:fields].each do |thisfield|
      normaldata = 'blah&'
      if thisfield[:value] && thisfield[:value] != ('')
        normaldata = thisfield[:value].strip + '&'
      end
      datastr << thisfield[:name].downcase.strip + '=' + normaldata
    end
    if !datastr.empty?
      datastr = datastr[0, datastr.length - 1] + "\r\n"
    else
      datastr = "\r\n"
    end
    # first, check the original header fields and add some others - just for fun
    myheaders = @send_data[:headers]
    mysendheaders = @send_data[:headers].dup
    # get or post ?
    mysendheaders[:method] = form[:method].upcase
    myheaders.each do |thisheader|
      if !(headers[thisheader[0]])
        # add header if needed
        mysendheaders[thisheader[0]] = thisheader[1]
      end
    end
    nrheaderstofuzz = mysendheaders.size
    mysendheaders.each do |thisheader|
      @fuzzheader = mysendheaders.dup
      @nrerrors = 0
      fuzzpacket = @send_data.dup
      fuzzpacket[:method] = mysendheaders[:method]
      headername = thisheader[0]
      if fuzz_this_headerfield(headername.to_s.upcase) == 1
        print_status("    - Fuzzing header '#{headername}' (#{headercnt + 1}/#{nrheaderstofuzz})")
        init_fuzzdata
        while @fuzzsize <= @endsize + 1
          @fuzzheader[headername] = @fuzzdata
          fuzzpacket[:headers] = @fuzzheader
          response = send_fuzz(fuzzpacket, datastr)
          if !process_response(response, headername, 'header')
            @fuzzsize = @endsize + 2
          end
          if datastore['DELAY'] > 0
            print_status("      (Sleeping for #{datastore['DELAY']} seconds...)")
            select(nil, nil, nil, datastore['DELAY'])
          end
          incr_fuzzsize
        end
      else
        print_status("    - Skipping header '#{headername}' (#{headercnt + 1}/#{nrheaderstofuzz})")
      end
      headercnt += 1
    end
  end

  def do_fuzz_field(form, field)
    fieldstofuzz = field.downcase.strip.split(',')
    @nrerrors = 0
    while @fuzzsize <= @endsize + 1
      allfields = form[:fields]
      datastr = ''
      normaldata = ''
      allfields.each do |thisfield|
        dofuzzthis = false
        next unless thisfield[:name]

        fieldstofuzz.each do |fuzzthis|
          next unless fuzzthis

          if (thisfield[:name].downcase.strip == fuzzthis.downcase.strip)
            dofuzzthis = true
          end
        end
        if thisfield[:value]
          normaldata = thisfield[:value].strip
        else
          normaldata = ''
        end
        if dofuzzthis
          datastr << thisfield[:name].downcase.strip + '=' + @fuzzdata + '&'
        else
          datastr << thisfield[:name].downcase.strip + '=' + normaldata + '&'
        end
      end
      datastr = datastr[0, datastr.length - 1]
      @send_data[:uri] = form[:action]
      @send_data[:uri] = "/#{form[:action]}" if @send_data[:uri][0, 1] != '/'

      @send_data[:method] = form[:method].upcase
      response = send_fuzz(@send_data, datastr)
      if !process_response(response, field, 'field')
        return
      end

      if datastore['DELAY'] > 0
        print_status("      (Sleeping for #{datastore['DELAY']} seconds...)")
        select(nil, nil, nil, datastore['DELAY'])
      end
    end
  end

  def process_response(response, field, type)
    if response.nil?
      print_error("      No response - #{@nrerrors + 1} / #{datastore['STOPAFTER']} - fuzzdata length : #{@fuzzsize}")
      if @nrerrors + 1 >= datastore['STOPAFTER']
        print_status("      *!* No response : #{type} #{field} | fuzzdata length : #{@fuzzsize}")
        return false
      else
        @nrerrors += 1
      end
    else
      okcode = is_error_code(response.code)
      if okcode
        @nrerrors = 0
        incr_fuzzsize
      end
      if !okcode && (@nrerrors + 1 >= datastore['STOPAFTER'])
        print_status("      *!* Error response code #{response.code} | #{type} #{field} | fuzzdata length #{@fuzzsize}")
        return false
      else
        @nrerrors += 1
      end
    end
    return true
  end

  def send_fuzz(postdata, data)
    header = postdata[:headers]
    response = send_request_raw({
      'uri' => postdata[:uri],
      'version' => postdata[:version],
      'method' => postdata[:method],
      'headers' => header,
      'data' => data
    }, datastore['TIMEOUT'])
    return response
  end

  def get_field_val(input)
    tmp = input.split(/=/)
    # get delimiter
    tmp2 = tmp[1].strip
    delim = tmp2[0, 1]
    if delim != "'" && delim != '"'
      delim = ''
    end
    tmp3 = tmp[1].split(/>/)
    tmp4 = tmp3[0].gsub(delim, '')
    return tmp4
  end

  def get_form_data(body)
    print_status('Enumerating form data')
    body = body.gsub("\r", '')
    body = body.gsub("\n", '')
    bodydata = body.downcase.split(/<form/)
    # we need part after <form
    totalforms = bodydata.size - 1
    print_status("    Number of forms : #{totalforms}")
    formcnt = 0
    formidx = 1
    forms = []
    while formcnt < totalforms
      fdata = bodydata[formidx]
      print_status("    - Enumerating form ##{formcnt + 1}")
      data = fdata.downcase.split(%r{</form>})
      # first, get action and name
      formdata = data[0].downcase.split(/>/)
      subdata = formdata[0].downcase.split(/ /)
      namefound = false
      actionfound = false
      idfound = false
      actionname = ''
      formname = ''
      formid = ''
      formmethod = 'post'
      subdata.each do |thisfield|
        if thisfield.match(/^name=/) && !namefound
          formname = get_field_val(thisfield)
          namefound = true
        end
        if thisfield.match(/^id=/) && !idfound
          formid = get_field_val(thisfield)
          idfound = true
        end
        if thisfield.match(/^method=/)
          formmethod = get_field_val(thisfield)
        end
        next unless thisfield.match(/^action=/) && !actionfound

        actionname = get_field_val(thisfield)
        if (actionname.length < datastore['URL'].length) && (datastore['URL'].downcase.index(actionname.downcase).to_i > -1)
          actionname = datastore['URL']
        end
        actionfound = true
      end
      if !datastore['ACTION'].empty?
        actionname = datastore['ACTION']
        actionfound = true
      end

      if (formname == '') && (formid != '')
        formname = formid
      end
      if (formid == '') && (formname != '')
        formid = formname
      end
      if (formid == '') && (formname == '')
        formid = 'noname_' + (formcnt + 1).to_s
        formname = formid
      end
      idfound = true
      namefound = true

      formfields = []
      # input boxes
      fieldtypemarks = [ '<input', '<select' ]
      fieldtypemarks.each do |currfieldmark|
        formfieldcnt = 0
        next unless (namefound || idfound) && actionfound

        # get fields in current form - data[0]
        subdata = data[0].downcase.split(currfieldmark)
        skipflag = 0
        next unless subdata.size > 1

        subdata.each do |thisinput|
          if skipflag == 1
            # first, find the delimiter
            fielddata = thisinput.downcase.split(/>/)
            fields = fielddata[0].split(/ /)
            fieldname = ''
            fieldtype = ''
            fieldvalue = ''
            fieldid = ''
            fields.each do |thisfield|
              if thisfield.match(/^type=/)
                fieldtype = get_field_val(thisfield)
              end
              if (currfieldmark == '<select') && thisfield.match(/^class=/)
                fieldtype = get_field_val(thisfield)
              end
              if thisfield.match(/^name=/)
                fieldname = get_field_val(thisfield)
              end
              if thisfield.match(/^id=/)
                fieldid = get_field_val(thisfield)
              end
              next unless thisfield.match(/^value=/)

              # special case
              location = fielddata[0].index(thisfield)
              delta = fielddata[0].size - location
              remaining = fielddata[0][location, delta]
              tmp = remaining.strip.split(/=/)
              next unless tmp.size > 1

              delim = tmp[1][0, 1]
              tmp2 = tmp[1].split(delim)
              fieldvalue = tmp2[1]
            end
            if (fieldname == '') && (fieldid != '')
              fieldname = fieldid
            end
            if (fieldid == '') && (fieldname != '')
              fieldid = fieldname
            end
            print_status("      Field : #{fieldname}, type #{fieldtype}")
            if fieldid != ''
              formfields << {
                id: fieldid,
                name: fieldname,
                type: fieldtype,
                value: fieldvalue
              }
              formfieldcnt += 1
            end
          else
            skipflag += 1
          end
        end
      end
      print_status("      Nr of fields in form '#{formname}' : #{formfields.size}")
      # store in multidimensional array
      forms << {
        name: formname,
        id: formid,
        action: actionname,
        method: formmethod,
        fields: formfields
      }
      formidx += 1
      formcnt += 1
    end

    if !forms.empty?
      print_status('    Forms : ')
    end

    forms.each do |thisform|
      print_status("     - Name : #{thisform[:name]}, ID : #{thisform[:id]}, Action : #{thisform[:action]}, Method : #{thisform[:method]}")
    end

    return forms
  end

  def set_cookie(cookie)
    @get_data_headers['Cookie'] = cookie
    @send_data[:headers]['Cookie'] = cookie
  end

  def run
    init_fuzzdata
    init_vars

    print_status("Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']}")
    response = send_request_raw(
      {
        'uri' => normalize_uri(datastore['URL']),
        'version' => '1.1',
        'method' => 'GET',
        'headers' => @get_data_headers

      }, datastore['TIMEOUT']
    )
    if response.nil?
      print_error('No response')
      return
    end

    if datastore['HANDLECOOKIES']
      cookie = response.get_cookies
      set_cookie(cookie)
      print_status("Set cookie: #{cookie}")
      print_status("Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']} using cookies")

      response = send_request_raw(
        {
          'uri' => normalize_uri(datastore['URL']),
          'version' => '1.1',
          'method' => 'GET',
          'headers' => @get_data_headers
        }, datastore['TIMEOUT']
      )
    end
    if response.nil?
      print_error('No response')
      return
    end
    print_status("Code : #{response.code}")
    okcode = is_error_code(response.code)
    if !okcode
      print_error('Server replied with error code. Check URL or set CODE to another value, and try again.')
      return
    end
    if response.body
      formfound = response.body.downcase.index('<form')
      if formfound
        formdata = get_form_data(response.body)
        # fuzz !
        # for each form that needs to be fuzzed
        formdata.each do |thisform|
          next if thisform[:name].empty?

          if ((datastore['FORM'].strip == '') || (datastore['FORM'].upcase.strip == thisform[:name].upcase.strip)) && !thisform[:fields].empty?
            print_status("Fuzzing fields in form #{thisform[:name].upcase.strip}")
            # for each field in this form, fuzz one field at a time
            formfields = thisform[:fields]
            formfields.each do |thisfield|
              next unless thisfield[:name]

              next unless fuzz_this_field(thisfield[:name], thisfield[:type]) == 1

              print_status("    - Fuzzing field #{thisfield[:name]}")
              do_fuzz_field(thisform, thisfield[:name])
              init_fuzzdata
            end
            print_status("Done fuzzing fields in form #{thisform[:name].upcase.strip}")
          end
          # fuzz headers ?
          if datastore['FUZZHEADERS']
            print_status('Fuzzing header fields')
            do_fuzz_headers(thisform, response.headers)
          end
        end

      else
        print_error('No form found in response body')
        print_status(response.body)
        return
      end
    else
      print_error('No response data')
    end
  end
end
