Skip to content

ESC9 and ESC10 detection for ldap_esc_vulnerable_cert_finder #20149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,50 @@ a normal user account by analyzing the objects in LDAP.
1. Scroll down and select the `ESC3-Template2` certificate, and select `OK`.
1. The certificate should now be available to be issued by the CA server.

### Setting up a ESC9 Vulnerable Certificate Template
1. Open up the run prompt and type in `certsrv`.
1. In the window that appears you should see your list of certification authorities under `Certification Authority (Local)`.
1. Right click on the folder in the drop down marked `Certificate Templates` and then click `Manage`.
1. Scroll down to the `User` certificate. Right click on it and select `Duplicate Template`.
1. The `User` certificate already has the `Client Authentication` EKU enabled so we can use this as a base template.
1. Select the `General` tab and rename this to something meaningful like `ESC9-Template`, then click the `Apply` button.
1. Select the Security tab and click the `Add` button.
1. Enter `user2` (or whatever user's UPN you will be changing for this attack). Click OK.
1. Under Permissions for `user2` select `Allow` for `Enroll` and `Read`.
1. Click `Apply` and then `OK`.
1. Open Active Directory Users and Computers, expand the domain on the left hand side.
1. Right click `Users` and navigate `user2` and select `Properties`.
1. In the security tab, select `Add` and enter `user1` (or whatever user you will be using to perform the attack). Click OK.
1. Under Permissions for `user1` select `Allow` for `Read` and `Write` (or select `Allow` for `Full Control`).
1. Open a Powershell prompt as Administrator and run the following (change `kerberos.issue` to your domain name):
```powershell
$template = [ADSI]"LDAP://CN=ESC9-Template,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=kerberos,DC=issue"
$template.Put("msPKI-Enrollment-Flag", 0x80000)
$template.SetInfo()
```
#### Configuring Windows to be Vulnerable to ESC9
1. The template should now be reported as `Potentially Vulnerable` by the module.
1. In order to be able to exploit this template run the following Powershell command and ensure `StrongCertificateBindingEnforcement` is not set to `2` (it should be 1, or 0):
```powershell
Get-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Services\Kdc\" -Name StrongCertificateBindingEnforcement
```

### Setting up a ESC10 Vulnerable Certificate Template
1. Follow the first 13 steps `Setting up a ESC9 Vulnerable Certificate Template` to create the `ESC10-Template`.
1. Everything up to and excluding the `msPKI-Enrollment-Flag", 0x80000` powershell step.
#### Configuring Windows to be Vulnerable to ESC10
1. The template should now be reported as `Potentially Vulnerable` by the module.
##### ESC10 Case1:
1. In order to be able to exploit this template run the following Powershell command and ensure `StrongCertificateBindingEnforcement` is set to `0`
```powershell
Get-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Services\Kdc\" -Name StrongCertificateBindingEnforcement
```
##### ESC10 Case2:
1. In order to be able to exploit this template run the following Powershell command and ensure `CertificateMappingMethods` is set to `0x4`
```powershell
Get-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel\" -Name CertificateMappingMethods
```

### Setting up a ESC13 Vulnerable Certificate Template
1. Follow the instructions above to duplicate the ESC2 template and name it `ESC13`, then click `Apply`.
1. Go to the `Extensions` tab, click the Issuance Policies entry, click the `Add` button, click the `New...` button.
Expand Down
3 changes: 3 additions & 0 deletions lib/rex/proto/crypto_asn1/o_i_ds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class OIDs
OID_AES192_CCM = ObjectId.new('2.16.840.1.101.3.4.1.27', name: 'OID_AES192_CCM', label: 'AES192 in CCM mode')
OID_AES256_GCM = ObjectId.new('2.16.840.1.101.3.4.1.46', name: 'OID_AES256_GCM', label: 'AES256 in GCM mode')
OID_AES256_CCM = ObjectId.new('2.16.840.1.101.3.4.1.47', name: 'OID_AES256_CCM', label: 'AES256 in CCM mode')

# https://oidref.com/2.5.29.37.0
OID_ANY_EXTENDED_KEY_USAGE = ObjectId.new('2.5.29.37.0', name: 'OID_ANY_EXTENDED_KEY_USAGE', label: 'Any Extended Key Usage')

def self.name(value)
value = ObjectId.new(value) if value.is_a?(String)
Expand Down
206 changes: 193 additions & 13 deletions modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class MetasploitModule < Msf::Auxiliary
include Rex::Proto::MsDnsp
include Rex::Proto::Secauthz
include Rex::Proto::LDAP
include Rex::Proto::CryptoAsn1

class LdapWhoamiError < StandardError; end

ADS_GROUP_TYPE_BUILTIN_LOCAL_GROUP = 0x00000001
ADS_GROUP_TYPE_GLOBAL_GROUP = 0x00000002
Expand All @@ -18,6 +21,8 @@ class MetasploitModule < Msf::Auxiliary
'ESC2' => [ SiteReference.new('URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2') ],
'ESC3' => [ SiteReference.new('URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2') ],
'ESC4' => [ SiteReference.new('URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2') ],
'ESC9' => [ SiteReference.new('URL', 'https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7') ],
'ESC10' => [ SiteReference.new('URL', 'https://research.ifcr.dk/certipy-4-0-esc9-esc10-bloodhound-gui-new-authentication-and-request-methods-and-more-7237d88061f7') ],
'ESC13' => [ SiteReference.new('URL', 'https://posts.specterops.io/adcs-esc13-abuse-technique-fda4272fbd53') ],
'ESC15' => [ SiteReference.new('URL', 'https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc') ]
}.freeze
Expand Down Expand Up @@ -61,7 +66,7 @@ def initialize(info = {})
'Author' => [
'Grant Willcox', # Original module author
'Spencer McIntyre', # ESC13 and ESC15 updates
'jheysel-r7' # ESC4 update
'jheysel-r7' # ESC4, ESC9 and ESC10 update
],
'References' => REFERENCES.values.flatten.map { |r| [ r.ctx_id, r.ctx_val ] }.uniq,
'DisclosureDate' => '2021-06-17',
Expand Down Expand Up @@ -91,6 +96,9 @@ def initialize(info = {})
CERTIFICATE_ENROLLMENT_EXTENDED_RIGHT = '0e10c968-78fb-11d2-90d4-00c04f79dc55'.freeze
CERTIFICATE_AUTOENROLLMENT_EXTENDED_RIGHT = 'a05b8cc2-17bc-4802-a710-e7c15ab866a2'.freeze
CONTROL_ACCESS = 0x00000100
CT_FLAG_NO_SECURITY_EXTENSION = 0x80000
CT_FLAG_SUBJECT_ALT_REQUIRE_DNS = 0x8000000 # 134217728
CT_FLAG_SUBJECT_ALT_REQUIRE_UPN = 0x2000000 # 33554432

