Module: Msf::Exploit::Remote::Kerberos::Client

Includes:
ApRequest, AsRequest, AsResponse, Base, Pac, Pkinit, TgsRequest, TgsResponse
Included in:
Metasploit::Framework::LoginScanner::Kerberos, AuthBrute, ServiceAuthenticator::Base
Defined in:
lib/msf/core/exploit/remote/kerberos/client.rb,
lib/msf/core/exploit/remote/kerberos/client/pac.rb,
lib/msf/core/exploit/remote/kerberos/client/base.rb,
lib/msf/core/exploit/remote/kerberos/client/pkinit.rb,
lib/msf/core/exploit/remote/kerberos/client/ap_request.rb,
lib/msf/core/exploit/remote/kerberos/client/as_request.rb,
lib/msf/core/exploit/remote/kerberos/client/as_response.rb,
lib/msf/core/exploit/remote/kerberos/client/tgs_request.rb,
lib/msf/core/exploit/remote/kerberos/client/tgs_response.rb

Overview

Kerberos client helpers shared across mixins.

Defined Under Namespace

Modules: ApRequest, AsRequest, AsResponse, Base, Pac, Pkinit, TgsRequest, TgsResponse

Constant Summary collapse

TOK_ID_KRB_AP_REQ =
"\x01\x00"
TOK_ID_KRB_AP_REP =
"\x02\x00"
TOK_ID_KRB_ERROR =
"\x03\x00"
NEG_TOKEN_ACCEPT_COMPLETED =
0
NEG_TOKEN_ACCEPT_INCOMPLETE =
1
NEG_TOKEN_REJECT =
2
NEG_TOKEN_REQUEST_MIC =
3

Constants included from ApRequest

ApRequest::AP_MUTUAL_REQUIRED, ApRequest::AP_USE_SESSION_KEY

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Pkinit

#build_dh, #build_pa_pk_as_req, #calculate_shared_key, #extract_user_and_realm, #k_truncate, #sign_auth_pack

Methods included from Pac

#build_empty_auth_data, #build_pa_pac_request, #build_pac, #build_pac_authorization_data

Methods included from TgsResponse

#decrypt_kdc_tgs_rep_enc_part, #extract_kerb_creds, #format_tgs_rep_to_john_hash

Methods included from TgsRequest

#build_ap_req, #build_authenticator, #build_enc_auth_data, #build_pa_for_user, #build_subkey, #build_tgs_body_checksum, #build_tgs_request, #build_tgs_request_body

Methods included from AsResponse

#decrypt_kdc_as_rep_enc_part, #extract_logon_time, #extract_session_key, #format_as_rep_to_john_hash

Methods included from AsRequest

#build_as_pa_time_stamp, #build_as_request, #build_as_request_body

Methods included from ApRequest

#build_service_ap_request, #encode_gss_kerberos_ap_request, #encode_gss_spnego_ap_request

Methods included from Base

#build_client_name, #build_server_name

Instance Attribute Details

#clientRex::Proto::Kerberos::Client

Returns The kerberos client.

Returns:



34
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 34

attr_accessor :kerberos_client

#kerberos_clientObject

Returns the value of attribute kerberos_client.



34
35
36
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 34

def kerberos_client
  @kerberos_client
end

Instance Method Details

#cleanupObject

Performs cleanup as necessary, disconnecting the Kerberos client if it’s still established.



173
174
175
176
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 173

def cleanup
  super
  disconnect
end

#connect(opts = {}) ⇒ Rex::Proto::Kerberos::Client

Creates a kerberos connection

Parameters:

  • opts (Hash{Symbol => <String, Integer>}) (defaults to: {})

Options Hash (opts):

  • :rhost (String)
  • :rport (<String, Integer>)

Returns:



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
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 134

