diff options
Diffstat (limited to '')
-rw-r--r-- | nselib/sasl.lua | 510 |
1 files changed, 510 insertions, 0 deletions
diff --git a/nselib/sasl.lua b/nselib/sasl.lua new file mode 100644 index 0000000..910e1f3 --- /dev/null +++ b/nselib/sasl.lua @@ -0,0 +1,510 @@ +--- +-- Simple Authentication and Security Layer (SASL). +-- +-- The library contains some low level functions and a high level class. +-- +-- The <code>DigestMD5</code> class contains all code necessary to calculate +-- a DIGEST-MD5 response based on the servers challenge and the other +-- necessary arguments. +-- It can be called through the SASL helper or directly like this: +-- <code> +-- local dmd5 = DigestMD5:new(chall, user, pass, "AUTHENTICATE", nil, "imap") +-- local digest = dmd5:calcDigest() +-- </code> +-- +-- The <code>NTLM</code> class contains all code necessary to calculate a +-- NTLM response based on the servers challenge and the other necessary +-- arguments. It can be called through the SASL helper or +-- directly like this: +-- <code> +-- local ntlm = NTLM:new(chall, user, pass) +-- local response = ntlm:calcResponse() +-- </code> +-- +-- The Helper class contains the high level methods: +-- * <code>new</code>: This is the SASL object constructor. +-- * <code>set_mechanism</code>: Sets the authentication mechanism to use. +-- * <code>set_callback</code>: Sets the encoding function to use. +-- * <code>encode</code>: Encodes the parameters according to the +-- authentication mechanism. +-- * <code>reset_callback</code>: Resets the authentication function. +-- * <code>reset</code>: Resets the SASL object. +-- +-- The script writers should use the Helper class to create SASL objects, +-- and they can also use the low level functions to customize their +-- encoding functions. +-- +-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html + +-- Version 0.2 +-- Created 07/17/2011 - v0.1 - Created by Djalal Harouni +-- Revised 07/18/2011 - v0.2 - Added NTLM, DIGEST-MD5 classes + + +local smbauth = require "smbauth" +local stdnse = require "stdnse" +local string = require "string" +local unicode = require "unicode" +_ENV = stdnse.module("sasl", stdnse.seeall) + +local HAVE_SSL, openssl = pcall(require, 'openssl') +if ( not(HAVE_SSL) ) then + stdnse.debug1( + "sasl.lua: OpenSSL not present, SASL support limited.") +end +local MECHANISMS = { } + +if HAVE_SSL then + -- 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, + + -- parses a challenge received from the server + -- takes care of both quoted and unquoted identifiers + -- regardless of what RFC says + parseChallenge = function(self) + local results = {} + if self.chall then + local start, stop = self.chall:find("^[Dd][Ii][Gg][Ee][Ss][Tt]%s+") + stop = stop or 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 + 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" + 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 b1 + if not self.challnvs.algorithm or self.challnvs.algorithm:upper() == "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)? + + 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 .. "\"" + .. (",%s=\"%s\""):format("realm", realm) + .. (",%s=\"%s\""):format("nonce", self.challnvs.nonce) + .. (",%s=\"%s\""):format("cnonce", cnonce) + .. (",%s=%08d"):format("nc", self.nc) + .. (",%s=%s"):format("qop", "auth") + .. (",%s=\"%s\""):format("digest-uri", uri) + .. (",%s=%s"):format("response", digest) + .. (",%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 = { + + --- Creates a new instance of the NTLM class + -- + -- @param chall string containing the challenge received from the server + -- @param username string containing the username + -- @param password string containing the password + -- @return new instance of NTML + new = function(self, chall, username, password) + local o = { nc = 0, + chall = chall, + username = username, + password = password} + setmetatable(o, self) + self.__index = self + o:parseChallenge() + return o + end, + + --- Parses the NTLM challenge as received from the server + parseChallenge = function(self) + local NTLM_NegotiateUnicode = 0x00000001 + local NTLM_NegotiateExtendedSecurity = 0x00080000 + local pos, _, message_type + + _, message_type, _, _, + _, self.flags, self.chall, _, + _, _, _, pos = string.unpack("<c8 I4 I2 I2 I4 I4 c8 I8 I2 I2 I4", self.chall) + + if ( message_type ~= 0x02 ) then + error("NTLM parseChallenge expected message type: 0x02") + end + + self.is_extended = ( (self.flags & NTLM_NegotiateExtendedSecurity) == NTLM_NegotiateExtendedSecurity ) + local is_unicode = ( (self.flags & NTLM_NegotiateUnicode) == NTLM_NegotiateUnicode ) + + self.workstation = "NMAP-HOST" + self.domain = self.username:match("^(.-)\\(.*)$") or "DOMAIN" + + if ( is_unicode ) then + self.workstation = unicode.utf8to16(self.workstation) + self.username = unicode.utf8to16(self.username) + self.domain = unicode.utf8to16(self.domain) + end + end, + + --- Calculates the response + calcResponse = function(self) + local ntlm, lm = smbauth.get_password_response(nil, self.username, self.domain, self.password, nil, "v1", self.chall, self.is_extended) + local msg_type = 3 + local response + local BASE_OFFSET = 72 + local offset + local encrypted_random_sesskey = "" + local flags = 0xa2888205 -- (NTLM_NegotiateUnicode | \ + -- NTLM_RequestTarget | \ + -- NTLM_NegotiateNTLM | \ + -- NTLM_NegotiateAlwaysSign | \ + -- NTLM_NegotiateExtendedSecurity | \ + -- NTLM_NegotiateTargetInfo | \ + -- NTLM_NegotiateVersion | \ + -- NTLM_Negotiate128 | \ + -- NTLM_Negotiate56) + + response = string.pack("<zI4", "NTLMSSP", msg_type) + + offset = BASE_OFFSET + #self.workstation + #self.username + #self.domain + response = response .. string.pack("<I2I2I4", #lm, #lm, offset) + + offset = offset + #lm + response = response .. string.pack("<I2I2I4", #ntlm, #ntlm, offset) + + offset = BASE_OFFSET + response = response .. string.pack("<I2I2I4", #self.domain, #self.domain, offset) + + offset = BASE_OFFSET + #self.domain + response = response .. string.pack("<I2I2I4", #self.username, #self.username, offset) + + offset = BASE_OFFSET + #self.domain + #self.username + response = response .. string.pack("<I2I2I4", #self.workstation, #self.workstation, offset) + + offset = offset + #self.workstation + #lm + #ntlm + response = response .. string.pack("<I2I2I4", #encrypted_random_sesskey, #encrypted_random_sesskey, offset) + + response = response .. string.pack("<I4", flags) + + -- add version info (major 5, minor 1, build 2600, reserved(1-3) 0, + -- NTLM Revision 15) + response = response .. string.pack("<BBI2 BBBB", 5, 1, 2600, 0, 0, 0, 15) + response = response .. self.domain .. self.username .. self.workstation .. ntlm .. lm .. encrypted_random_sesskey + + return response + end + + } + + --- Encodes the parameters using the <code>CRAM-MD5</code> mechanism. + -- + -- @param username string. + -- @param password string. + -- @param challenge The challenge as it is returned by the server. + -- @return string The encoded string on success, or nil if Nmap was + -- compiled without OpenSSL. + function cram_md5_enc(username, password, challenge) + local encode = stdnse.tohex(openssl.hmac('md5', + password, + challenge)) + return username.." "..encode + end + + --- Encodes the parameters using the <code>DIGEST-MD5</code> mechanism. + -- + -- @param username string. + -- @param password string. + -- @param challenge The challenge as it is returned by the server. + -- @param service string containing the service that is requesting the + -- encryption (eg. POP, IMAP, STMP) + -- @param uri string containing the URI + -- @return string The encoded string on success, or nil if Nmap was + -- compiled without OpenSSL. + function digest_md5_enc(username, password, challenge, service, uri) + return DigestMD5:new(challenge, + username, + password, + "AUTHENTICATE", + uri, + service):calcDigest() + end + + function ntlm_enc(username, password, challenge) + return NTLM:new(challenge, username, password):calcResponse() + end + +else + function cram_md5_enc() + error("cram_md5_enc not supported without OpenSSL") + end + + function digest_md5_enc() + error("digest_md5_enc not supported without OpenSSL") + end + + function ntlm_enc() + error("ntlm_enc not supported without OpenSSL") + end +end + +MECHANISMS["CRAM-MD5"] = cram_md5_enc +MECHANISMS["DIGEST-MD5"] = digest_md5_enc +MECHANISMS["NTLM"] = ntlm_enc + + +--- Encodes the parameters using the <code>PLAIN</code> mechanism. +-- +-- @param username string. +-- @param password string. +-- @return string The encoded string. +function plain_enc(username, password) + return username.."\0"..username.."\0"..password +end +MECHANISMS["PLAIN"] = plain_enc + + +--- Checks if the given mechanism is supported by this library. +-- +-- @param mechanism string to check. +-- @return mechanism if it is supported, otherwise nil. +-- @return callback The mechanism encoding function on success. +function check_mechanism(mechanism) + local lmech, lcallback + if mechanism then + mechanism = string.upper(mechanism) + if MECHANISMS[mechanism] then + lmech = mechanism + lcallback = MECHANISMS[mechanism] + else + stdnse.debug3( + "sasl library does not support '%s' mechanism", mechanism) + end + end + return lmech, lcallback +end + +--- This is the SASL Helper class, script writers should use it to create +-- SASL objects. +-- +-- Usage of the Helper class: +-- +-- local sasl_enc = sasl.Helper.new("CRAM-MD5") +-- local result = sasl_enc:encode(username, password, challenge) +-- +-- sasl_enc:set_mechanism("LOGIN") +-- local user, pass = sasl_enc:encode(username, password) +Helper = { + + --- SASL object constructor. + -- + -- @param mechanism The authentication mechanism to use + -- (optional parameter). + -- @param callback The encoding function associated with the + -- mechanism (optional parameter). + -- @usage + -- local sasl_enc = sasl.Helper:new() + -- local sasl_enc = sasl.Helper:new("CRAM-MD5") + -- local sasl_enc = sasl.Helper:new("CRAM-MD5", my_cram_md5_func) + -- @return sasl object. + new = function(self, mechanism, callback) + local o = {} + setmetatable(o, self) + self.__index = self + if self:set_mechanism(mechanism) then + self:set_callback(callback) + end + return o + end, + + --- Sets the SASL mechanism to use. + -- + -- @param string The authentication mechanism. + -- @usage + -- local sasl_enc = sasl.Helper:new() + -- sasl_enc:set_mechanism("CRAM-MD5") + -- sasl_enc:set_mechanism("PLAIN") + -- @return mechanism on success, or nil if the mechanism is not + -- supported. + set_mechanism = function(self, mechanism) + self.mechanism, self.callback = check_mechanism(mechanism) + return self.mechanism + end, + + --- Associates A custom encoding function with the authentication + -- mechanism. + -- + -- Note that the SASL object by default will have its own + -- callback functions. + -- + -- @param callback The function associated with the authentication + -- mechanism. + -- @usage + -- -- My personal CRAM-MD5 encode function + -- function cram_md5_encode_func(username, password, challenge) + -- ... + -- end + -- local sasl_enc = sasl.Helper:new("CRAM-MD5") + -- sasl_enc:set_callback(cram_md5_handle_func) + -- local result = sasl_enc:encode(username, password, challenge) + set_callback = function(self, callback) + if callback then + self.callback = callback + end + end, + + --- Resets the encoding function to the default SASL + -- callback function. + reset_callback = function(self) + self.callback = MECHANISMS[self.mechanism] + end, + + --- Resets all the data of the SASL object. + -- + -- This method will clear the specified SASL mechanism. + reset = function(self) + self:set_mechanism() + end, + + --- Returns the current authentication mechanism. + -- + -- @return mechanism on success, or nil on failures. + get_mechanism = function(self) + return self.mechanism + end, + + --- Encodes the parameters according to the specified mechanism. + -- + -- @param ... The parameters to encode. + -- @usage + -- local sasl_enc = sasl.Helper:new("CRAM-MD5") + -- local result = sasl_enc:encode(username, password, challenge) + -- local sasl_enc = sasl.Helper:new("PLAIN") + -- local result = sasl_enc:encode(username, password) + -- @return string The encoded string on success, or nil on failures. + encode = function(self, ...) + return self.callback(...) + end, +} + +local unittest = require "unittest" + +if not unittest.testing() then + return _ENV +end + +test_suite = unittest.TestSuite:new() + +-- Crypto tests require OpenSSL +if HAVE_SSL then + local _ = "ignored" + + local object = DigestMD5:new('Digest realm="test", domain="/HTTP/Digest",\z + nonce="c8563a5b367e66b3693fbb07a53a30ba"', + _, _, _, _) + test_suite:add_test(unittest.keys_equal( + object.challnvs, + { + nonce='c8563a5b367e66b3693fbb07a53a30ba', + realm='test', + domain='/HTTP/Digest', + } + )) + + object = DigestMD5:new('Digest nonce="9e4ab724d272474ab13b64d75300a47b", \z + opaque="de40b82666bd5fe631a64f3b2d5a019e", \z + realm="me@kennethreitz.com", qop=auth', + _, _, _, _) + test_suite:add_test(unittest.keys_equal( + object.challnvs, + { + nonce='9e4ab724d272474ab13b64d75300a47b', + opaque='de40b82666bd5fe631a64f3b2d5a019e', + realm='me@kennethreitz.com', + qop='auth', + } + )) + + object = DigestMD5:new('realm=test, domain="/HTTP/Digest",\tnonce=c8563a5b367e66b3693fbb07a53a30ba', + _, _, _, _) + test_suite:add_test(unittest.keys_equal( + object.challnvs, + { + nonce='c8563a5b367e66b3693fbb07a53a30ba', + realm='test', + domain='/HTTP/Digest', + } + )) +end + +return _ENV; |