From cf6da2c5941dd54335f16fc86c8b2a6e1f10225a Mon Sep 17 00:00:00 2001 From: Joshua Drake Date: Fri, 12 Nov 2010 20:59:36 +0000 Subject: [PATCH] add http form fuzzer from corelanc0d3r git-svn-id: file:///home/svn/framework3/trunk@11013 4d416f70-5f16-0410-b530-b9f4589650da --- .../auxiliary/fuzzers/http/http_form_field.rb | 540 ++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 modules/auxiliary/fuzzers/http/http_form_field.rb diff --git a/modules/auxiliary/fuzzers/http/http_form_field.rb b/modules/auxiliary/fuzzers/http/http_form_field.rb new file mode 100644 index 0000000000..600ce0df89 --- /dev/null +++ b/modules/auxiliary/fuzzers/http/http_form_field.rb @@ -0,0 +1,540 @@ +## +# $Id$ +## + +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# Framework web site for more information on licensing and terms of use. +# http://metasploit.com/framework/ +## + +## +# +# Tip : run "show advanced" for more options +# +## + +require 'msf/core' + +class Metasploit3 < 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' ], + 'License' => MSF_LICENSE, + 'Version' => '$Revision$', + '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" ] ) + ], self.class ) + end + + def init_vars + proto = "http://" + if datastore['SSL'] + proto = "https://" + end + useragent="Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.15) Gecko/2009102814 Ubuntu/8.10 (intrepid) Firefox/3.0.15" + if datastore['UserAgent'] != nil + if datastore['UserAgent'].length > 0 + useragent = datastore['UserAgent'] + end + end + host = datastore['RHOST'] + if datastore['VHOST'] + if datastore['VHOST'].length > 0 + host = datastore['VHOST'] + end + end + @send_data= { + :uri => '', + :version => '1.1', + :method => 'POST', + :headers => { + 'Content-Length' => 100, + 'Host' => host, + 'User-Agent' => useragent, + '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'], + 'Content-Type' => 'application/x-www-form-urlencoded' + } + } + 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[: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(/
/) + #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 = [ ' 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 == " 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 run + init_fuzzdata() + init_vars() + + print_status("[+] Grabbing webpage #{datastore['URL']} from #{datastore['RHOST']}") + response = send_request_raw( + { + 'uri' => datastore['URL'], + 'version' => '1.1', + 'method' => 'GET' + }, datastore['TIMEOUT']) + 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(" 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'] == true + 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