summaryrefslogtreecommitdiffstats
path: root/nselib/sasl.lua
diff options
context:
space:
mode:
Diffstat (limited to 'nselib/sasl.lua')
-rw-r--r--nselib/sasl.lua510
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;