def connect(opts = {})
  has_session = defined?(session) && session
  remote_host = has_session ? session.client.peerhost : rhost
  # Can't use session.client.rport as a fallback here with an LDAP session as that's port 389. We need port 88.
  remote_port = has_session ? 88 : rport
  subscriber = opts.key?(:subscriber) ? opts[:subscriber] : kerberos_trace_subscriber

  kerb_client = Rex::Proto::Kerberos::Client.new(
    host: opts[:rhost] || remote_host,
    port: (opts[:rport] || remote_port).to_i,
    proxies: opts[:proxies] || proxies,
    timeout: (opts[:timeout] || timeout).to_i,
    context: {
      'Msf' => framework,
      'MsfExploit' => framework_module
    },
    protocol: 'tcp',
    subscriber: subscriber
  )

  disconnect if kerberos_client
  self.kerberos_client = kerb_client

  kerb_client
end

#disconnect(kerb_client = kerberos_client) ⇒ Object

Disconnects the Kerberos client

Parameters:



163
164
165
166
167
168
169
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 163

def disconnect(kerb_client = kerberos_client)
  kerb_client.close if kerb_client

  if kerb_client == kerberos_client
    self.kerberos_client = nil
  end
end

#framework_moduleObject (protected)



492
493
494
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 492

def framework_module
  self
end

#initialize(info = {}) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 36

def initialize(info = {})
  super

  register_options(
    [
      Opt::RHOST,
      Opt::RPORT(88),
      OptInt.new('Timeout', [true, 'The TCP timeout to establish Kerberos connection and read data', 10])
    ], self.class
  )

  register_advanced_options(
    [
      OptTimedelta.new('KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s']),
      OptBool.new('KerberosTicketTrace', [false, 'Show AS/TGS/AP Kerberos requests and responses', false]),
      OptString.new('KerberosTicketTraceColors', [false, 'Kerberos request and response colors for KerberosTicketTrace (unset to disable)', 'red/blu'])
    ], self.class
  )
end

#kerberos_clock_skewFloat

Returns the configured Kerberos clock skew in seconds.

Returns:

  • (Float)


80
81
82
83
84
85
86
87
88
89
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 80

def kerberos_clock_skew
  return @kerberos_clock_skew if instance_variable_defined?(:@kerberos_clock_skew) && !@kerberos_clock_skew.nil?

  if respond_to?(:datastore) && datastore
    self.kerberos_clock_skew = datastore['KrbClockSkew']
  else
    self.kerberos_clock_skew = 0
  end
  @kerberos_clock_skew
end

#kerberos_clock_skew=(value) ⇒ Object

Sets the Kerberos clock skew.

Parameters:

  • value (String, Numeric, nil)


94
95
96
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 94

def kerberos_clock_skew=(value)
  @kerberos_clock_skew = Msf::OptTimedelta.parse(value)
end

#kerberos_time(base_time = Time.now.utc) ⇒ Time

Returns the current time adjusted for Kerberos clock skew in UTC.

Parameters:

  • base_time (Time) (defaults to: Time.now.utc)

    base time to adjust

Returns:

  • (Time)


102
103
104
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 102

def kerberos_time(base_time = Time.now.utc)
  (base_time + kerberos_clock_skew).utc
end

#kerberos_time_local(base_time = Time.now) ⇒ Time

Returns the current time adjusted for Kerberos clock skew in the local timezone.

Parameters:

  • base_time (Time) (defaults to: Time.now)

    base time to adjust

Returns:

  • (Time)


110
111
112
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 110

def kerberos_time_local(base_time = Time.now)
  base_time + kerberos_clock_skew
end

#kerberos_trace_subscriberObject (protected)



496
497
498
499
500
501
502
503
504
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 496

def kerberos_trace_subscriber
  logger = framework_module

  if logger.respond_to?(:print_line) && logger.respond_to?(:datastore)
    Rex::Proto::Kerberos::KerberosLoggerSubscriber.new(logger: logger)
  else
    Rex::Proto::Kerberos::KerberosSubscriber.new
  end
end

#peerString

Returns the kdc peer

Returns:

  • (String)


117
118
119
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 117

