diff --git a/nselib/http.lua b/nselib/http.lua
index 6a7925ecf..9899edeb2 100644
--- a/nselib/http.lua
+++ b/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
-- * header: A table containing additional headers to be used for the request.
-- * content: The content of the message (content-length will be added -- set header['Content-Length'] to override)
-- * cookies: A table of cookies in the form returned by parse_set_cookie.
+-- * auth: A table containing the keys username and password.
-- @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
-- * header: A table containing additional headers to be used for the request.
-- * content: The content of the message (content-length will be added -- set header['Content-Length'] to override)
-- * cookies: A table of cookies in the form returned by parse_set_cookie.
+-- * auth: A table containing the keys username and password.
-- @return A table as described in the module description.
-- @see request
generic_request = function(host, port, method, path, options)
@@ -937,6 +970,7 @@ end
-- * header: A table containing additional headers to be used for the request.
-- * content: The content of the message (content-length will be added -- set header['Content-Length'] to override)
-- * cookies: A table of cookies in the form returned by parse_set_cookie.
+-- * auth: A table containing the keys username and password.
-- @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 print_debug messages and even for advanced output.
--
diff --git a/scripts/http-auth.nse b/scripts/http-auth.nse
index 55e90798d..87f35d077 100644
--- a/scripts/http-auth.nse
+++ b/scripts/http-auth.nse
@@ -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
-