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:



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

attr_accessor :kerberos_client

#kerberos_clientObject

Returns the value of attribute kerberos_client.



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

def kerberos_client
  @kerberos_client
end

Instance Method Details

#cleanupObject

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



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

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:



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 132

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

  disconnect if kerberos_client
  self.kerberos_client = kerb_client

  kerb_client
end

#disconnect(kerb_client = kerberos_client) ⇒ Object

Disconnects the Kerberos client

Parameters:



154
155
156
157
158
159
160
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 154

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)



483
484
485
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 483

def framework_module
  self
end

#initialize(info = {}) ⇒ Object



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

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(
    [
      OptString.new('KrbClockSkew', [true, 'Adjust Kerberos client clock by this offset (e.g. 90s, -5m, 1h)', '0s'],
                    regex: Msf::Exploit::Remote::Kerberos::ClockSkew::CLOCK_SKEW_REGEX)
    ], self.class
  )
end

#kerberos_clock_skewFloat

Returns the configured Kerberos clock skew in seconds.

Returns:

  • (Float)


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

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)


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

def kerberos_clock_skew=(value)
  @kerberos_clock_skew = Msf::Exploit::Remote::Kerberos::ClockSkew.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)


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

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)


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

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

#peerString

Returns the kdc peer

Returns:

  • (String)


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

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

#proxiesString?

Returns the configured proxy list

Returns:

  • (String, nil)


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

def proxies
  datastore['Proxies']
end

#rhostString

Returns the target host

Returns:

  • (String)


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

def rhost
  datastore['RHOST']
end

#rportInteger

Returns the remote port

Returns:

  • (Integer)


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

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



204
205
206
207
208
209
210
211
212
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 204

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:



175
176
177
178
179
180
181
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 175

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:



189
190
191
192
193
194
195
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 189

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:



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

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:



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

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)


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

def timeout
  datastore['Timeout']
end