diff --git a/nselib/http.lua b/nselib/http.lua
index 6d375347c..238b8e18f 100644
--- a/nselib/http.lua
+++ b/nselib/http.lua
@@ -73,6 +73,7 @@
-- Only name and value fields are required.
-- * auth: A table containing the keys username and password, which will be used for HTTP Basic authentication.
-- If a server requires HTTP Digest authentication, then there must also be a key digest, with value true.
+-- If a server requires NTLM authentication, then there must also be a key ntlm, with value true.
-- * bypass_cache: Do not perform a lookup in the local HTTP cache.
-- * no_cache: Do not save the result of this request to the local HTTP cache.
-- * no_cache_body: Do not save the body of the response to the local HTTP cache.
@@ -109,6 +110,8 @@
local base64 = require "base64"
+local bin = require "bin"
+local bit = require "bit"
local comm = require "comm"
local coroutine = require "coroutine"
local nmap = require "nmap"
@@ -118,6 +121,8 @@ local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local url = require "url"
+local smbauth = require "smbauth"
+local unicode = require "unicode"
_ENV = stdnse.module("http", stdnse.seeall)
---Use ssl if we have it
@@ -338,6 +343,8 @@ local function validate_options(options)
bad = true
stdnse.debug1("http: options.digestauth should be a table")
end
+ elseif (key == 'ntlmauth') then
+ stdnse.debug1("Proceeding with ntlm message")
elseif(key == 'bypass_cache' or key == 'no_cache' or key == 'no_cache_body') then
if(type(value) ~= 'boolean') then
stdnse.debug1("http: options.bypass_cache, options.no_cache, and options.no_cache_body must be boolean values")
@@ -1116,7 +1123,7 @@ local function build_request(host, port, method, path, options)
end
end
- if options.auth and not options.auth.digest then
+ if options.auth and not (options.auth.digest or options.auth.ntlm) then
local username = options.auth.username
local password = options.auth.password
local credentials = "Basic " .. base64.enc(username .. ":" .. password)
@@ -1145,6 +1152,11 @@ local function build_request(host, port, method, path, options)
mod_options.header["Authorization"] = credentials
end
+ if options.ntlmauth then
+ mod_options.header["Authorization"] = "NTLM " .. base64.enc(options.ntlmauth)
+ end
+
+
local body
-- Build a form submission from a table, like "k1=v1&k2=v2".
if type(options.content) == "table" then
@@ -1255,8 +1267,9 @@ function generic_request(host, port, method, path, options)
end
local digest_auth = options and options.auth and options.auth.digest
+ local ntlm_auth = options and options.auth and options.auth.ntlm
- if digest_auth and not have_ssl then
+ if (digest_auth or ntlm_auth) and not have_ssl then
stdnse.debug1("http: digest auth requires openssl.")
end
@@ -1277,6 +1290,148 @@ function generic_request(host, port, method, path, options)
options.digestauth = digest_table
end
+ if ntlm_auth and have_ssl then
+
+ local custom_options = tcopy(options) -- to be sent with the type 1 request
+ custom_options["auth"] = nil -- removing the auth options
+ -- let's check if the target supports ntlm with a simple get request.
+ -- Setting a timeout here other than nil messes up the authentication if this is the first device sending
+ -- a request to the server. Don't know why.
+ custom_options.timeout = nil
+ local response = generic_request(host, port, method, path, custom_options)
+ local authentication_header = response.header['www-authenticate']
+ -- get back the timeout option.
+ custom_options.timeout = options.timeout
+ custom_options.header = options.header or {}
+ custom_options.header["Connection"] = "Keep-Alive" -- Keep-Alive headers are needed for authentication.
+
+ if (not authentication_header) or (not response.status) or (not string.find(authentication_header:lower(), "ntlm")) then
+ stdnse.debug1("http: the target doesn't support NTLM or there was an error during request.")
+ return http_error("The target doesn't support NTLM or there was an error during request.")
+ end
+
+ -- ntlm works with three messages. we send a request, it sends
+ -- a challenge, we respond to the challenge.
+ local hostname = options.auth.hostname or "localhost" -- the hostname to be sent
+ local workstation_name = options.auth.workstation_name or "NMAP" -- the workstation name to be sent
+ local username = options.auth.username -- the username as specified
+
+ local auth_blob = "NTLMSSP\x00" .. -- NTLM signature
+ "\x01\x00\x00\x00" .. -- NTLM Type 1 message
+ bin.pack("= 100 and response.status <= 199)
+
+ authentication_header = response.header['www-authenticate']
+ -- take out the challenge
+ local type2_response = authentication_header:sub(authentication_header:find(' ')+1, -1)
+ local _, _, message_type, _, _, _, flags_received, challenge= bin.unpack(", flags, and OS Version structure are all present.
+
+ auth_blob = bin.pack("= 100 and response.status <= 199)
+
+ socket:close()
+ response.ssl = ( opts == 'ssl' )
+
+ return response
+ end
+
return request(host, port, build_request(host, port, method, path, options), options)
end
diff --git a/nselib/smbauth.lua b/nselib/smbauth.lua
index bee8d99c4..de1612008 100644
--- a/nselib/smbauth.lua
+++ b/nselib/smbauth.lua
@@ -589,6 +589,26 @@ function ntlmv2_create_response(ntlm, username, domain, challenge, client_challe
return true, openssl.hmac("MD5", ntlmv2_hash, challenge .. client_challenge) .. client_challenge
end
+
+--- Generates the ntlmv2 session response.
+-- It starts by generatng an 8 byte random client nonce, it is padded to 24 bytes.
+-- The padded value is the lanman response. A session nonce is made by
+-- concatenating the server challenge and the client nonce. The ntlm session hash
+-- is first 8 bytes of the md5 hash of the session nonce.
+-- The ntlm response is the lm response with session hash as challenge.
+-- @param ntlm_passsword_hash The md4 hash of the utf-16 password.
+-- @param challenge The challenge sent by the server.
+function ntlmv2_session_response(ntlm_password_hash, challenge)
+ local client_nonce = openssl.rand_bytes(8)
+
+ local lm_response = client_nonce .. string.rep('\0', 24 - #client_nonce)
+ local session_nonce = challenge .. client_nonce
+ local ntlm_session_hash = openssl.md5(session_nonce):sub(1,8)
+
+ local status, ntlm_response = lm_create_response(ntlm_password_hash, ntlm_session_hash)
+
+ return status, lm_response, ntlm_response
+end
---Generate the Lanman and NTLM password hashes.
--
-- The password itself is taken from the function parameters, the script
@@ -703,6 +723,9 @@ function get_password_response(ip, username, domain, password, password_hash, ha
status, lm_response = lmv2_create_response(ntlm_hash, username, domain, challenge)
ntlm_response = ""
+ elseif(hash_type == "ntlmv2_session") then
+ stdnse.debug2("SMB: Creating nltmv2 session response")
+ status, lm_response, ntlm_response = ntlmv2_session_response(ntlm_hash, challenge)
else
-- Default to NTLMv1
if(hash_type ~= nil) then
@@ -921,7 +944,6 @@ if not unittest.testing() then
end
test_suite = unittest.TestSuite:new()
-if have_ssl then
test_suite:add_test(unittest.equal(
stdnse.tohex(select(-1, lm_create_hash("passphrase"))),
"855c3697d9979e78ac404c4ba2c66533"
@@ -946,9 +968,5 @@ test_suite:add_test(unittest.equal(
),
"ntlm_create_hash"
)
-else
- test_suite:add_test(unittest.is_false(lm_create_hash("a"), "lm_create_hash"))
- test_suite:add_test(unittest.is_false(ntlm_create_hash("a"), "ntlm_create_hash"))
-end
return _ENV;