summaryrefslogtreecommitdiffstats
path: root/scripts/krb5-enum-users.nse
blob: 05668eb7e106549bd9b833899e7a29dfb60aeb0f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
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