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

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

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:



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

attr_accessor :kerberos_client

#kerberos_clientObject

Returns the value of attribute kerberos_client.



30
31
32
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 30

def kerberos_client
  @kerberos_client
end

Instance Method Details

#cleanupObject

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



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

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:



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 85

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:



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

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)



433
434
435
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 433

def framework_module
  self
end

#initialize(info = {}) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 32

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
  )
end

#peerString

Returns the kdc peer

Returns:

  • (String)


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

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

#proxiesString?

Returns the configured proxy list

Returns:

  • (String, nil)


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

def proxies
  datastore['Proxies']
end

#rhostString

Returns the target host

Returns:

  • (String)


47
48
49
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 47

def rhost
  datastore['RHOST']
end

#rportInteger

Returns the remote port

Returns:

  • (Integer)


54
55
56
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 54

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



158
159
160
161
162
163
164
165
166
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 158

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:



129
130
131
132
133
134
135
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 129

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:



143
144
145
146
147
148
149
# File 'lib/msf/core/exploit/remote/kerberos/client.rb', line 143

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:



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

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 = Time.now.utc
  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.new('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
  raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(res: initial_as_res) if password.nil? && key.nil?

  # 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
      raise ArgumentError.new('Encryption key provided without one offered encryption type') unless options[:offered_etypes]&.length == 1
      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:



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

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 = Time.now.utc
  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 {|entry| entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_PK_AS_REP}
    raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new('No PKINIT PreAuth data received') if entry.nil? # Should never happen from a spec-compliant server

    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.new('Unexpected response type (expected AS_REP or KRB_ERROR)')
  end
end

#timeoutInteger

Returns the TCP timeout

Returns:

  • (Integer)


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

def timeout
  datastore['Timeout']
end