diff options
Diffstat (limited to 'scripts/http-form-fuzzer.nse')
-rw-r--r-- | scripts/http-form-fuzzer.nse | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/scripts/http-form-fuzzer.nse b/scripts/http-form-fuzzer.nse new file mode 100644 index 0000000..6c1e3cc --- /dev/null +++ b/scripts/http-form-fuzzer.nse @@ -0,0 +1,204 @@ +description = [[ +Performs a simple form fuzzing against forms found on websites. +Tries strings and numbers of increasing length and attempts to +determine if the fuzzing was successful. +]] + +--- +-- @usage +-- nmap --script http-form-fuzzer --script-args 'http-form-fuzzer.targets={1={path=/},2={path=/register.html}}' -p 80 <host> +-- +-- This script attempts to fuzz fields in forms it detects (it fuzzes one field at a time). +-- In each iteration it first tries to fuzz a field with a string, then with a number. +-- In the output, actions and paths for which errors were observed are listed, along with +-- names of fields that were being fuzzed during error occurrence. Length and type +-- (string/integer) of the input that caused the error are also provided. +-- We consider an error to be either: a response with status 500 or with an empty body, +-- a response that contains "server error" or "sql error" strings. ATM anything other than +-- that is considered not to be an 'error'. +-- TODO: develop more sophisticated techniques that will let us determine if the fuzzing was +-- successful (i.e. we got an 'error'). Ideally, an algorithm that will tell us a percentage +-- difference between responses should be implemented. +-- +-- @output +-- PORT STATE SERVICE REASON +-- 80/tcp open http syn-ack +-- | http-form-fuzzer: +-- | Path: /register.html Action: /validate.php +-- | age +-- | integer lengths that caused errors: +-- | 10000, 10001 +-- | name +-- | string lengths that caused errors: +-- | 40000 +-- | Path: /form.html Action: /check_form.php +-- | fieldfoo +-- | integer lengths that caused errors: +-- |_ 1, 2 +-- +-- @args http-form-fuzzer.targets a table with the targets of fuzzing, for example +-- {{path = /index.html, minlength = 40002}, {path = /foo.html, maxlength = 10000}}. +-- The path parameter is required, if minlength or maxlength is not specified, +-- then the values of http-form-fuzzer.minlength or http-form-fuzzer.maxlength will be used. +-- Defaults to {{path="/"}} +-- @args http-form-fuzzer.minlength the minimum length of a string that will be used for fuzzing, +-- defaults to 300000 +-- @args http-form-fuzzer.maxlength the maximum length of a string that will be used for fuzzing, +-- defaults to 310000 +-- + +author = {"Piotr Olma", "Gioacchino Mazzurco"} +license = "Same as Nmap--See https://nmap.org/book/man-legal.html" +categories = {"fuzzer", "intrusive"} + +local shortport = require 'shortport' +local http = require 'http' +local httpspider = require 'httpspider' +local stdnse = require 'stdnse' +local string = require 'string' +local table = require 'table' +local url = require 'url' +local rand = require 'rand' + +-- check if the response we got indicates that fuzzing was successful +local function check_response(response) + if not(response.body) or response.status==500 then + return true + end + if response.body:find("[Ss][Ee][Rr][Vv][Ee][Rr]%s*[Ee][Rr][Rr][Oo][Rr]") or response.body:find("[Ss][Qq][Ll]%s*[Ee][Rr][Rr][Oo][Rr]") then + return true + end + return false +end + +-- check from response if request was too big +local function request_too_big(response) + return response.status==413 or response.status==414 +end + +-- checks if a field is of type we want to fuzz +local function fuzzable(field_type) + return field_type=="text" or field_type=="radio" or field_type=="checkbox" or field_type=="textarea" +end + +-- generates postdata with value of "sampleString" for every field (that is fuzzable()) of a form +local function generate_safe_postdata(form) + local postdata = {} + for _,field in ipairs(form["fields"]) do + if fuzzable(field["type"]) then + postdata[field["name"]] = "sampleString" + end + end + return postdata +end + +-- generate a charset of characters with ascii codes from 33 to 126 +-- you can use http://www.asciitable.com/ to see which characters those actually are +local charset = rand.charset(33,126) +local charset_number = rand.charset(49,57) -- ascii 49 -> 1; 57 -> 9 + +local function fuzz_form(form, minlen, maxlen, host, port, path) + local affected_fields = {} + local postdata = generate_safe_postdata(form) + local action_absolute = httpspider.LinkExtractor.isAbsolute(form["action"]) + + -- determine the path where the form needs to be submitted + local form_submission_path + if action_absolute then + form_submission_path = form["action"] + else + local path_cropped = string.match(path, "(.*/).*") + path_cropped = path_cropped and path_cropped or "" + form_submission_path = path_cropped..form["action"] + end + + -- determine should the form be sent by post or get + local sending_function + if form["method"]=="post" then + sending_function = function(data) return http.post(host, port, form_submission_path, nil, nil, data) end + else + sending_function = function(data) return http.get(host, port, form_submission_path.."?"..url.build_query(data), {no_cache=true, bypass_cache=true}) end + end + + local function fuzz_field(field) + local affected_string = {} + local affected_int = {} + + for i=minlen,maxlen do -- maybe a better idea would be to increment the string's length by more then 1 in each step + local response_string + local response_number + + --first try to fuzz with a string + postdata[field["name"]] = rand.random_string(i, charset) + response_string = sending_function(postdata) + --then with a number + postdata[field["name"]] = rand.random_string(i, charset_number) + response_number = sending_function(postdata) + + if check_response(response_string) then + affected_string[#affected_string+1]=i + elseif request_too_big(response_string) then + maxlen = i-1 + break + end + + if check_response(response_number) then + affected_int[#affected_int+1]=i + elseif request_too_big(response_number) then + maxlen = i-1 + break + end + end + postdata[field["name"]] = "sampleString" + return affected_string, affected_int + end + + for _,field in ipairs(form["fields"]) do + if fuzzable(field["type"]) then + local affected_string, affected_int = fuzz_field(field, minlen, maxlen, postdata, sending_function) + if #affected_string > 0 or #affected_int > 0 then + local affected_next_index = #affected_fields+1 + affected_fields[affected_next_index] = {name = field["name"]} + if #affected_string>0 then + table.insert(affected_fields[affected_next_index], {name="string lengths that caused errors:", table.concat(affected_string, ", ")}) + end + if #affected_int>0 then + table.insert(affected_fields[affected_next_index], {name="integer lengths that caused errors:", table.concat(affected_int, ", ")}) + end + end + end + end + return affected_fields +end + +portrule = shortport.http + +function action(host, port) + local targets = stdnse.get_script_args('http-form-fuzzer.targets') or {{path="/"}} + local return_table = {} + local minlen = stdnse.get_script_args("http-form-fuzzer.minlength") or 300000 + local maxlen = stdnse.get_script_args("http-form-fuzzer.maxlength") or 310000 + + for _,target in pairs(targets) do + stdnse.debug2("testing path: "..target["path"]) + local path = target["path"] + if path then + local response = http.get( host, port, path ) + local all_forms = http.grab_forms(response.body) + minlen = target["minlength"] or minlen + maxlen = target["maxlength"] or maxlen + for _,form_plain in ipairs(all_forms) do + local form = http.parse_form(form_plain) + if form and form.action then + local affected_fields = fuzz_form(form, minlen, maxlen, host, port, path) + if #affected_fields > 0 then + affected_fields["name"] = "Path: "..path.." Action: "..form["action"] + table.insert(return_table, affected_fields) + end + end + end + end + end + return stdnse.format_output(true, return_table) +end + |