# 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
Expand Down Expand Up @@ -329,20 +337,14 @@ def find_esc3_vuln_cert_templates

def find_esc4_vuln_cert_templates
# Determine who we are authenticating with. Retrieve the username and user SID
whoami_response = ''
begin
whoami_response = @ldap.ldapwhoami
rescue Net::LDAP::Error => e
print_warning("The module failed to run the ldapwhoami command, ESC4 detection can't continue. Error was: #{e.class}: #{e.message}.")
user_info = get_authenticated_user_info
rescue LdapWhoamiError => e
print_warning("ESC4 detection skipped: #{e.message}")
return
end

if whoami_response.empty?
print_error("Unable to retrieve the username using ldapwhoami, ESC4 detection can't continue")
return
end

sam_account_name = whoami_response.split('\\')[1]
sam_account_name = user_info[:sam_account_name]
user_raw_filter = "(sAMAccountName=#{sam_account_name})"
attributes = ['DN', 'objectSID', 'objectClass', 'primarygroupID']
our_account = query_ldap_server(user_raw_filter, attributes)&.first
Expand Down Expand Up @@ -431,6 +433,175 @@ def find_esc4_vuln_cert_templates
end
end

def get_object_by_dn(dn)
object = @ldap_objects.find { |o| o['dn']&.first == dn }
return object if object

object = query_ldap_server("(distinguishedName=#{ldap_escape_filter(dn)})", nil)&.first
@ldap_objects << object if object
object
end

def get_authenticated_user_info
# Check if the authenticated user info is already cached
cached_user_info = @ldap_objects.find { |obj| obj[:type] == :authenticated_user }
return cached_user_info if cached_user_info

whoami_response = @ldap.ldapwhoami
if whoami_response.empty?
raise LdapWhoamiError, 'Unable to retrieve the username using ldapwhoami.'
end

