---
-- Simple Authentication and Security Layer (SASL).
--
-- The library contains some low level functions and a high level class.
--
-- The DigestMD5
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:
--
-- local dmd5 = DigestMD5:new(chall, user, pass, "AUTHENTICATE", nil, "imap")
-- local digest = dmd5:calcDigest()
--
--
-- The NTLM
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:
--
-- local ntlm = NTLM:new(chall, user, pass)
-- local response = ntlm:calcResponse()
--
--
-- The Helper class contains the high level methods:
-- * new
: This is the SASL object constructor.
-- * set_mechanism
: Sets the authentication mechanism to use.
-- * set_callback
: Sets the encoding function to use.
-- * encode
: Encodes the parameters according to the
-- authentication mechanism.
-- * reset_callback
: Resets the authentication function.
-- * reset
: 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("CRAM-MD5 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 DIGEST-MD5
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 PLAIN
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;