diff --git a/CHANGELOG b/CHANGELOG index 7c2714abd..505ec2133 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,10 @@ # Nmap Changelog ($Id$); -*-text-*- +o [NSE] Added SASL library created by Djalal Harouni and Patrik Karlsson + providing common code for "Simple Authentication and Security Layer" to + services supporting it. The algorithms supported by the library are: + PLAIN, CRAM-MD5, DIGEST-MD5 and NTLM. [Patrik Karlsson, Djalal Harouni] + o [NSE] Added scripts cvs-brute.nse, cvs-brute-repository.nse and the cvs library. The cvs-brute-repository script allows for guessing possible repository names needed in order to perform password guessing using the diff --git a/nselib/sasl.lua b/nselib/sasl.lua new file mode 100644 index 000000000..b8e4fd964 --- /dev/null +++ b/nselib/sasl.lua @@ -0,0 +1,432 @@ +--- +-- 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 (@see DigestMD5.new). +-- It can be called throught 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 (@see NTLM.new). 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 methodes: +-- * 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 http://nmap.org/book/man-legal.html + +-- Version 0.2 +-- Created 07/17/2011 - v0.1 - Created by Djalal Harouini +-- Revised 07/18/2011 - v0.2 - Added NTLM, DIGEST-MD5 classes + + +module(... or "sasl", package.seeall) + +local HAVE_SSL = false + +require 'stdnse' +require 'base64' +require 'smbauth' + +local MECHANISMS = { } + +-- 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 unqoted identifiers + -- regardless of what RFC says + parseChallenge = function(self) + local results = {} + local start, stop = 0,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 + self.challnvs[name:lower()] = value + start, stop = self.chall:find("%s*,%s*", stop + 1) + if ( not(start) ) then break 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" + 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 response = "username=\"" .. self.username .. "\"" + response = response .. (",%s=\"%s\""):format("realm", realm) + response = response .. (",%s=\"%s\""):format("nonce", self.challnvs.nonce) + response = response .. (",%s=\"%s\""):format("cnonce", cnonce) + response = response .. (",%s=%08d"):format("nc", self.nc) + response = response .. (",%s=%s"):format("qop", "auth") + response = response .. (",%s=\"%s\""):format("digest-uri", uri) + response = response .. (",%s=%s"):format("response", digest) + response = response .. (",%s=%s"):format("charset", "utf-8") + + return response + 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, + + --- Converst str to "unicode" (adds null bytes for every other byte) + -- @param str containing string to convert + -- @return unicode string containing the unicoded str + to_unicode = function(str) + local unicode = "" + for i = 1, #str, 1 do + unicode = unicode .. bin.pack("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.print_debug(3, + "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 methode 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, +}