local asn1 = require "asn1" local coroutine = require "coroutine" local nmap = require "nmap" local os = require "os" local shortport = require "shortport" local stdnse = require "stdnse" local string = require "string" local table = require "table" local unpwdb = require "unpwdb" description = [[ Discovers valid usernames by brute force querying likely usernames against a Kerberos service. When an invalid username is requested the server will respond using the Kerberos error code KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN, allowing us to determine that the user name was invalid. Valid user names will illicit either the TGT in a AS-REP response or the error KRB5KDC_ERR_PREAUTH_REQUIRED, signaling that the user is required to perform pre authentication. The script should work against Active Directory and ? It needs a valid Kerberos REALM in order to operate. ]] --- -- @usage -- nmap -p 88 --script krb5-enum-users --script-args krb5-enum-users.realm='test' -- -- @output -- PORT STATE SERVICE REASON -- 88/tcp open kerberos-sec syn-ack -- | krb5-enum-users: -- | Discovered Kerberos principals -- | administrator@test -- | mysql@test -- |_ tomcat@test -- -- @args krb5-enum-users.realm this argument is required as it supplies the -- script with the Kerberos REALM against which to guess the user names. -- -- -- -- Version 0.1 -- Created 10/16/2011 - v0.1 - created by Patrik Karlsson -- author = "Patrik Karlsson" license = "Same as Nmap--See https://nmap.org/book/man-legal.html" categories = {"auth", "intrusive"} portrule = shortport.port_or_service( 88, {"kerberos-sec"}, {"udp","tcp"}, {"open", "open|filtered"} ) -- This an embryo of a Kerberos 5 packet creation and parsing class. It's very -- tiny class and holds only the necessary functions to support this script. -- This class be factored out into its own library, once more scripts make use -- of it. KRB5 = { -- Valid Kerberos message types MessageType = { ['AS-REQ'] = 10, ['AS-REP'] = 11, ['KRB-ERROR'] = 30, }, -- Some of the used error messages ErrorMessages = { ['KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN'] = 6, ['KRB5KDC_ERR_PREAUTH_REQUIRED'] = 25, ['KDC_ERR_WRONG_REALM'] = 68, }, -- A list of some ot the encryption types EncryptionTypes = { { ['aes256-cts-hmac-sha1-96'] = 18 }, { ['aes128-cts-hmac-sha1-96'] = 17 }, { ['des3-cbc-sha1'] = 16 }, { ['rc4-hmac'] = 23 }, -- { ['des-cbc-crc'] = 1 }, -- { ['des-cbc-md5'] = 3 }, -- { ['des-cbc-md4'] = 2 } }, -- A list of principal name types NameTypes = { ['NT-PRINCIPAL'] = 1, ['NT-SRV-INST'] = 2, }, -- Creates a new Krb5 instance -- @return o as the new instance new = function(self) local o = {} setmetatable(o, self) self.__index = self return o end, -- A number of custom ASN1 decoders needed to decode the response tagDecoder = { ["\x18"] = function( self, encStr, elen, pos ) return string.unpack("c" .. elen, encStr, pos) end, ["\x1B"] = function( ... ) return KRB5.tagDecoder["\x18"](...) end, ["\x6B"] = function( self, encStr, elen, pos ) return self:decodeSeq(encStr, elen, pos) end, -- Not really sure what these are, but they all decode sequences ["\x7E"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA0"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA1"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA2"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA3"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA4"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA5"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA6"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA7"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA8"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xA9"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xAA"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, ["\xAC"] = function( ... ) return KRB5.tagDecoder["\x6B"](...) end, }, -- A few Kerberos ASN1 encoders tagEncoder = { ['table'] = function(self, val) local types = { ['GeneralizedTime'] = 0x18, ['GeneralString'] = 0x1B, } local len = asn1.ASN1Encoder.encodeLength(#val[1]) if ( val._type and types[val._type] ) then return string.pack("B", types[val._type]) .. len .. val[1] elseif ( val._type and 'number' == type(val._type) ) then return string.pack("B", val._type) .. len .. val[1] end end, }, -- Encodes a sequence using a custom type -- @param encoder class containing an instance of a ASN1Encoder -- @param seqtype number the sequence type to encode -- @param seq string containing the sequence to encode encodeSequence = function(self, encoder, seqtype, seq) return encoder:encode( { _type = seqtype, seq } ) end, -- Encodes a Kerberos Principal -- @param encoder class containing an instance of ASN1Encoder -- @param name_type number containing a valid Kerberos name type -- @param names table containing a list of names to encode -- @return princ string containing an encoded principal encodePrincipal = function(self, encoder, name_type, names ) local princ = {} for i, n in ipairs(names) do princ[i] = encoder:encode( { _type = 'GeneralString', n } ) end princ = self:encodeSequence(encoder, 0x30, table.concat(princ)) princ = self:encodeSequence(encoder, 0xa1, princ) princ = encoder:encode( name_type ) .. princ -- not sure about how this works, but apparently it does princ = stdnse.fromhex( "A003") .. princ princ = self:encodeSequence(encoder,0x30, princ) return princ end, -- Encodes the Kerberos AS-REQ request -- @param realm string containing the Kerberos REALM -- @param user string containing the Kerberos principal name -- @param protocol string containing either of "tcp" or "udp" -- @return data string containing the encoded request encodeASREQ = function(self, realm, user, protocol) assert(protocol == "tcp" or protocol == "udp", "Protocol has to be either \"tcp\" or \"udp\"") local encoder = asn1.ASN1Encoder:new() encoder:registerTagEncoders(KRB5.tagEncoder) local data = {} -- encode encryption types for _,enctype in ipairs(KRB5.EncryptionTypes) do for k, v in pairs( enctype ) do data[#data+1] = encoder:encode(v) end end data = self:encodeSequence(encoder, 0x30, table.concat(data) ) data = self:encodeSequence(encoder, 0xA8, data ) -- encode nonce local nonce = 155874945 data = self:encodeSequence(encoder, 0xA7, encoder:encode(nonce) ) .. data -- encode from/to local fromdate = os.time() + 10 * 60 * 60 local from = os.date("%Y%m%d%H%M%SZ", fromdate) data = self:encodeSequence(encoder, 0xA5, encoder:encode( { from, _type='GeneralizedTime' })) .. data local names = { "krbtgt", realm } local sname = self:encodePrincipal( encoder, KRB5.NameTypes['NT-SRV-INST'], names ) sname = self:encodeSequence(encoder, 0xA3, sname) data = sname .. data -- realm data = self:encodeSequence(encoder, 0xA2, encoder:encode( { _type = 'GeneralString', realm })) .. data local cname = self:encodePrincipal(encoder, KRB5.NameTypes['NT-PRINCIPAL'], { user }) cname = self:encodeSequence(encoder, 0xA1, cname) data = cname .. data -- forwardable local kdc_options = 0x40000000 data = string.pack(">I4", kdc_options) .. data -- add padding data = '\0' .. data -- hmm, wonder what this is data = stdnse.fromhex( "A0070305") .. data data = self:encodeSequence(encoder, 0x30, data) data = self:encodeSequence(encoder, 0xA4, data) data = self:encodeSequence(encoder, 0xA2, encoder:encode(KRB5.MessageType['AS-REQ'])) .. data local pvno = 5 data = self:encodeSequence(encoder, 0xA1, encoder:encode(pvno) ) .. data data = self:encodeSequence(encoder, 0x30, data) data = self:encodeSequence(encoder, 0x6a, data) if ( protocol == "tcp" ) then data = string.pack(">s4", data) end return data end, -- Parses the result from the AS-REQ -- @param data string containing the raw unparsed data -- @return status boolean true on success, false on failure -- @return msg table containing the fields type and -- error_code if the type is an error. parseResult = function(self, data) local decoder = asn1.ASN1Decoder:new() decoder:registerTagDecoders(KRB5.tagDecoder) decoder:setStopOnError(true) local result = decoder:decode(data) local msg = {} if ( #result == 0 or #result[1] < 2 or #result[1][2] < 1 ) then return false, nil end msg.type = result[1][2][1] if ( msg.type == KRB5.MessageType['KRB-ERROR'] ) then if ( #result[1] < 5 and #result[1][5] < 1 ) then return false, nil end msg.error_code = result[1][5][1] return true, msg elseif ( msg.type == KRB5.MessageType['AS-REP'] ) then return true, msg end return false, nil end, } -- Checks whether the user exists or not -- @param host table as received by the action method -- @param port table as received by the action method -- @param realm string containing the Kerberos REALM -- @param user string containing the Kerberos principal -- @return status boolean, true on success, false on failure -- @return state VALID or INVALID or error message if status was false local function checkUser( host, port, realm, user ) local krb5 = KRB5:new() local data = krb5:encodeASREQ(realm, user, port.protocol) local socket = nmap.new_socket() local status = socket:connect(host, port) if ( not(status) ) then return false, "ERROR: Failed to connect to Kerberos service" end socket:send(data) status, data = socket:receive() if ( port.protocol == 'tcp' ) then data = data:sub(5) end if ( not(status) ) then return false, "ERROR: Failed to receive result from Kerberos service" end socket:close() local msg status, msg = krb5:parseResult(data) if ( not(status) ) then return false, "ERROR: Failed to parse the result returned from the Kerberos service" end if ( msg and msg.error_code ) then if ( msg.error_code == KRB5.ErrorMessages['KRB5KDC_ERR_PREAUTH_REQUIRED'] ) then return true, "VALID" elseif ( msg.error_code == KRB5.ErrorMessages['KDC_ERR_WRONG_REALM'] ) then return false, "Invalid Kerberos REALM" end elseif ( msg.type == KRB5.MessageType['AS-REP'] ) then return true, "VALID" end return true, "INVALID" end -- Checks whether the Kerberos REALM exists or not -- @param host table as received by the action method -- @param port table as received by the action method -- @param realm string containing the Kerberos REALM -- @return status boolean, true on success, false on failure local function isValidRealm( host, port, realm ) return checkUser( host, port, realm, "nmap") end -- Wraps the checkUser function so that it is suitable to be called from -- a thread. Adds a user to the result table in case it's valid. -- @param host table as received by the action method -- @param port table as received by the action method -- @param realm string containing the Kerberos REALM -- @param user string containing the Kerberos principal -- @param result table to which all discovered users are added local function checkUserThread( host, port, realm, user, result ) local condvar = nmap.condvar(result) local status, state = checkUser(host, port, realm, user) if ( status and state == "VALID" ) then table.insert(result, ("%s@%s"):format(user,realm)) end condvar "signal" end local function fail (err) return stdnse.format_output(false, err) end action = function( host, port ) local realm = stdnse.get_script_args("krb5-enum-users.realm") local result = {} local condvar = nmap.condvar(result) -- did the user supply a realm if ( not(realm) ) then return fail("No Kerberos REALM was supplied, aborting ...") end -- does the realm appear to exist if ( not(isValidRealm(host, port, realm)) ) then return fail("Invalid Kerberos REALM, aborting ...") end -- load our user database from unpwdb local status, usernames = unpwdb.usernames() if( not(status) ) then return fail("Failed to load unpwdb usernames") end -- start as many threads as there are names in the list local threads = {} for user in usernames do local co = stdnse.new_thread( checkUserThread, host, port, realm, user, result ) threads[co] = true end -- wait for all threads to finish up repeat for t in pairs(threads) do if ( coroutine.status(t) == "dead" ) then threads[t] = nil end end if ( next(threads) ) then condvar "wait" end until( next(threads) == nil ) if ( #result > 0 ) then result = { name = "Discovered Kerberos principals", result } end return stdnse.format_output(true, result) end