rapid7/metasploit-framework

View on GitHub
modules/auxiliary/fuzzers/http/http_form_field.rb

Summary

Maintainability
F
1 wk
Test Coverage
##
# 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'],
        ]
      ))

    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 = @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.size > 0
      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.size > 0
      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]
        if thisfield[:value] != ""
          normaldata = thisfield[:value].strip + "&"
        end
      end
      datastr << thisfield[:name].downcase.strip + "=" + normaldata
    end
    if datastr.length > 0
      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 not 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 not 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
        if thisfield[:name]
          fieldstofuzz.each do | fuzzthis |
            if fuzzthis
              if (thisfield[:name].downcase.strip == fuzzthis.downcase.strip)
                dofuzzthis = true
              end
            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
      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 not 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 = @nrerrors + 1
      end
    else
      okcode = is_error_code(response.code)
      if okcode
          @nrerrors = 0
          incr_fuzzsize()
      end
      if not okcode and @nrerrors+1 >= datastore['STOPAFTER']
        print_status("      *!* Error response code #{response.code} | #{type} #{field} | fuzzdata length #{@fuzzsize}")
        return false
      else
        @nrerrors = @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 delimeter
    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(/<\/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=/) and not namefound
          formname = get_field_val(thisfield)
          namefound = true
        end
        if thisfield.match(/^id=/) and not idfound
          formid = get_field_val(thisfield)
          idfound = true
        end
        if thisfield.match(/^method=/)
          formmethod = get_field_val(thisfield)
        end
        if thisfield.match(/^action=/) and not actionfound
          actionname = get_field_val(thisfield)
          if (actionname.length < datastore['URL'].length) and (datastore['URL'].downcase.index(actionname.downcase).to_i() > -1)
            actionname = datastore['URL']
          end
          actionfound = true
        end
      end
      if datastore['ACTION'].length > 0
        actionname = datastore['ACTION']
        actionfound = true
      end

      if formname == "" and formid != ""
        formname = formid
      end
      if formid == "" and formname != ""
        formid = formname
      end
      if formid == "" and 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
        if (namefound or idfound) and actionfound
          # get fields in current form - data[0]
          subdata = data[0].downcase.split(currfieldmark)
          skipflag=0
          if subdata.size > 1
            subdata.each do | thisinput |
              if skipflag == 1
                # first, find the delimeter
                fielddata = thisinput.downcase.split(/>/)
                fields = fielddata[0].split(/ /)
                fieldname = ""
                fieldtype = ""
                fieldvalue = ""
                fieldmethod = "post"
                fieldid = ""
                fields.each do | thisfield |
                  if thisfield.match(/^type=/)
                    fieldtype = get_field_val(thisfield)
                  end
                  if currfieldmark == "<select" and 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
                  if thisfield.match(/^value=/)
                    # special case
                    location = fielddata[0].index(thisfield)
                    delta = fielddata[0].size - location
                    remaining = fielddata[0][location,delta]
                    tmp = remaining.strip.split(/\=/)
                    if tmp.size > 1
                      delim = tmp[1][0,1]
                      tmp2 = tmp[1].split(delim)
                      fieldvalue = tmp2[1]
                    end
                  end
                end
                if fieldname == "" and fieldid != ""
                  fieldname = fieldid
                end
                if fieldid == "" and 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
        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 = formidx + 1
      formcnt += 1
    end

    if forms.size > 0
      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 not 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 |
          if thisform[:name].length > 0
            if ((datastore['FORM'].strip == "") || (datastore['FORM'].upcase.strip == thisform[:name].upcase.strip)) && (thisform[:fields].size > 0)
              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 |
                if thisfield[:name]
                  if fuzz_this_field(thisfield[:name],thisfield[:type]) == 1
                    print_status("    - Fuzzing field #{thisfield[:name]}")
                    do_fuzz_field(thisform,thisfield[:name])
                    init_fuzzdata()
                  end
                end
              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
        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