summaryrefslogtreecommitdiffstats
path: root/scripts/krb5-enum-users.nse
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/krb5-enum-users.nse')
-rw-r--r--scripts/krb5-enum-users.nse404
1 files changed, 404 insertions, 0 deletions
diff --git a/scripts/krb5-enum-users.nse b/scripts/krb5-enum-users.nse
new file mode 100644
index 0000000..05668eb
--- /dev/null
+++ b/scripts/krb5-enum-users.nse
@@ -0,0 +1,404 @@
+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 <patrik@cqure.net>
+--
+
+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 <code>type</code> and
+ -- <code>error_code</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