diff --git a/scripts/http-methods.nse b/scripts/http-methods.nse
index 1ed1c8481..be3dc9404 100644
--- a/scripts/http-methods.nse
+++ b/scripts/http-methods.nse
@@ -3,12 +3,15 @@ local nmap = require "nmap"
local shortport = require "shortport"
local stdnse = require "stdnse"
local string = require "string"
+local table = require "table"
description = [[
Finds out what options are supported by an HTTP server by sending an
-OPTIONS request. Lists potentially risky methods. Optionally tests each
-method individually to see if they are subject to e.g. IP address
-restrictions.
+OPTIONS request. Lists potentially risky methods. It tests those methods
+not mentioned in the OPTIONS headers individually and sees if they are
+implemented. Any output other than 501/405 suggests that the method is
+if not in the range 400 to 600. If the response falls under that range then
+it is compared to the response from a randomly generated method.
In this script, "potentially risky" methods are anything except GET,
HEAD, POST, and OPTIONS. If the script reports potentially risky
@@ -24,105 +27,46 @@ only the potentially risky methods are shown.
]]
---
--- @args http-methods.url-path The path to request. Defaults to
+-- @args http.url-path The path to request. Defaults to
-- /.
--- @args http-methods.retest If defined, do a request using each method
+-- @args http.retest If defined, do a request using each method
-- individually and show the response code. Use of this argument can
-- make this script unsafe; for example DELETE / is
--- possible.
+-- possible. All methods received through options are tested with generic
+-- requests. Saved status lines are shown for rest.
+-- @args http.test-all If set true tries all the unsafe methods as well.
--
-- @output
--- 80/tcp open http
--- | http-methods: GET HEAD POST OPTIONS TRACE
--- | Potentially risky methods: TRACE
--- | See http://nmap.org/nsedoc/scripts/http-methods.html
--- | GET / -> HTTP/1.1 200 OK
--- | HEAD / -> HTTP/1.1 200 OK
--- | POST / -> HTTP/1.1 200 OK
--- | OPTIONS / -> HTTP/1.1 200 OK
--- |_TRACE / -> HTTP/1.1 200 OK
+-- PORT STATE SERVICE REASON
+-- 80/tcp open http syn-ack
+-- | http-methods:
+-- |_ Supported Methods: GET HEAD POST OPTIONS
--
-- @usage
--- nmap --script=http-methods.nse --script-args http-methods.retest=1
--- nmap --script=http-methods.nse --script-args http-methods.url-path=/website
+-- nmap --script http-methods --script-args
+-- nmap --script http-methods --script-args http.url-path='/website'
+--
+-- @xmloutput
+-- GET HEAD POST OPTIONS
-author = "Bernd Stroessenreuther "
+
+author = {"Bernd Stroessenreuther ", "Gyanendra Mishra"}
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"default", "safe"}
-
--- We don't report these methods except with verbosity.
-local UNINTERESTING_METHODS = {
- "GET", "HEAD", "POST", "OPTIONS"
-}
-
-local filter_out, merge_headers
-
-portrule = shortport.http
-
-action = function(host, port)
- local url_path, retest_http_methods
- local response, methods, options_status_line, output
-
- -- default values for script-args
- url_path = stdnse.get_script_args("http-methods.url-path") or "/"
- retest_http_methods = stdnse.get_script_args("http-methods.retest") ~= nil
-
- response = http.generic_request(host, port, "OPTIONS", url_path)
- if not response.status then
- stdnse.debug1("OPTIONS %s failed.", url_path)
- return
+local function check_allowed(random_resp, response)
+ if response.status == 405 or response.status == 501 then
+ return false
end
- -- Cache in case retest is requested.
- options_status_line = response["status-line"]
- stdnse.debug1("HTTP Status for OPTIONS is " .. response.status)
-
- if not (response.header["allow"] or response.header["public"]) then
- return string.format("No Allow or Public header in OPTIONS response (status code %d)", response.status)
+ if response.status < 600 and response.status >= 400 and response.status == random_resp.status then
+ return false
end
-
- -- The Public header is defined in RFC 2068, but was removed in its
- -- successor RFC 2616. It is implemented by at least IIS 6.0.
- methods = merge_headers(response.header, {"Allow", "Public"})
-
- output = {}
-
- if nmap.verbosity() > 0 then
- output[#output + 1] = stdnse.strjoin(" ", methods)
- end
-
- local interesting = filter_out(methods, UNINTERESTING_METHODS)
- if #interesting > 0 then
- output[#output + 1] = "Potentially risky methods: " .. stdnse.strjoin(" ", interesting)
- output[#output + 1] = "See http://nmap.org/nsedoc/scripts/http-methods.html"
- end
-
- -- retest http methods if requested
- if retest_http_methods then
- local _
- for _, method in ipairs(methods) do
- local str
- if method == "OPTIONS" then
- -- Use the saved value.
- str = options_status_line
- else
- response = http.generic_request(host, port, method, url_path)
- if not response.status then
- str = "Error getting response"
- else
- str = response["status-line"]
- end
- end
- output[#output + 1] = string.format("%s %s -> %s", method, url_path, str)
- end
- end
-
- return #output > 0 and stdnse.strjoin("\n", output) or nil
+ return true
end
-function filter_out(t, filter)
+local function filter_out(t, filter)
local result = {}
local _, e, f
for _, e in ipairs(t) do
@@ -133,8 +77,9 @@ function filter_out(t, filter)
return result
end
+
-- Split header field contents on commas and return a table without duplicates.
-function merge_headers(headers, names)
+local function merge_headers(headers, names)
local seen = {}
local result = {}
@@ -152,3 +97,113 @@ function merge_headers(headers, names)
return result
end
+
+-- We don't report these methods except with verbosity.
+local SAFE_METHODS = {
+ "GET", "HEAD", "POST", "OPTIONS"
+}
+
+local UNSAFE_METHODS = {
+"DELETE", "PUT", "CONNECT", "TRACE"
+}
+
+portrule = shortport.http
+
+action = function(host, port)
+
+ local path, retest_http_methods, test_all_unsafe
+ local response, methods, options_status_line
+ local output = stdnse.output_table()
+ local options_status = true
+
+ -- default values for script-args
+ path = stdnse.get_script_args(SCRIPT_NAME .. ".url-path") or '/'
+ retest_http_methods = stdnse.get_script_args(SCRIPT_NAME .. ".retest") or false
+ test_all_unsafe = stdnse.get_script_args(SCRIPT_NAME .. ".test-all") or false
+
+ response = http.generic_request(host, port, "OPTIONS", path)
+ if not response.status then
+ options_status = false
+ stdnse.debug1("OPTIONS %s failed.", path)
+ end
+ -- Cache in case retest is requested.
+ if options_status then
+ options_status_line = response["status-line"]
+ stdnse.debug1("HTTP Status for OPTIONS is " .. response.status)
+ if not(response.header["allow"] or response.header["public"]) then
+ stdnse.debug1("No Allow or Public header in OPTIONS response (status code %d)", response.status)
+ end
+ end
+
+ -- The Public header is defined in RFC 2068, but was removed in its
+ -- successor RFC 2616. It is implemented by at least IIS 6.0.
+ methods = merge_headers(response.header, {"Allow", "Public"})
+
+ local to_test = {}
+ local status_lines = {}
+
+ for _, method in pairs(SAFE_METHODS) do
+ if not stdnse.contains(methods, method) then
+ table.insert(to_test, method)
+ end
+ end
+
+ if test_all_unsafe then
+ for _, method in pairs(UNSAFE_METHODS) do
+ if not stdnse.contains(methods, method) then
+ table.insert(to_test, method)
+ end
+ end
+ end
+
+ local random_resp = http.generic_request(host, port, stdnse.generate_random_string(4), path)
+ stdnse.debug1("Response Code to Random Method is %d", random_resp.status or nil)
+ for _, method in pairs(to_test) do
+ response = http.generic_request(host, port, method, path)
+ if response.status and check_allowed(random_resp, response) then
+ stdnse.debug2("Method %s not in OPTIONS found to exist. STATUS %d", method, response.status)
+ table.insert(methods, method)
+ status_lines[method] = response['status-line']
+ end
+ end
+
+ if nmap.verbosity() > 0 and #methods > 0 then
+ output["Supported Methods"] = stdnse.strjoin(" ", methods)
+ end
+
+ local interesting = filter_out(methods, SAFE_METHODS)
+ if #interesting > 0 then
+ output["Potentially risky methods"] = stdnse.strjoin(" ", interesting)
+ end
+
+ if path ~= '/' then
+ output["Path tested"] = path
+ end
+
+ -- retest http methods if requested
+ if retest_http_methods then
+ output["Status Lines"] = {}
+ for _, method in ipairs(methods) do
+ local str
+ if method == "OPTIONS" then
+ -- Use the saved value.
+ str = options_status_line
+ elseif stdnse.contains(to_test, method) then
+ -- use the value saved earlier.
+ str = status_lines[method]
+ -- this case arises when methods in the Public or Allow headers are retested.
+ else
+ response = http.generic_request(host, port, method, path)
+ if not response.status then
+ str = "Error getting response"
+ else
+ str = response["status-line"]
+ end
+ end
+ str = str:gsub('\r?\n?', "")
+ output["Status Lines"][method] = str
+ end
+ end
+ if #output > 0 then return output else return nil end
+end
+