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 +