sam_account_name = whoami_response.split('\\').last
user_filter = "(sAMAccountName=#{sam_account_name})"
attributes = ['objectSID', 'dn']
user_entry = query_ldap_server(user_filter, attributes).first

if user_entry.nil? || user_entry[:objectsid].nil?
raise LdapWhoamiError, 'Unable to determine the SID for the authenticated user.'
end

user_info = {
type: :authenticated_user,
sid: Rex::Proto::MsDtyp::MsDtypSid.read(user_entry[:objectsid].first).value,
dn: user_entry[:dn].first,
sam_account_name: sam_account_name
}

@ldap_objects << user_info
user_info
end

# Helper method for ESC9 and ESC10. Queries the LDAP server to find users that the authenticated user has
# write privileges over. This is done by checking the ntSecurityDescriptor attribute of each user object.
# @param authenticated_user_sid [String] The SID of the authenticated user.
# @return [Array<String>] A list of distinguished names (DNs) of users the authenticated user can write to.
def find_users_with_writable_sids(authenticated_user_sid)
cached_writable_users = @ldap_objects.find { |obj| obj[:type] == :writable_users }
return cached_writable_users[:users] if cached_writable_users

user_filter = '(objectClass=user)'
user_attributes = ['dn', 'ntSecurityDescriptor']
users = query_ldap_server(user_filter, user_attributes)

writable_users = []
users.each do |user|
security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(user[:ntsecuritydescriptor].first)
writable_sids = get_sids_for_write(security_descriptor.dacl) if security_descriptor.dacl

if writable_sids.any? { |sid| sid.value == authenticated_user_sid }
writable_users << user[:dn].first
end
end

# Cache the writable users in @ldap_objects
@ldap_objects << { type: :writable_users, users: writable_users }

writable_users
end

# Helper method for ESC9 and ESC10. Processes a list of certificate templates to determine if the authenticated
# user has write privileges over any of the users that can enroll in the template. If one of these users are found the
# the method updates the @certificate_details hash with necessary info.
# @param templates [Array<hash>] The list of certificate templates to process.
# @param authenticated_user_sid [String] The SID of the authenticated user.
# @param writable_users [Array<string>] A list of distinguished names (DNs) of users the authenticated user can write to.
# @param note_template [String] A template string for the note to be added, with placeholders for authenticated and writable user names.
# @param technique [String] The technique identifier (e.g., 'ESC9', 'ESC10') to associate with the template.
# @return [void]
def process_templates(templates, authenticated_user_sid, writable_users, note_template, technique)
templates.each do |template|
enrollable_sids = @certificate_details[template[:cn][0].to_sym][:enroll_sids]
writable_users.any? do |user_dn|
user_object = get_object_by_dn(user_dn)
next false unless user_object && user_object[:objectsid]

user_sid = Rex::Proto::MsDtyp::MsDtypSid.read(user_object[:objectsid].first).value
if enrollable_sids.any? { |sid| sid.value == user_sid }
authenticated_user_name = map_sids_to_names([authenticated_user_sid]).first.name
writable_user_name = map_sids_to_names([user_sid]).first.name
note = [note_template % { authenticated_user: authenticated_user_name, writable_user: writable_user_name }]
certificate_symbol = template[:cn][0].to_sym
@certificate_details[certificate_symbol][:techniques] << technique
@certificate_details[certificate_symbol][:notes] += note
end
end
end
end

def find_esc9_vuln_cert_templates
esc9_raw_filter = '(&'\
'(objectclass=pkicertificatetemplate)'\
"(mspki-enrollment-flag:1.2.840.113556.1.4.803:=#{CT_FLAG_NO_SECURITY_EXTENSION})"\
'(|(mspki-ra-signature=0)(!(mspki-ra-signature=*)))'\
'(|'\
"(pkiextendedkeyusage=#{OIDs::OID_KP_SMARTCARD_LOGON.value})"\
"(pkiextendedkeyusage=#{OIDs::OID_PKIX_KP_CLIENT_AUTH.value})"\
"(pkiextendedkeyusage=#{OIDs::OID_ANY_EXTENDED_KEY_USAGE.value})"\
'(!(pkiextendedkeyusage=*))'\
')'\
'(|'\
"(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_UPN})"\
"(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_DNS})"\
')'\
')'

begin
authenticated_user_sid = get_authenticated_user_info[:sid]
rescue LdapWhoamiError => e
print_warning("ESC9 detection skipped: #{e.message}")
return
end

