Lucene search

K
nmapPatrik KarlssonNMAP:KRB5-ENUM-USERS.NSE
HistoryOct 20, 2011 - 2:49 a.m.

krb5-enum-users NSE Script

2011-10-2002:49:00
Patrik Karlsson
nmap.org
685

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

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.

Script Arguments

krb5-enum-users.realm

this argument is required as it supplies the script with the Kerberos REALM against which to guess the user names.

passdb, unpwdb.passlimit, unpwdb.timelimit, unpwdb.userlimit, userdb

See the documentation for the unpwdb library.

Example Usage

nmap -p 88 --script krb5-enum-users --script-args krb5-enum-users.realm='test'

Script Output

PORT   STATE SERVICE      REASON
88/tcp open  kerberos-sec syn-ack
| krb5-enum-users:
| Discovered Kerberos principals
|     administrator@test
|     mysql@test
|_    tomcat@test

Requires


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 <[email protected]>
--

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

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.973 High

EPSS

Percentile

99.8%

Related for NMAP:KRB5-ENUM-USERS.NSE