def peer
  "#{rhost}:#{rport}"
end

#proxiesString?

Returns the configured proxy list

Returns:

  • (String, nil)


124
125
126
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 124

def proxies
  datastore['Proxies']
end

#rhostString

Returns the target host

Returns:

  • (String)


59
60
61
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 59

def rhost
  datastore['RHOST']
end

#rportInteger

Returns the remote port

Returns:

  • (Integer)


66
67
68
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 66

def rport
  datastore['RPORT']
end

#select_cipher(client_etypes, server_etypeinfos_entries) ⇒ Rex::Proto::Kerberos::Model::EtypeInfo

Select a cipher that both the server and client support, preferencing ours in order. This may just be the default behaviour on Windows, but let’s be sure about it.

Parameters:

  • client_etypes (Array<Integer>)

    Available ciphers on the client side (etypes from Rex::Proto::Kerberos::Crypto::Encryption)

  • server_etypeinfos_entries (Array<Rex::Proto::Kerberos::Model::PreAuthEtypeInfo2Entry>)

    Available ciphers (including additional info such as salts) on the server

Returns:

  • (Rex::Proto::Kerberos::Model::EtypeInfo)

    The selected cipher



213
214
215
216
217
218
219
220
221
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 213

def select_cipher(client_etypes, server_etypeinfos_entries)
  client_etypes.each do |client_etype|
    server_etypeinfos_entries.each do |server_etypeinfo2_entry|
      if server_etypeinfo2_entry.etype == client_etype
        return server_etypeinfo2_entry
      end
    end
  end
end

#send_request_as(opts = {}) ⇒ Rex::Proto::Kerberos::Model::KdcResponse

Sends a kerberos AS request and reads the response

Parameters:

  • opts (Hash) (defaults to: {})

Returns:

See Also:



184
185
186
187
188
189
190
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 184

def send_request_as(opts = {})
  connect(opts)
  req = opts.fetch(:req) { build_as_request(opts) }
  res = kerberos_client.send_recv(req)
  disconnect
  res
end

#send_request_tgs(opts = {}) ⇒ Rex::Proto::Kerberos::Model::KdcResponse

Sends a kerberos TGS request and reads the response

Parameters:

  • opts (Hash) (defaults to: {})

Returns:

See Also:



198
199
200
201
202
203
204
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 198

def send_request_tgs(opts = {})
  connect(opts)
  req = opts.fetch(:req) { build_tgs_request(opts) }
  res = kerberos_client.send_recv(req)
  disconnect
  res
end

#send_request_tgt(options = {}) ⇒ Msf::Exploit::Remote::Kerberos::Model::TgtResponse

Sends the required kerberos AS requests for a kerberos Ticket Granting Ticket

Parameters:

  • options (Hash) (defaults to: {})

Returns:

Raises:



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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 302

