@@ -109,7 +109,7 @@ def run_host(_ip)
109109 print_status ( "Searching base DN: #{ base_dn } " )
110110 entries_returned += ldap_search ( ldap , base_dn , base : base_dn )
111111 unless @ad_ds_domain_info . nil?
112- attributes = %w[ dn sAMAccountName msDS-ManagedPassword ]
112+ attributes = %w[ dn msDS-ManagedPassword sAMAccountName ]
113113 attributes << datastore [ 'USER_ATTR' ] unless datastore [ 'USER_ATTR' ] . blank? || attributes . include? ( datastore [ 'USER_ATTR' ] )
114114 entries_returned += ldap_search ( ldap , base_dn , base : base_dn , filter : '(objectClass=msDS-GroupManagedServiceAccount)' , attributes : attributes )
115115 end
@@ -142,7 +142,7 @@ def ldap_search(ldap, base_dn, args)
142142 entries_returned += 1
143143 password_attributes . each do |attr |
144144 if entry [ attr ] . any?
145- creds_found += process_hash ( entry , attr )
145+ creds_found += process_entry ( entry , attr )
146146 end
147147 end
148148 end
@@ -188,7 +188,7 @@ def decode_pwdhistory(hash)
188188 hash
189189 end
190190
191- def process_hash ( entry , attr )
191+ def process_entry ( entry , attr )
192192 creds_found = 0
193193 username = [ datastore [ 'USER_ATTR' ] , 'sAMAccountName' , 'uid' , 'dn' ] . map { entry [ _1 ] } . reject ( &:blank? ) . first . first
194194
@@ -229,61 +229,79 @@ def process_hash(entry, attr)
229229
230230 if attr =~ /^samba(lm|nt)password$/
231231 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*' ) )
233234 end
234235
235236 # observed sambapassword history with either 56 or 64 zeros
236237 next if attr == 'sambapasswordhistory' && private_data =~ /^(0{64}|0{56})$/
237238
238- jtr_format = nil
239- annotation = ''
239+ artifacts = SecretArtifact . new ( public_data : username , private_data : private_data , private_type : :password )
240240
241241 case attr
242242 when 'sambalmpassword'
243- jtr_format = 'lm'
243+ artifacts . jtr_format = 'lm'
244+ artifacts . private_type = :nonreplayable_hash
244245 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 } "
246249 when 'sambapasswordhistory'
247250 # 795471346779677A336879366B654870 1F18DC5E346FDA5E335D9AE207C82CC9
248251 # where the left part is a salt and the right part is MD5(Salt+NTHash)
249252 # attribute value may contain multiple concatenated history entries
250253 # 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
252256 when 'krbprincipalkey'
253- jtr_format = 'krbprincipal'
257+ artifacts . jtr_format = 'krbprincipal'
258+ artifacts . private_type = :nonreplayable_hash
254259 # TODO: krbprincipalkey is asn.1 encoded string. In case of vmware vcenter 6.7
255260 # it contains user password encrypted with (23) rc4-hmac and (18) aes256-cts-hmac-sha1-96:
256261 # https://github.com/vmware/lightwave/blob/d50d41edd1d9cb59e7b7cc1ad284b9e46bfa703d/vmdir/server/common/krbsrvutil.c#L480-L558
257262 # Salted with principal name:
258263 # https://github.com/vmware/lightwave/blob/c4ad5a67eedfefe683357bc53e08836170528383/vmdir/thirdparty/heimdal/krb5-crypto/salt.c#L133-L175
259264 # 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 )
261266 when 'ms-mcs-admpwd'
262267 # 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?
265270 when 'msds-managedpassword'
266271 managed_password = MsdsManagedpasswordBlob . read ( private_data )
267272 current_password = managed_password . buffer_fields [ :current_password ] # this field should always be present
268273 if current_password && ( domain_dns_name = @ad_ds_domain_info &.fetch ( :dns_name ) )
274+ artifacts = [ ]
269275 sam_account_name = entry [ :sAMAccountName ] . first . to_s
276+
270277 salt = "#{ domain_dns_name . upcase } host#{ sam_account_name . delete_suffix ( '$' ) . downcase } .#{ domain_dns_name . downcase } "
271278 encoded_current_password = current_password . force_encoding ( 'UTF-16LE' ) . encode ( 'UTF-8' , invalid : :replace , undef : :replace ) . force_encoding ( 'ASCII-8BIT' )
272279
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+ )
287305 end
288306 when 'mslaps-password'
289307 begin
@@ -294,34 +312,35 @@ def process_hash(entry, attr)
294312 next
295313 end
296314
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?
300318 when 'mslaps-encryptedpassword'
301319 lapsv2 = process_result_lapsv2_encrypted ( entry )
302320 next if lapsv2 . nil?
303321
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?
307325 when 'userpkcs12'
308- # if we get non printable chars, encode into base64
326+ # if we get non- printable chars, encode into base64
309327 if ( private_data =~ /[^[:print:]]/ ) . nil?
310- jtr_format = 'pkcs12'
328+ artifacts . jtr_format = 'pkcs12'
311329 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 )
314332 end
333+ artifacts . private_type = :nonreplayable_hash
315334 else
316335 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'
319338 elsif private_data . start_with? ( /{crypt}/i ) && private_data . length == 20
320339 # 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 )
322341 # FIXME: what is the right jtr_hash - des,crypt or descrypt ?
323342 # identify_hash returns des,crypt, while JtR acceppts descrypt
324- jtr_format = 'descrypt'
343+ artifacts . jtr_format = 'descrypt'
325344 # TODO: not sure if we shall slice the prefixes here or in the JtR/Hashcat formatter
326345 # elsif hash.start_with?(/{sha256}/i)
327346 # hash.slice!(/{sha256}/i)
@@ -330,27 +349,51 @@ def process_hash(entry, attr)
330349 # handle vcenter vmdir binary hash format
331350 if private_data [ 0 ] . ord == 1 && private_data . length == 81
332351 _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 } "
334353 else
335354 # 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' )
337356 end
338- jtr_format = Metasploit ::Framework ::Hashes . identify_hash ( private_data )
357+ artifacts . jtr_format = Metasploit ::Framework ::Hashes . identify_hash ( artifacts . private_data )
339358 end
359+ artifacts . private_type = :nonreplayable_hash
340360 end
341361
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
345387
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
347390 creds_found += 1
348391 end
349392
350393 creds_found
351394 end
352395
353- def report_creds ( username , private_data , jtr_format )
396+ def report_creds ( username , private_data , private_type , jtr_format )
354397 # this is the service the credentials came from, not necessarily where they can be used
355398 service_data = {
356399 address : rhost ,
@@ -365,7 +408,7 @@ def report_creds(username, private_data, jtr_format)
365408 origin_type : :service ,
366409 status : Metasploit ::Model ::Login ::Status ::UNTRIED ,
367410 private_data : private_data ,
368- private_type : ( jtr_format . nil? ? :password : :nonreplayable_hash ) ,
411+ private_type : private_type ,
369412 jtr_format : jtr_format ,
370413 username : username
371414 } . merge ( service_data )
@@ -375,8 +418,7 @@ def report_creds(username, private_data, jtr_format)
375418 credential_data [ :realm_value ] = @ad_ds_domain_info [ :dns_name ]
376419 end
377420
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 )
380422 end
381423
382424 def process_result_lapsv2_encrypted ( result )
@@ -450,6 +492,8 @@ def process_result_lapsv2_encrypted(result)
450492 JSON . parse ( RubySMB ::Field ::Stringz16 . read ( plaintext ) . value )
451493 end
452494
495+ SecretArtifact = Struct . new ( :public_data , :private_data , :private_type , :jtr_format , :annotation )
496+
453497 # https://blog.xpnsec.com/lapsv2-internals/#:~:text=msLAPS%2DEncryptedPassword%20attribute
454498 # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/b6ea7b78-64da-48d3-87cb-2cff378e4597
455499 class LAPSv2EncryptedPasswordBlob < BinData ::Record
0 commit comments