1
0
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:
perdo
2012-08-05 22:23:54 +00:00
parent b13bd155ab
commit ba049718b0
3 changed files with 215 additions and 98 deletions

View File

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

View File

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

View File

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