def send_request_tgt(options = {})
  realm = options[:realm]
  server_name = options[:server_name] || "krbtgt/#{realm}"
  client_name = options[:client_name]
  client_name = client_name.dup.force_encoding('utf-8') if client_name
  password = options[:password]
  password = password.dup.force_encoding('utf-8') if password
  key = options[:key]
  request_pac = options.fetch(:request_pac, true)
  ticket_options = options.fetch(:options, 0x50800000) # Forwardable, Proxiable, Renewable

  # First stage: Send an initial AS-REQ request, used to exchange supported encryption methods.
  # The server may respond with a ticket granting ticket (TGT) immediately,
  # or the client may require preauthentication, and a second AS-REQ is required

  now = kerberos_time
  expiry_time = now + 1.day

  offered_etypes = options[:offered_etypes] || Rex::Proto::Kerberos::Crypto::Encryption::DefaultOfferedEtypes
  if !password && key && offered_etypes.length != 1
    raise ArgumentError, 'Exactly one etype must be specified in :offered_etypes when a key is is defined without a password'
  end

  initial_as_req = build_as_request(
    pa_data: [
      build_pa_pac_request(pac_request_value: request_pac)
    ],
    body: build_as_request_body(
      client_name: client_name,
      server_name: server_name,
      realm: realm,

      etype: offered_etypes,

      # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket
      from: nil,
      till: expiry_time,
      rtime: expiry_time,
      options: ticket_options
    )
  )

  req_opts = { req: initial_as_req }
  req_opts.update(options)
  initial_as_res = send_request_as(req_opts)

  # If we receive an AS_REP response immediately, no-preauthentication was required and we can return immediately
  if initial_as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP
    pa_data = initial_as_res.pa_data
    if password.nil? && key.nil?
      decrypted_part = nil
      krb_enc_key = nil
    else
      etype_entries = pa_data.find { |entry| entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_ETYPE_INFO2 }

      # Let's try to check the password
      server_ciphers = etype_entries.decoded_value
      # Should only have one etype
      etype_info = server_ciphers.etype_info2_entries[0]
      if password
        enc_key, salt = get_enc_key_from_password(password, etype_info)
      elsif key
        enc_key = key
      end
      begin
        decrypted_part = decrypt_kdc_as_rep_enc_part(initial_as_res, enc_key)
        krb_enc_key = {
          enctype: etype_info.etype,
          key: enc_key,
          salt: salt
        }
      rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError
        # It's as if it were an invalid password
        decrypted_part = nil
        krb_enc_key = nil
      end
    end

    return Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
      as_rep: initial_as_res,
      preauth_required: false,
      decrypted_part: decrypted_part,
      krb_enc_key: krb_enc_key
    )
  end

  # If we're just AS_REP Roasting, we can't go any further
  if password.nil? && key.nil?
    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: initial_as_res)
  end

  # Verify error codes. Anything other than the server requiring an additional preauth request is considered a failure.
  if initial_as_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR && initial_as_res.error_code != Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_PREAUTH_REQUIRED
    if initial_as_res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_ETYPE_NOSUPP
      raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new(encryption_type: offered_etypes)
    end

    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: initial_as_res)
  end

  # Second stage: Send an additional AS-REQ request with preauthentication provided
  # Note that Clock skew issues may be raised at this point

  pa_data = initial_as_res.e_data_as_pa_data
  etype_entries = pa_data.find { |entry| entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_ETYPE_INFO2 }

  # No etypes specified - how are we supposed to negotiate ciphers?
  raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new(encryption_type: offered_etypes) unless etype_entries

  server_ciphers = etype_entries.decoded_value
  remaining_server_ciphers_to_attempt = server_ciphers.etype_info2_entries.select do |server_etypeinfo2_entry|
    offered_etypes.include?(server_etypeinfo2_entry.etype)
  end

  if remaining_server_ciphers_to_attempt.empty?
    raise Rex::Proto::Kerberos::Model::Error::KerberosEncryptionNotSupported.new(encryption_type: offered_etypes)
  end

  # Attempt to use the available ciphers; In some scenarios they can fail due to GPO configurations
  # So we need to iterate until a success - or there's no more ciphers available
  while remaining_server_ciphers_to_attempt.any?
    selected_etypeinfo = select_cipher(offered_etypes, remaining_server_ciphers_to_attempt)
    selected_etype = selected_etypeinfo.etype

    if password
      enc_key, salt = get_enc_key_from_password(password, selected_etypeinfo)
    elsif key
      unless options[:offered_etypes]&.length == 1
        raise ArgumentError, 'Encryption key provided without one offered encryption type'
      end

      enc_key = key

    end

    preauth_as_req = build_as_request(
      pa_data: [
        build_as_pa_time_stamp(key: enc_key, etype: selected_etype),
        build_pa_pac_request(pac_request_value: request_pac)
      ],
      body: build_as_request_body(
        client_name: client_name,
        server_name: server_name,
        realm: realm,
        key: enc_key,

        etype: remaining_server_ciphers_to_attempt.map(&:etype),

        # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket
        from: nil,
        till: expiry_time,
        rtime: expiry_time
      )
    )

    req_opts = { req: preauth_as_req }
    req_opts.update(options)
    preauth_as_res = send_request_as(req_opts)

    # If we've succeeded - break out of trying ciphers
    break if preauth_as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP

    # If we've hit a cipher not supported error, try the next cipher if there's more to try
    is_etype_not_supported_error = preauth_as_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR && preauth_as_res.error_code == Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_ETYPE_NOSUPP
    if is_etype_not_supported_error
      remaining_server_ciphers_to_attempt -= [selected_etypeinfo]
      next if remaining_server_ciphers_to_attempt.any?
    end

    # Unexpected server response
    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: preauth_as_res)
  end

  Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
    as_rep: preauth_as_res,
    preauth_required: true,
    krb_enc_key: {
      enctype: selected_etype,
      key: enc_key,
      salt: salt
    },
    decrypted_part: decrypt_kdc_as_rep_enc_part(
      preauth_as_res,
      enc_key
    )
  )
