NSE LIB

Back to library
Official intrusive Fuzzer

http-form-fuzzer

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.

Ports

Any

Protocols

n/a

Attribution

Nmap Project

Usage

Copy the command and adjust the target or script arguments as needed.

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.
Script Source Toggle

The full script source is stored with this entry and is hidden by default to keep the page easier to scan.

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

Overview

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.