metasploit-framework/modules/auxiliary/fuzzers/http/http_form_field.rb

559 lines
18 KiB
Ruby

##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
##
#
# Tip : run "show advanced" for more options
#
##
require 'msf/core'
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])
], self.class )
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