mirror of
https://github.com/nmap/nmap.git
synced 2025-12-06 04:31:29 +00:00
Merge from /nmap-exp/david/nmap-http-brute. This adds Basic
authentication support for http requests.
This commit is contained in:
156
nselib/http.lua
156
nselib/http.lua
@@ -31,6 +31,7 @@ local MAX_CACHE_SIZE = "http-max-cache-size";
|
||||
local coroutine = require "coroutine";
|
||||
local table = require "table";
|
||||
|
||||
local base64 = require "base64";
|
||||
local nmap = require "nmap";
|
||||
local url = require "url";
|
||||
local stdnse = require "stdnse";
|
||||
@@ -823,12 +824,35 @@ local function lookup_cache (method, host, port, path, options)
|
||||
end
|
||||
end
|
||||
|
||||
local function response_is_cacheable(response)
|
||||
-- 206 Partial Content. RFC 2616, 1.34: "...a cache that does not support the
|
||||
-- Range and Content-Range headers MUST NOT cache 206 (Partial Content)
|
||||
-- responses."
|
||||
if response.status == 206 then
|
||||
return false
|
||||
end
|
||||
|
||||
-- RFC 2616, 13.4. "A response received with any [status code other than 200,
|
||||
-- 203, 206, 300, 301 or 410] (e.g. status codes 302 and 307) MUST NOT be
|
||||
-- returned in a reply to a subsequent request unless there are cache-control
|
||||
-- directives or another header(s) that explicitly allow it."
|
||||
-- We violate the standard here and allow these other codes to be cached,
|
||||
-- with the exceptions listed below.
|
||||
|
||||
-- 401 Unauthorized. Caching this would prevent us from retrieving it later
|
||||
-- with the correct credentials.
|
||||
if response.status == 401 then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function insert_cache (state, response)
|
||||
local key = assert(state.key);
|
||||
local mutex = assert(state.mutex);
|
||||
|
||||
if response == nil or state.no_cache or
|
||||
response.status == 206 then -- ignore partial content response
|
||||
if response == nil or state.no_cache or not response_is_cacheable(response) then
|
||||
cache[key] = state.old_record;
|
||||
else
|
||||
local record = {
|
||||
@@ -870,6 +894,7 @@ end
|
||||
-- * <code>header</code>: A table containing additional headers to be used for the request.
|
||||
-- * <code>content</code>: The content of the message (content-length will be added -- set header['Content-Length'] to override)
|
||||
-- * <code>cookies</code>: A table of cookies in the form returned by <code>parse_set_cookie</code>.
|
||||
-- * <code>auth</code>: A table containing the keys <code>username</code> and <code>password</code>.
|
||||
-- @return A request string.
|
||||
-- @see generic_request
|
||||
local build_request = function(host, port, method, path, options)
|
||||
@@ -892,6 +917,13 @@ local build_request = function(host, port, method, path, options)
|
||||
mod_options.header["Cookie"] = cookies
|
||||
end
|
||||
end
|
||||
-- Only Basic authentication is supported.
|
||||
if options.auth then
|
||||
local username = options.auth.username
|
||||
local password = options.auth.password
|
||||
local credentials = "Basic " .. base64.enc(username .. ":" .. password)
|
||||
mod_options.header["Authorization"] = credentials
|
||||
end
|
||||
|
||||
-- Add any other options into the local copy.
|
||||
table_augment(mod_options, options)
|
||||
@@ -920,6 +952,7 @@ end
|
||||
-- * <code>header</code>: A table containing additional headers to be used for the request.
|
||||
-- * <code>content</code>: The content of the message (content-length will be added -- set header['Content-Length'] to override)
|
||||
-- * <code>cookies</code>: A table of cookies in the form returned by <code>parse_set_cookie</code>.
|
||||
-- * <code>auth</code>: A table containing the keys <code>username</code> and <code>password</code>.
|
||||
-- @return A table as described in the module description.
|
||||
-- @see request
|
||||
generic_request = function(host, port, method, path, options)
|
||||
@@ -937,6 +970,7 @@ end
|
||||
-- * <code>header</code>: A table containing additional headers to be used for the request.
|
||||
-- * <code>content</code>: The content of the message (content-length will be added -- set header['Content-Length'] to override)
|
||||
-- * <code>cookies</code>: A table of cookies in the form returned by <code>parse_set_cookie</code>.
|
||||
-- * <code>auth</code>: A table containing the keys <code>username</code> and <code>password</code>.
|
||||
-- @return A table as described in the module description.
|
||||
-- @see generic_request
|
||||
request = function(host, port, data, options)
|
||||
@@ -1254,6 +1288,69 @@ pipeline = function(host, port, allReqs)
|
||||
end
|
||||
|
||||
|
||||
-- Parsing of specific headers.
|
||||
|
||||
local skip_space = function(s, pos)
|
||||
local _
|
||||
|
||||
_, pos = string.find(s, "^[ \t]*", pos)
|
||||
|
||||
return pos + 1
|
||||
end
|
||||
|
||||
local read_token = function(s, pos)
|
||||
local _, token
|
||||
|
||||
pos = skip_space(s, pos)
|
||||
_, pos, token = string.find(s, "^([^%z\001-\031()<>@,;:\\\"/?={} \t%[%]\127-\255]+)", pos)
|
||||
|
||||
if token then
|
||||
return pos + 1, token
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local read_quoted_string = function(s, pos)
|
||||
local chars = {}
|
||||
|
||||
if string.sub(s, pos, pos) ~= "\"" then
|
||||
return nil
|
||||
end
|
||||
pos = pos + 1
|
||||
pos = skip_space(s, pos)
|
||||
while pos <= string.len(s) and string.sub(s, pos, pos) ~= "\"" do
|
||||
local c
|
||||
|
||||
c = string.sub(s, pos, pos)
|
||||
if c == "\\" then
|
||||
if pos < string.len(s) then
|
||||
pos = pos + 1
|
||||
c = string.sub(s, pos, pos)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
chars[#chars + 1] = c
|
||||
pos = pos + 1
|
||||
end
|
||||
if pos > string.len(s) or string.sub(s, pos, pos) ~= "\"" then
|
||||
return nil
|
||||
end
|
||||
|
||||
return pos + 1, table.concat(chars)
|
||||
end
|
||||
|
||||
local read_token_or_quoted_string = function(s, pos)
|
||||
pos = skip_space(s, pos)
|
||||
if string.sub(s, pos, pos) == "\"" then
|
||||
return read_quoted_string(s, pos)
|
||||
else
|
||||
return read_token(s, pos)
|
||||
end
|
||||
end
|
||||
|
||||
local MONTH_MAP = {
|
||||
Jan = 1, Feb = 2, Mar = 3, Apr = 4, May = 5, Jun = 6,
|
||||
Jul = 7, Aug = 8, Sep = 9, Oct = 10, Nov = 11, Dec = 12
|
||||
@@ -1327,6 +1424,61 @@ get_default_timeout = function( nmap_timing )
|
||||
return timeout
|
||||
end
|
||||
|
||||
local read_auth_challenge = function(s, pos)
|
||||
local _, pos, scheme, namevals
|
||||
|
||||
pos, scheme = read_token(s, pos)
|
||||
if not scheme then
|
||||
return nil
|
||||
end
|
||||
|
||||
namevals = {}
|
||||
pos = skip_space(s, pos)
|
||||
while pos < string.len(s) do
|
||||
local name, val
|
||||
|
||||
pos, name = read_token(s, pos)
|
||||
pos = skip_space(s, pos)
|
||||
if string.sub(s, pos, pos) ~= "=" then
|
||||
break
|
||||
end
|
||||
pos = pos + 1
|
||||
pos, val = read_token_or_quoted_string(s, pos)
|
||||
if namevals[name] then
|
||||
return nil
|
||||
end
|
||||
namevals[name] = val
|
||||
pos = skip_space(s, pos)
|
||||
if string.sub(s, pos, pos) == "," then
|
||||
pos = skip_space(s, pos + 1)
|
||||
if pos > string.len(s) then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return pos, { scheme = scheme, namevals = namevals }
|
||||
end
|
||||
|
||||
parse_www_authenticate = function(s)
|
||||
local challenges = {}
|
||||
local pos
|
||||
|
||||
pos = 1
|
||||
while pos <= string.len(s) do
|
||||
local challenge
|
||||
|
||||
pos, challenge = read_auth_challenge(s, pos)
|
||||
if not challenge then
|
||||
return nil
|
||||
end
|
||||
challenges[#challenges + 1] = challenge
|
||||
end
|
||||
|
||||
return challenges
|
||||
end
|
||||
|
||||
|
||||
--- Take the data returned from a HTTP request and return the status string.
|
||||
-- Useful for <code>print_debug</code> messages and even for advanced output.
|
||||
--
|
||||
|
||||
@@ -5,10 +5,11 @@ authentication.
|
||||
|
||||
---
|
||||
-- @output
|
||||
-- 80/tcp open http
|
||||
-- | http-auth: HTTP Service requires authentication
|
||||
-- | Auth type: Basic, realm = Password Required
|
||||
-- |_ HTTP server may accept admin:admin combination for Basic authentication
|
||||
-- PORT STATE SERVICE REASON
|
||||
-- 80/tcp open http syn-ack
|
||||
-- | http-auth: HTTP/1.1 401 Unauthorized
|
||||
-- | Basic realm=WebAdmin
|
||||
-- |_HTTP server may accept admin:admin combination for Basic authentication.
|
||||
|
||||
-- HTTP authentication information gathering script
|
||||
-- rev 1.1 (2007-05-25)
|
||||
@@ -24,51 +25,58 @@ categories = {"default", "auth", "intrusive"}
|
||||
|
||||
require "shortport"
|
||||
require "http"
|
||||
require "base64"
|
||||
|
||||
portrule = shortport.port_or_service({80, 443, 8080}, {"http","https"})
|
||||
|
||||
action = function(host, port)
|
||||
local realm,scheme,result,authheader
|
||||
local basic = false
|
||||
local authcombinations= {"admin:", "admin:admin"}
|
||||
local www_authenticate
|
||||
local challenges, basic_challenge
|
||||
local authcombinations= {
|
||||
{ username = "admin", password = ""},
|
||||
{ username = "admin", password = "admin"},
|
||||
}
|
||||
|
||||
local answer = http.get( host, port, "/" )
|
||||
local result = {}
|
||||
local answer = http.get(host, port, "/")
|
||||
|
||||
--- check for 401 response code
|
||||
if answer.status == 401 then
|
||||
result = "HTTP Service requires authentication\n"
|
||||
if answer.status ~= 401 then
|
||||
return
|
||||
end
|
||||
|
||||
-- split www-authenticate header
|
||||
local auth_headers = {}
|
||||
local pcre = pcre.new('\\w+( (\\w+=("[^"]+"|\\w+), *)*(\\w+=("[^"]+"|\\w+)))?',0,"C")
|
||||
local match = function( match ) table.insert(auth_headers, match) end
|
||||
pcre:gmatch( answer.header['www-authenticate'], match )
|
||||
result[#result + 1] = answer["status-line"]
|
||||
|
||||
for _, value in pairs( auth_headers ) do
|
||||
result = result .. " Auth type: "
|
||||
scheme, realm = string.match(value, "(%a+).-[Rr]ealm=\"(.-)\"")
|
||||
if scheme == "Basic" then
|
||||
basic = true
|
||||
end
|
||||
if realm ~= nil then
|
||||
result = result .. scheme .. ", realm = " .. realm .. "\n"
|
||||
else
|
||||
result = result .. string.match(value, "(%a+)") .. "\n"
|
||||
www_authenticate = answer.header["www-authenticate"]
|
||||
if not www_authenticate then
|
||||
result[#result + 1] = string.format("Server returned status %d but no WWW-Authenticate header.", answer.status)
|
||||
return table.concat(result, "\n")
|
||||
end
|
||||
challenges = http.parse_www_authenticate(www_authenticate)
|
||||
if not challenges then
|
||||
result[#result + 1] = string.format("Server returned status %d but the WWW-Authenticate header could not be parsed.", answer.status)
|
||||
result[#result + 1] = string.format("WWW-Authenticate: %s", www_authenticate)
|
||||
return table.concat(result, "\n")
|
||||
end
|
||||
|
||||
basic_challenge = nil
|
||||
for _, challenge in ipairs(challenges) do
|
||||
if challenge.scheme == "Basic" then
|
||||
basic_challenge = challenge
|
||||
end
|
||||
local line = challenge.scheme
|
||||
for name, value in pairs(challenge.namevals) do
|
||||
line = line .. string.format(" %s=%s", name, value)
|
||||
end
|
||||
result[#result + 1] = line
|
||||
end
|
||||
if basic_challenge then
|
||||
for _, auth in ipairs(authcombinations) do
|
||||
answer = http.get(host, port, '/', {auth = auth})
|
||||
if answer.status ~= 401 and answer.status ~= 403 then
|
||||
result[#result + 1] = string.format("HTTP server may accept %s:%s combination for Basic authentication.", auth.username, auth.password)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if basic then
|
||||
for _, combination in pairs (authcombinations) do
|
||||
authheader = "Basic " .. base64.enc(combination)
|
||||
answer = http.get(host, port, '/', {header={Authorization=authheader}})
|
||||
if answer.status ~= 401 and answer.status ~= 403 then
|
||||
result = result .. " HTTP server may accept " .. combination .. " combination for Basic authentication\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
return table.concat(result, "\n")
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user