1
0
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:
david
2010-07-25 17:12:52 +00:00
parent e1607c5509
commit 42a1bd99ab
2 changed files with 200 additions and 40 deletions

View File

@@ -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.
--

View File

@@ -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