Path: blob/master/modules/auxiliary/fuzzers/http/http_form_field.rb
19535 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::HttpClient78def initialize(info = {})9super(10update_info(11info,12'Name' => 'HTTP Form Field Fuzzer',13'Description' => %q{14This module will grab all fields from a form,15and launch a series of POST actions, fuzzing the contents16of the form fields. You can optionally fuzz headers too17(option is enabled by default)18},19'Author' => [20'corelanc0d3r',21'Paulino Calderon <calderon[at]websec.mx>' # Added cookie handling22],23'License' => MSF_LICENSE,24'References' => [25['URL', 'http://www.corelan.be:8800/index.php/2010/11/12/metasploit-module-http-form-field-fuzzer'],26],27'Notes' => {28'Stability' => [CRASH_SERVICE_DOWN],29'SideEffects' => [],30'Reliability' => []31}32)33)3435register_options(36[37OptString.new('URL', [ false, 'The URL that contains the form', '/']),38OptString.new('FORM', [ false, 'The name of the form to use. Leave empty to fuzz all forms', '' ]),39OptString.new('FIELDS', [ false, 'Name of the fields to fuzz. Leave empty to fuzz all fields', '' ]),40OptString.new('ACTION', [ false, 'Form action full URI. Leave empty to autodetect', '' ]),41OptInt.new('STARTSIZE', [ true, 'Fuzzing string startsize.', 1000]),42OptInt.new('ENDSIZE', [ true, 'Max Fuzzing string size.', 40000]),43OptInt.new('STEPSIZE', [ true, 'Increment fuzzing string each attempt.', 1000]),44OptInt.new('TIMEOUT', [ true, 'Number of seconds to wait for response on GET or POST', 15]),45OptInt.new('DELAY', [ true, 'Number of seconds to wait between 2 actions', 0]),46OptInt.new('STOPAFTER', [ false, 'Stop after x number of consecutive errors', 2]),47OptBool.new('CYCLIC', [ true, "Use Cyclic pattern instead of A's (fuzzing payload).", true]),48OptBool.new('FUZZHEADERS', [ true, 'Fuzz headers', true]),49OptString.new('HEADERFIELDS', [ false, 'Name of the headerfields to fuzz. Leave empty to fuzz all fields', '' ]),50OptString.new('TYPES', [ true, 'Field types to fuzz', 'text,password,inputtextbox']),51OptString.new('CODE', [ true, 'Response code(s) indicating OK', '200,301,302,303' ]),52OptBool.new('HANDLECOOKIES', [ true, 'Appends cookies with every request.', false])53]54)55end5657def init_vars58proto = 'http://'59if datastore['SSL']60proto = 'https://'61end6263@send_data = {64uri: '',65version: '1.1',66method: 'POST',67headers: {68'Content-Length' => 100,69'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',70'Accept-Language' => 'en-us,en;q=0.5',71'Accept-Encoding' => 'gzip,deflate',72'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',73'Keep-Alive' => '300',74'Connection' => 'keep-alive',75'Referer' => proto + datastore['RHOST'] + ':' + datastore['RPORT'].to_s,76'Content-Type' => 'application/x-www-form-urlencoded'77}78}79@get_data_headers = {80'Referer' => proto + datastore['RHOST'] + ':' + datastore['RPORT'].to_s81}82end8384def init_fuzzdata85@fuzzsize = datastore['STARTSIZE']86@endsize = datastore['ENDSIZE']87set_fuzz_payload88@nrerrors = 089end9091def incr_fuzzsize92@stepsize = datastore['STEPSIZE'].to_i93@fuzzsize += @stepsize94end9596def set_fuzz_payload97if datastore['CYCLIC']98@fuzzdata = Rex::Text.pattern_create(@fuzzsize)99else100@fuzzdata = 'A' * @fuzzsize101end102end103104def is_error_code(code)105okcode = false106checkcodes = datastore['CODE'].split(',')107checkcodes.each do |testcode|108testcode = testcode.upcase.gsub(' ', '')109if testcode == code.to_s.upcase.gsub(' ', '')110okcode = true111end112end113return okcode114end115116def fuzz_this_field(fieldname, fieldtype)117fuzzcommands = datastore['FIELDS'].split(',')118fuzzme = 0119if !fuzzcommands.empty?120fuzzcommands.each do |thiscmd|121thiscmd = thiscmd.strip122if ((fieldname.upcase == thiscmd.upcase) || (thiscmd == '')) && (fuzzme == 0)123fuzzme = 1124end125end126else127fuzztypes = datastore['TYPES'].split(',')128fuzztypes.each do |thistype|129if (fieldtype.upcase.strip == thistype.upcase.strip)130fuzzme = 1131end132end133end134if fuzzme == 1135set_fuzz_payload136end137return fuzzme138end139140def fuzz_this_headerfield(fieldname)141fuzzheaderfields = datastore['HEADERFIELDS'].split(',')142fuzzme = 0143if !fuzzheaderfields.empty?144fuzzheaderfields.each do |thisfield|145thisfield = thisfield.strip146if ((fieldname.upcase == thisfield.upcase) || (thisfield == '')) && (fuzzme == 0)147fuzzme = 1148end149end150else151fuzzme = 1152end153if fuzzme == 1154set_fuzz_payload155end156return fuzzme157end158159def do_fuzz_headers(form, headers)160headercnt = 0161datastr = ''162form[:fields].each do |thisfield|163normaldata = 'blah&'164if thisfield[:value] && thisfield[:value] != ('')165normaldata = thisfield[:value].strip + '&'166end167datastr << thisfield[:name].downcase.strip + '=' + normaldata168end169if !datastr.empty?170datastr = datastr[0, datastr.length - 1] + "\r\n"171else172datastr = "\r\n"173end174# first, check the original header fields and add some others - just for fun175myheaders = @send_data[:headers]176mysendheaders = @send_data[:headers].dup177# get or post ?178mysendheaders[:method] = form[:method].upcase179myheaders.each do |thisheader|180if !(headers[thisheader[0]])181# add header if needed182mysendheaders[thisheader[0]] = thisheader[1]183end184end185nrheaderstofuzz = mysendheaders.size186mysendheaders.each do |thisheader|187@fuzzheader = mysendheaders.dup188@nrerrors = 0189fuzzpacket = @send_data.dup190fuzzpacket[:method] = mysendheaders[:method]191headername = thisheader[0]192if fuzz_this_headerfield(headername.to_s.upcase) == 1193print_status(" - Fuzzing header '#{headername}' (#{headercnt + 1}/#{nrheaderstofuzz})")194init_fuzzdata195while @fuzzsize <= @endsize + 1196@fuzzheader[headername] = @fuzzdata197fuzzpacket[:headers] = @fuzzheader198response = send_fuzz(fuzzpacket, datastr)199if !process_response(response, headername, 'header')200@fuzzsize = @endsize + 2201end202if datastore['DELAY'] > 0203print_status(" (Sleeping for #{datastore['DELAY']} seconds...)")204select(nil, nil, nil, datastore['DELAY'])205end206incr_fuzzsize207end208else209print_status(" - Skipping header '#{headername}' (#{headercnt + 1}/#{nrheaderstofuzz})")210end211headercnt += 1212end213end214215def do_fuzz_field(form, field)216fieldstofuzz = field.downcase.strip.split(',')217@nrerrors = 0218while @fuzzsize <= @endsize + 1219allfields = form[:fields]220datastr = ''221normaldata = ''222allfields.each do |thisfield|223dofuzzthis = false224next unless thisfield[:name]225226fieldstofuzz.each do |fuzzthis|227next unless fuzzthis228229if (thisfield[:name].downcase.strip == fuzzthis.downcase.strip)230dofuzzthis = true231end232end233if thisfield[:value]234normaldata = thisfield[:value].strip235else236normaldata = ''237end238if dofuzzthis239datastr << thisfield[:name].downcase.strip + '=' + @fuzzdata + '&'240else241datastr << thisfield[:name].downcase.strip + '=' + normaldata + '&'242end243end244datastr = datastr[0, datastr.length - 1]245@send_data[:uri] = form[:action]246@send_data[:uri] = "/#{form[:action]}" if @send_data[:uri][0, 1] != '/'247248@send_data[:method] = form[:method].upcase249response = send_fuzz(@send_data, datastr)250if !process_response(response, field, 'field')251return252end253254if datastore['DELAY'] > 0255print_status(" (Sleeping for #{datastore['DELAY']} seconds...)")256select(nil, nil, nil, datastore['DELAY'])257end258end259end260261def process_response(response, field, type)262if response.nil?263print_error(" No response - #{@nrerrors + 1} / #{datastore['STOPAFTER']} - fuzzdata length : #{@fuzzsize}")264if @nrerrors + 1 >= datastore['STOPAFTER']265print_status(" *!* No response : #{type} #{field} | fuzzdata length : #{@fuzzsize}")266return false267else268@nrerrors += 1269end270else271okcode = is_error_code(response.code)272if okcode273@nrerrors = 0274incr_fuzzsize275end276if !okcode && (@nrerrors + 1 >= datastore['STOPAFTER'])277print_status(" *!* Error response code #{response.code} | #{type} #{field} | fuzzdata length #{@fuzzsize}")278return false279else280@nrerrors += 1281end282end283return true284end285286def send_fuzz(postdata, data)287header = postdata[:headers]288response = send_request_raw({289'uri' => postdata[:uri],290'version' => postdata[:version],291'method' => postdata[:method],292'headers' => header,293'data' => data294}, datastore['TIMEOUT'])295return response296end297298def get_field_val(input)299tmp = input.split(/=/)300# get delimiter301tmp2 = tmp[1].strip302delim = tmp2[0, 1]303if delim != "'" && delim != '"'304delim = ''305end306tmp3 = tmp[1].split(/>/)307tmp4 = tmp3[0].gsub(delim, '')308return tmp4309end310311def get_form_data(body)312print_status('Enumerating form data')313body = body.gsub("\r", '')314body = body.gsub("\n", '')315bodydata = body.downcase.split(/<form/)316# we need part after <form317totalforms = bodydata.size - 1318print_status(" Number of forms : #{totalforms}")319formcnt = 0320formidx = 1321forms = []322while formcnt < totalforms323fdata = bodydata[formidx]324print_status(" - Enumerating form ##{formcnt + 1}")325data = fdata.downcase.split(%r{</form>})326# first, get action and name327formdata = data[0].downcase.split(/>/)328subdata = formdata[0].downcase.split(/ /)329namefound = false330actionfound = false331idfound = false332actionname = ''333formname = ''334formid = ''335formmethod = 'post'336subdata.each do |thisfield|337if thisfield.match(/^name=/) && !namefound338formname = get_field_val(thisfield)339namefound = true340end341if thisfield.match(/^id=/) && !idfound342formid = get_field_val(thisfield)343idfound = true344end345if thisfield.match(/^method=/)346formmethod = get_field_val(thisfield)347end348next unless thisfield.match(/^action=/) && !actionfound349350actionname = get_field_val(thisfield)351if (actionname.length < datastore['URL'].length) && (datastore['URL'].downcase.index(actionname.downcase).to_i > -1)352actionname = datastore['URL']353end354actionfound = true355end356if !datastore['ACTION'].empty?357actionname = datastore['ACTION']358actionfound = true359end360361if (formname == '') && (formid != '')362formname = formid363end364if (formid == '') && (formname != '')365formid = formname366end367if (formid == '') && (formname == '')368formid = 'noname_' + (formcnt + 1).to_s369formname = formid370end371idfound = true372namefound = true373374formfields = []375# input boxes376fieldtypemarks = [ '<input', '<select' ]377fieldtypemarks.each do |currfieldmark|378formfieldcnt = 0379next unless (namefound || idfound) && actionfound380381# get fields in current form - data[0]382subdata = data[0].downcase.split(currfieldmark)383skipflag = 0384next unless subdata.size > 1385386subdata.each do |thisinput|387if skipflag == 1388# first, find the delimiter389fielddata = thisinput.downcase.split(/>/)390fields = fielddata[0].split(/ /)391fieldname = ''392fieldtype = ''393fieldvalue = ''394fieldid = ''395fields.each do |thisfield|396if thisfield.match(/^type=/)397fieldtype = get_field_val(thisfield)398end399if (currfieldmark == '<select') && thisfield.match(/^class=/)400fieldtype = get_field_val(thisfield)401end402if thisfield.match(/^name=/)403fieldname = get_field_val(thisfield)404end405if thisfield.match(/^id=/)406fieldid = get_field_val(thisfield)407end408next unless thisfield.match(/^value=/)409410# special case411location = fielddata[0].index(thisfield)412delta = fielddata[0].size - location413remaining = fielddata[0][location, delta]414tmp = remaining.strip.split(/=/)415next unless tmp.size > 1416417delim = tmp[1][0, 1]418tmp2 = tmp[1].split(delim)419fieldvalue = tmp2[1]420end421if (fieldname == '') && (fieldid != '')422fieldname = fieldid423end424if (fieldid == '') && (fieldname != '')425fieldid = fieldname426end427print_status(" Field : #{fieldname}, type #{fieldtype}")428if fieldid != ''429formfields << {430id: fieldid,431name: fieldname,432type: fieldtype,433value: fieldvalue434}435formfieldcnt += 1436end437else438skipflag += 1439end440end441end442print_status(" Nr of fields in form '#{formname}' : #{formfields.size}")443# store in multidimensional array444forms << {445name: formname,446id: formid,447action: actionname,448method: formmethod,449fields: formfields450}451formidx += 1452formcnt += 1453end454455if !forms.empty?456print_status(' Forms : ')457end458459forms.each do |thisform|460print_status(" - Name : #{thisform[:name]}, ID : #{thisform[:id]}, Action : #{thisform[:action]}, Method : #{thisform[:method]}")461end462463return forms464end465466def set_cookie(cookie)467@get_data_headers['Cookie'] = cookie468@send_data[:headers]['Cookie'] = cookie469end470471def run472init_fuzzdata473init_vars474475print_status("Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']}")476response = send_request_raw(477{478'uri' => normalize_uri(datastore['URL']),479'version' => '1.1',480'method' => 'GET',481'headers' => @get_data_headers482483}, datastore['TIMEOUT']484)485if response.nil?486print_error('No response')487return488end489490if datastore['HANDLECOOKIES']491cookie = response.get_cookies492set_cookie(cookie)493print_status("Set cookie: #{cookie}")494print_status("Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']} using cookies")495496response = send_request_raw(497{498'uri' => normalize_uri(datastore['URL']),499'version' => '1.1',500'method' => 'GET',501'headers' => @get_data_headers502}, datastore['TIMEOUT']503)504end505if response.nil?506print_error('No response')507return508end509print_status("Code : #{response.code}")510okcode = is_error_code(response.code)511if !okcode512print_error('Server replied with error code. Check URL or set CODE to another value, and try again.')513return514end515if response.body516formfound = response.body.downcase.index('<form')517if formfound518formdata = get_form_data(response.body)519# fuzz !520# for each form that needs to be fuzzed521formdata.each do |thisform|522next if thisform[:name].empty?523524if ((datastore['FORM'].strip == '') || (datastore['FORM'].upcase.strip == thisform[:name].upcase.strip)) && !thisform[:fields].empty?525print_status("Fuzzing fields in form #{thisform[:name].upcase.strip}")526# for each field in this form, fuzz one field at a time527formfields = thisform[:fields]528formfields.each do |thisfield|529next unless thisfield[:name]530531next unless fuzz_this_field(thisfield[:name], thisfield[:type]) == 1532533print_status(" - Fuzzing field #{thisfield[:name]}")534do_fuzz_field(thisform, thisfield[:name])535init_fuzzdata536end537print_status("Done fuzzing fields in form #{thisform[:name].upcase.strip}")538end539# fuzz headers ?540if datastore['FUZZHEADERS']541print_status('Fuzzing header fields')542do_fuzz_headers(thisform, response.headers)543end544end545546else547print_error('No form found in response body')548print_status(response.body)549return550end551else552print_error('No response data')553end554end555end556557558