mirror of
https://github.com/nmap/nmap.git
synced 2026-02-09 15:06:35 +00:00
Add digest auth support to http-brute (and to http library). Also fix whitespace in sasl.lua.
This commit is contained in:
@@ -65,7 +65,8 @@
|
||||
-- ** <code>name</code>
|
||||
-- ** <code>value</code>
|
||||
-- ** <code>path</code>
|
||||
-- * <code>auth</code>: A table containing the keys <code>username</code> and <code>password</code>, which will be used for HTTP Basic authentication
|
||||
-- * <code>auth</code>: A table containing the keys <code>username</code> and <code>password</code>, which will be used for HTTP Basic authentication.
|
||||
-- If a server requires HTTP Digest authentication, then there must also be a key <code>digest</code>, with value <code>true</code>.
|
||||
-- * <code>bypass_cache</code>: Do not perform a lookup in the local HTTP cache.
|
||||
-- * <code>no_cache</code>: Do not save the result of this request to the local HTTP cache.
|
||||
-- * <code>no_cache_body</code>: Do not save the body of the response to the local HTTP cache.
|
||||
@@ -106,6 +107,7 @@ local comm = require "comm"
|
||||
local coroutine = require "coroutine"
|
||||
local nmap = require "nmap"
|
||||
local os = require "os"
|
||||
local sasl = require "sasl"
|
||||
local stdnse = require "stdnse"
|
||||
local string = require "string"
|
||||
local table = require "table"
|
||||
@@ -328,6 +330,20 @@ local function validate_options(options)
|
||||
stdnse.print_debug(1, "http: options.auth should be a table")
|
||||
bad = true
|
||||
end
|
||||
elseif (key == 'digestauth') then
|
||||
if(type(value) == 'table') then
|
||||
local req_keys = {"username","realm","nonce","digest-uri","response"}
|
||||
for _,k in ipairs(req_keys) do
|
||||
if not value[k] then
|
||||
stdnse.print_debug(1, "http: options.digestauth missing key: %s",k)
|
||||
bad = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
bad = true
|
||||
stdnse.print_debug(1, "http: options.digestauth should be a table")
|
||||
end
|
||||
elseif(key == 'bypass_cache' or key == 'no_cache' or key == 'no_cache_body') then
|
||||
if(type(value) ~= 'boolean') then
|
||||
stdnse.print_debug(1, "http: options.bypass_cache, options.no_cache, and options.no_cache_body must be boolean values")
|
||||
@@ -1101,14 +1117,36 @@ local function build_request(host, port, method, path, options)
|
||||
mod_options.header["Cookie"] = cookies
|
||||
end
|
||||
end
|
||||
-- Only Basic authentication is supported.
|
||||
if options.auth then
|
||||
|
||||
if options.auth and not options.auth.digest then
|
||||
local username = options.auth.username
|
||||
local password = options.auth.password
|
||||
local credentials = "Basic " .. base64.enc(username .. ":" .. password)
|
||||
mod_options.header["Authorization"] = credentials
|
||||
end
|
||||
|
||||
if options.digestauth then
|
||||
local order = {"username", "realm", "nonce", "digest-uri", "algorithm", "response", "qop", "nc", "cnonce"}
|
||||
local no_quote = {algorithm=true, qop=true, nc=true}
|
||||
local creds = {}
|
||||
for _,k in ipairs(order) do
|
||||
local v = options.digestauth[k]
|
||||
if v then
|
||||
if no_quote[k] then
|
||||
table.insert(creds, ("%s=%s"):format(k,v))
|
||||
else
|
||||
if k == "digest-uri" then
|
||||
table.insert(creds, ('%s="%s"'):format("uri",v))
|
||||
else
|
||||
table.insert(creds, ('%s="%s"'):format(k,v))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local credentials = "Digest "..table.concat(creds, ", ")
|
||||
mod_options.header["Authorization"] = credentials
|
||||
end
|
||||
|
||||
local body
|
||||
-- Build a form submission from a table, like "k1=v1&k2=v2".
|
||||
if type(options.content) == "table" then
|
||||
@@ -1217,6 +1255,30 @@ function generic_request(host, port, method, path, options)
|
||||
if(not(validate_options(options))) then
|
||||
return http_error("Options failed to validate.")
|
||||
end
|
||||
|
||||
local digest_auth = options and options.auth and options.auth.digest
|
||||
|
||||
if digest_auth and not have_ssl then
|
||||
stdnse.print_debug("http: digest auth requires openssl.")
|
||||
end
|
||||
|
||||
if digest_auth and have_ssl then
|
||||
-- If we want to do digest authentication, we have to make an initial
|
||||
-- request to get realm, nonce and other fields.
|
||||
local options_with_auth_removed = tcopy(options)
|
||||
options_with_auth_removed["auth"] = nil
|
||||
local r = generic_request(host, port, method, path, options_with_auth_removed)
|
||||
local h = r.header['www-authenticate']
|
||||
if not r.status or (h and not string.find(h:lower(), "digest.-realm")) then
|
||||
stdnse.print_debug("http: the target doesn't support digest auth or there was an error during request.")
|
||||
return http_error("The target doesn't support digest auth or there was an error during request.")
|
||||
end
|
||||
-- Compute the response hash
|
||||
local dmd5 = sasl.DigestMD5:new(h, options.auth.username, options.auth.password, method, path)
|
||||
local _, digest_table = dmd5:calcDigest()
|
||||
options.digestauth = digest_table
|
||||
end
|
||||
|
||||
return request(host, port, build_request(host, port, method, path, options), options)
|
||||
end
|
||||
|
||||
|
||||
170
nselib/sasl.lua
170
nselib/sasl.lua
@@ -56,79 +56,117 @@ end
|
||||
local MECHANISMS = { }
|
||||
|
||||
if HAVE_SSL then
|
||||
-- Calculates a DIGEST MD5 response
|
||||
DigestMD5 = {
|
||||
-- Calculates a DIGEST MD5 response
|
||||
DigestMD5 = {
|
||||
|
||||
--- Instantiates DigestMD5
|
||||
--
|
||||
-- @param chall string containing the base64 decoded challenge
|
||||
-- @return a new instance of DigestMD5
|
||||
new = function(self, chall, username, password, method, uri, service, realm)
|
||||
local o = { nc = 0,
|
||||
chall = chall,
|
||||
challnvs = {},
|
||||
username = username,
|
||||
password = password,
|
||||
method = method,
|
||||
uri = uri,
|
||||
service = service,
|
||||
realm = realm }
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
o:parseChallenge()
|
||||
return o
|
||||
end,
|
||||
--- Instantiates DigestMD5
|
||||
--
|
||||
-- @param chall string containing the base64 decoded challenge
|
||||
-- @return a new instance of DigestMD5
|
||||
new = function(self, chall, username, password, method, uri, service, realm)
|
||||
local o = { nc = 0,
|
||||
chall = chall,
|
||||
challnvs = {},
|
||||
username = username,
|
||||
password = password,
|
||||
method = method,
|
||||
uri = uri,
|
||||
service = service,
|
||||
realm = realm }
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
o:parseChallenge()
|
||||
return o
|
||||
end,
|
||||
|
||||
-- parses a challenge received from the server
|
||||
-- takes care of both quoted and unqoted identifiers
|
||||
-- regardless of what RFC says
|
||||
parseChallenge = function(self)
|
||||
local results = {}
|
||||
local start, stop = 0,0
|
||||
while(true) do
|
||||
local name, value
|
||||
start, stop, name = self.chall:find("([^=]*)=%s*", stop + 1)
|
||||
if ( not(start) ) then break end
|
||||
if ( self.chall:sub(stop + 1, stop + 1) == "\"" ) then
|
||||
start, stop, value = self.chall:find("(.-)\"", stop + 2)
|
||||
else
|
||||
start, stop, value = self.chall:find("([^,]*)", stop + 1)
|
||||
end
|
||||
self.challnvs[name:lower()] = value
|
||||
start, stop = self.chall:find("%s*,%s*", stop + 1)
|
||||
if ( not(start) ) then break end
|
||||
end
|
||||
end,
|
||||
-- parses a challenge received from the server
|
||||
-- takes care of both quoted and unqoted identifiers
|
||||
-- regardless of what RFC says
|
||||
parseChallenge = function(self)
|
||||
local results = {}
|
||||
local start, stop = 0,0
|
||||
while(true) do
|
||||
local name, value
|
||||
start, stop, name = self.chall:find("([^=]*)=%s*", stop + 1)
|
||||
if ( not(start) ) then break end
|
||||
if ( self.chall:sub(stop + 1, stop + 1) == "\"" ) then
|
||||
start, stop, value = self.chall:find("(.-)\"", stop + 2)
|
||||
else
|
||||
start, stop, value = self.chall:find("([^,]*)", stop + 1)
|
||||
end
|
||||
name = name:lower()
|
||||
if name == "digest realm" then name="realm" end
|
||||
self.challnvs[name] = value
|
||||
start, stop = self.chall:find("%s*,%s*", stop + 1)
|
||||
if ( not(start) ) then break end
|
||||
end
|
||||
end,
|
||||
|
||||
--- Calculates the digest
|
||||
calcDigest = function( self )
|
||||
local uri = self.uri or ("%s/%s"):format(self.service, "localhost")
|
||||
local realm = self.realm or self.challnvs.realm or ""
|
||||
local cnonce = stdnse.tohex(openssl.rand_bytes( 8 ))
|
||||
local qop = "auth"
|
||||
self.nc = self.nc + 1
|
||||
local A1_part1 = openssl.md5(self.username .. ":" .. (self.challnvs.realm or "") .. ":" .. self.password)
|
||||
local A1 = stdnse.tohex(openssl.md5(A1_part1 .. ":" .. self.challnvs.nonce .. ':' .. cnonce))
|
||||
local A2 = stdnse.tohex(openssl.md5(("%s:%s"):format(self.method, uri)))
|
||||
local digest = stdnse.tohex(openssl.md5(A1 .. ":" .. self.challnvs.nonce .. ":" ..
|
||||
("%08d"):format(self.nc) .. ":" .. cnonce .. ":" ..
|
||||
qop .. ":" .. A2))
|
||||
--- Calculates the digest
|
||||
calcDigest = function( self )
|
||||
local uri = self.uri or ("%s/%s"):format(self.service, "localhost")
|
||||
local realm = self.realm or self.challnvs.realm or ""
|
||||
local cnonce = stdnse.tohex(openssl.rand_bytes( 8 ))
|
||||
local qop = "auth"
|
||||
local qop_not_specified
|
||||
if self.challnvs.qop then
|
||||
qop_not_specified = false
|
||||
else
|
||||
qop_not_specified = true
|
||||
end
|
||||
self.nc = self.nc + 1
|
||||
local A1_part1 = openssl.md5(self.username .. ":" .. (self.challnvs.realm or "") .. ":" .. self.password)
|
||||
local A1 = stdnse.tohex(openssl.md5(A1_part1 .. ":" .. self.challnvs.nonce .. ':' .. cnonce))
|
||||
local A2 = stdnse.tohex(openssl.md5(("%s:%s"):format(self.method, uri)))
|
||||
local digest = stdnse.tohex(openssl.md5(A1 .. ":" .. self.challnvs.nonce .. ":" ..
|
||||
("%08d"):format(self.nc) .. ":" .. cnonce .. ":" ..
|
||||
qop .. ":" .. A2))
|
||||
|
||||
local response = "username=\"" .. self.username .. "\""
|
||||
response = response .. (",%s=\"%s\""):format("realm", realm)
|
||||
response = response .. (",%s=\"%s\""):format("nonce", self.challnvs.nonce)
|
||||
response = response .. (",%s=\"%s\""):format("cnonce", cnonce)
|
||||
response = response .. (",%s=%08d"):format("nc", self.nc)
|
||||
response = response .. (",%s=%s"):format("qop", "auth")
|
||||
response = response .. (",%s=\"%s\""):format("digest-uri", uri)
|
||||
response = response .. (",%s=%s"):format("response", digest)
|
||||
response = response .. (",%s=%s"):format("charset", "utf-8")
|
||||
local b1
|
||||
if not self.challnvs.algorithm or self.challnvs.algorithm == "MD5" then
|
||||
b1 = stdnse.tohex(openssl.md5(self.username..":"..(self.challnvs.realm or "")..":"..self.password))
|
||||
else
|
||||
b1 = A1
|
||||
end
|
||||
-- should we make it work when qop == "auth-int" (we would need entity-body here, which
|
||||
-- might be complicated)?
|
||||
|
||||
return response
|
||||
end,
|
||||
local digest_http
|
||||
if not qop_not_specified then
|
||||
digest_http = stdnse.tohex(openssl.md5(b1 .. ":" .. self.challnvs.nonce .. ":" ..
|
||||
("%08d"):format(self.nc) .. ":" .. cnonce .. ":" .. qop .. ":" .. A2))
|
||||
else
|
||||
digest_http = stdnse.tohex(openssl.md5(b1 .. ":" .. self.challnvs.nonce .. ":" .. A2))
|
||||
end
|
||||
|
||||
local response = "username=\"" .. self.username .. "\""
|
||||
response = response .. (",%s=\"%s\""):format("realm", realm)
|
||||
response = response .. (",%s=\"%s\""):format("nonce", self.challnvs.nonce)
|
||||
response = response .. (",%s=\"%s\""):format("cnonce", cnonce)
|
||||
response = response .. (",%s=%08d"):format("nc", self.nc)
|
||||
response = response .. (",%s=%s"):format("qop", "auth")
|
||||
response = response .. (",%s=\"%s\""):format("digest-uri", uri)
|
||||
response = response .. (",%s=%s"):format("response", digest)
|
||||
response = response .. (",%s=%s"):format("charset", "utf-8")
|
||||
|
||||
-- response_table is used in http library because the request should
|
||||
-- be a little bit different then the string generated above
|
||||
local response_table = {
|
||||
username = self.username,
|
||||
realm = realm,
|
||||
nonce = self.challnvs.nonce,
|
||||
cnonce = cnonce,
|
||||
nc = ("%08d"):format(self.nc),
|
||||
qop = qop,
|
||||
["digest-uri"] = uri,
|
||||
algorithm = self.challnvs.algorithm,
|
||||
response = digest_http
|
||||
}
|
||||
|
||||
return response, response_table
|
||||
end,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
-- The NTLM class handling NTLM challenge response authentication
|
||||
NTLM = {
|
||||
|
||||
@@ -40,9 +40,11 @@ Performs brute force password auditing against http basic authentication.
|
||||
--
|
||||
-- Version 0.1
|
||||
-- Created 07/30/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
|
||||
-- Version 0.2
|
||||
-- 07/26/2012 - v0.2 - added digest auth support (Piotr Olma)
|
||||
--
|
||||
|
||||
author = "Patrik Karlsson"
|
||||
author = "Patrik Karlsson, Piotr Olma"
|
||||
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
|
||||
categories = {"intrusive", "brute"}
|
||||
|
||||
@@ -50,17 +52,14 @@ categories = {"intrusive", "brute"}
|
||||
portrule = shortport.port_or_service( {80, 443}, {"http", "https"}, "tcp", "open")
|
||||
|
||||
Driver = {
|
||||
|
||||
new = function(self, host, port, method)
|
||||
local o = {}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
o.host = stdnse.get_script_args("http-brute.hostname") or host
|
||||
o.port = port
|
||||
o.path = stdnse.get_script_args("http-brute.path") or "/"
|
||||
o.method = method
|
||||
return o
|
||||
end,
|
||||
|
||||
new = function(self, host, port, opts)
|
||||
local o = {port=port, path=opts.path, method=opts.method, digestauth=opts.digestauth}
|
||||
setmetatable(o, self)
|
||||
self.__index = self
|
||||
o.host = stdnse.get_script_args("http-brute.hostname") or host
|
||||
return o
|
||||
end,
|
||||
|
||||
connect = function( self )
|
||||
-- This will cause problems, as ther is no way for us to "reserve"
|
||||
@@ -69,11 +68,24 @@ Driver = {
|
||||
return true
|
||||
end,
|
||||
|
||||
login = function( self, username, password )
|
||||
-- we need to supply the no_cache directive, or else the http library
|
||||
-- incorrectly tells us that the authentication was successfull
|
||||
local response = http.generic_request( self.host, self.port, self.method, self.path, { auth = { username = username, password = password }, no_cache = true })
|
||||
|
||||
login = function( self, username, password )
|
||||
local response
|
||||
local opts_table
|
||||
if not self.digestauth then
|
||||
-- we need to supply the no_cache directive, or else the http library
|
||||
-- incorrectly tells us that the authentication was successful
|
||||
opts_table = { auth = { username = username, password = password }, no_cache = true }
|
||||
else
|
||||
opts_table = { auth = { username = username, password = password, digest = true }, no_cache = true }
|
||||
end
|
||||
response = http.generic_request( self.host, self.port, self.method, self.path, opts_table)
|
||||
|
||||
if not response.status then
|
||||
local err = brute.Error:new(response["status-line"])
|
||||
err:setRetry(true)
|
||||
return false, err
|
||||
end
|
||||
|
||||
-- Checking for ~= 401 *should* work to
|
||||
-- but gave me a number of false positives last time I tried.
|
||||
-- We decided to change it to ~= 4xx.
|
||||
@@ -95,12 +107,7 @@ Driver = {
|
||||
end,
|
||||
|
||||
check = function( self )
|
||||
local response = http.generic_request( self.host, self.port, self.method, self.path, { no_cache = true } )
|
||||
|
||||
if ( response.status == 401 ) then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
return true
|
||||
end,
|
||||
|
||||
}
|
||||
@@ -110,8 +117,6 @@ action = function( host, port )
|
||||
local status, result
|
||||
local path = stdnse.get_script_args("http-brute.path") or "/"
|
||||
local method = string.upper(stdnse.get_script_args("http-brute.method") or "GET")
|
||||
local engine = brute.Engine:new(Driver, host, port, method )
|
||||
engine.options.script_name = SCRIPT_NAME
|
||||
|
||||
if ( not(path) ) then
|
||||
return " \n ERROR: No path was specified (see http-brute.path)"
|
||||
@@ -122,9 +127,21 @@ action = function( host, port )
|
||||
if ( response.status ~= 401 ) then
|
||||
return (" \n Path \"%s\" does not require authentication"):format(path)
|
||||
end
|
||||
|
||||
|
||||
-- check if digest auth is required
|
||||
local digestauth = false
|
||||
local h = response.header['www-authenticate']
|
||||
if h then
|
||||
h = h:lower()
|
||||
if string.find(h, 'digest.-realm') then
|
||||
digestauth = true
|
||||
end
|
||||
end
|
||||
|
||||
status, result = engine:start()
|
||||
|
||||
return result
|
||||
local engine = brute.Engine:new(Driver, host, port, {method=method, path=path, digestauth=digestauth})
|
||||
engine.options.script_name = SCRIPT_NAME
|
||||
|
||||
status, result = engine:start()
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user