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;