esc9_templates = query_ldap_server(esc9_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE)
writable_users = find_users_with_writable_sids(authenticated_user_sid)

# Finds templates that the users we have write privileges over can enroll in
process_templates(
esc9_templates,
authenticated_user_sid,
writable_users,
'ESC9: Template has msPKI-Enrollment-Flag set to 0x80000 (CT_FLAG_NO_SECURITY_EXTENSION) and specifies a client authentication EKU and %<authenticated_user>s has write privileges over %<writable_user>s and the template has a subjectAltName (UPN or DNS) requirement',
'ESC9'
)
end

def find_esc10_vuln_cert_templates
esc10_raw_filter = '(&'\
'(objectclass=pkicertificatetemplate)'\
'(|(mspki-ra-signature=0)(!(mspki-ra-signature=*)))'\
'(|'\
"(pkiextendedkeyusage=#{OIDs::OID_KP_SMARTCARD_LOGON.value})"\
"(pkiextendedkeyusage=#{OIDs::OID_PKIX_KP_CLIENT_AUTH.value})"\
"(pkiextendedkeyusage=#{OIDs::OID_ANY_EXTENDED_KEY_USAGE.value})"\
'(!(pkiextendedkeyusage=*))'\
')'\
'(|'\
"(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_UPN})"\
"(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_SUBJECT_ALT_REQUIRE_DNS})"\
')'\
')'

begin
authenticated_user_sid = get_authenticated_user_info[:sid]
rescue LdapWhoamiError => e
print_warning("ESC10 detection skipped: #{e.message}")
return
end

esc10_templates = query_ldap_server(esc10_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE)
writable_users = find_users_with_writable_sids(authenticated_user_sid)

# Finds templates that the users we have write privileges over can enroll in
process_templates(
esc10_templates,
authenticated_user_sid,
writable_users,
'ESC10: Template specifies a client authentication EKU and %<authenticated_user>s has write privileges over %<writable_user>s and the template has a subjectAltName (UPN or DNS) requirement',
'ESC10'
)
end

def find_esc13_vuln_cert_templates
esc_raw_filter = <<~FILTER
(&
Expand Down Expand Up @@ -648,7 +819,14 @@ def print_vulnerable_cert_info
print_status(" Distinguished Name: #{hash[:dn]}")
print_status(" Manager Approval: #{hash[:manager_approval] ? '%redRequired' : '%grnDisabled'}%clr")
print_status(" Required Signatures: #{hash[:required_signatures] == 0 ? '%grn0' : '%red' + hash[:required_signatures].to_s}%clr")
print_good(" Vulnerable to: #{techniques.join(', ')}")
print_good(" Vulnerable to: #{(techniques - %w[ESC9 ESC10]).join(', ')}")
if techniques.include?('ESC9')
print_warning(' Potentially vulnerable to: ESC9 (the template is in a vulnerable configuration but in order to exploit registry key StrongCertificateBindingEnforcement must not be set to 2)')
end
if techniques.include?('ESC10')
print_warning(' Potentially vulnerable to: ESC10 (the template is in a vulnerable configuration but in order to exploit registry key StrongCertificateBindingEnforcement must be set to 0 or CertificateMappingMethods must be set to 0x4)')
end

if hash[:notes].present? && hash[:notes].length == 1
print_status(" Notes: #{hash[:notes].first}")
elsif hash[:notes].present? && hash[:notes].length > 1
Expand Down Expand Up @@ -729,7 +907,7 @@ def get_group_by_dn(group_dn)

def get_object_by_sid(object_sid)
object_sid = Rex::Proto::MsDtyp::MsDtypSid.new(object_sid)
object = @ldap_objects.find { |o| o['objectSID'].first == object_sid.to_binary_s }
object = @ldap_objects.find { |o| o['objectSID']&.first == object_sid.to_binary_s }

if object.nil?
object = query_ldap_server("(objectSID=#{ldap_escape_filter(object_sid.to_s)})", nil)&.first
Expand Down Expand Up @@ -825,6 +1003,8 @@ def run
find_esc2_vuln_cert_templates
find_esc3_vuln_cert_templates
find_esc4_vuln_cert_templates
find_esc9_vuln_cert_templates
find_esc10_vuln_cert_templates
find_esc13_vuln_cert_templates
find_esc15_vuln_cert_templates

Expand Down
Loading