summaryrefslogtreecommitdiffstats
path: root/scripts/http-form-fuzzer.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/http-form-fuzzer.nse')
-rw-r--r--scripts/http-form-fuzzer.nse204
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
+