end

#send_request_tgt_pkinit(options = {}) ⇒ Msf::Exploit::Remote::Kerberos::Model::TgtResponse

Send a TGT request using PKINIT (certificate) authentication

Parameters:

  • options (Hash) (defaults to: {})
  • [OpenSSL::PKCS12] (Hash)

    a customizable set of options

  • [Boolean] (Hash)

    a customizable set of options

  • [String] (Hash)

    a customizable set of options

  • [Array<Integer>] (Hash)

    a customizable set of options

Returns:



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
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 233

def send_request_tgt_pkinit(options = {})
  pfx = options[:pfx]
  request_pac = options.fetch(:request_pac, true)
  realm = options[:realm]
  server_name = options[:server_name] || "krbtgt/#{realm}"
  client_name = options[:client_name]
  client_name = client_name.dup.force_encoding('utf-8') if client_name
  ticket_options = options.fetch(:options, 0x50800000) # Forwardable, Proxiable, Renewable

  # The diffie hellman client parameters
  dh, dh_nonce = build_dh

  now = kerberos_time
  expiry_time = now + 1.day
  offered_etypes = options[:offered_etypes] || Rex::Proto::Kerberos::Crypto::Encryption::PkinitEtypes
  request_body = build_as_request_body(
    client_name: client_name,
    server_name: server_name,
    realm: realm,

    etype: offered_etypes,

    # Specify nil to ensure the KDC uses the current time for the desired starttime of the requested ticket
    from: nil,
    till: expiry_time,
    rtime: expiry_time,
    options: ticket_options
  )
  as_req = build_as_request(
    pa_data: [
      build_pa_pac_request(pac_request_value: request_pac),
      build_pa_pk_as_req(pfx, dh, dh_nonce, request_body, options)
    ],
    body: request_body
  )

  # Send the request
  options[:req] = as_req
  as_res = send_request_as(options)

  if as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP
    entry = as_res.pa_data.find { |data_entry| data_entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_PK_AS_REP }
    # Should never happen from a spec-compliant server
    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'No PKINIT PreAuth data received' if entry.nil?

    pa_pk_as_rep = entry.decoded_value
    key = calculate_shared_key(pa_pk_as_rep, dh, dh_nonce, as_res.enc_part.etype)
    return Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
      as_rep: as_res,
      preauth_required: true,
      decrypted_part: decrypt_kdc_as_rep_enc_part(as_res, key),
      krb_enc_key: {
        enctype: as_res.enc_part.etype,
        key: key
      }
    )
  elsif as_res.msg_type == Rex::Proto::Kerberos::Model::KRB_ERROR
    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: as_res)
  else
    # Should never happen, per the spec
    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'Unexpected response type (expected AS_REP or KRB_ERROR)'
  end
end

#timeoutInteger

Returns the TCP timeout

Returns:

  • (Integer)


73
74
75
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 73

def timeout
  datastore['Timeout']
end