-
Notifications
You must be signed in to change notification settings - Fork 14.4k
ESC9, ESC10 and ESC16 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
base: master
Are you sure you want to change the base?
ESC9, ESC10 and ESC16 detection for ldap_esc_vulnerable_cert_finder #20149
Conversation
Thanks for your pull request! Before this can be merged, we need the following documentation for your module: |
documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md
Outdated
Show resolved
Hide resolved
documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md
Outdated
Show resolved
Hide resolved
documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md
Show resolved
Hide resolved
Thanks for the review @smcintyre-r7. I added a datastore option in 31e2fca to allow the user to attempt to read the registry key values related to ESC9, 10 and potentially 16 later. I've included some excerpts from a few different testing scenarios, a bit about the user who ran the module and the test environment. testing
|
end | ||
|
||
def enum_registry_values | ||
endpoint = "http://#{datastore['RHOST']}:5985/wsman" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WMI port number here should be configurable. I know this could be done with SMB as well. I figured this is non-default functionality and thought it might be nice to be able to run Powershell commands (this might come in handy for ESC16). @smcintyre-r7 if you have any concerns around this design choice let me know.
find_esc13_vuln_cert_templates | ||
find_esc15_vuln_cert_templates | ||
if registry_values && registry_values[:disabled_extension_list]&.include?('1.3.6.1.4.1.311.25.2') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably makes sense to run find_esc16_vuln_cert_templates
even if we don't have the registry keys - in order to follow the same pattern of marking those templates as "Potentially vulnerable"... 🤔
ESC16 i guess is still a WIP, I need to refine the esc16_raw_filter
to correctly match both scenario 1 & 2 - with UPN / SAN requirements.
# Cache the registry values in @ldap_objects | ||
@ldap_objects << { type: :registry_values, values: registry_values } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just doesn't exactly seem right. The @ldap_objects
should just contain ldap objects, caching registry values and the authenticated user is great but they should be kept separate because it's confusing to store things that aren't LDAP object here. Maybe @registry_values
, keyed by the registry key would be a better fit?
|
||
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 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to my other comment about storing things that aren't LDAP objects in the @ldap_objects
array seems counter-intuitive. This also seems like we probably don't even need to cache it. If @whoami
is caching the name of the current user, then the LDAP object can just be cached and stored with the other LDAP objects by it's sAMAccountName
attribute.
edit_flags = registry_object[:values][:edit_flags] | ||
if edit_flags.to_i & 0x00040000 != 0 | ||
esc_entries.each do |entry| | ||
certificate_symbol = entry[:cn][0].to_sym | ||
@certificate_details[certificate_symbol][:techniques] << 'ESC16' | ||
@certificate_details[certificate_symbol][:notes] << 'ESC16: Template is vulnerable due to the active policy EditFlags having the bit: 0x00040000 set (which is essentially ESC6) combined with the CA\'s disabled policy extension list including: 1.3.6.1.4.1.311.25.2.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a contant we can add to and use from MsCrtd for 0x00040000
? That'd also make the description more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag is defined by Microsoft as EDITF_ATTRIBUTESUBJECTALTNAME2
:
https://learn.microsoft.com/en-us/defender-for-identity/security-assessment-edit-vulnerable-ca-setting
I've defined it at the top of this file for now as I'm not sure it belongs in MsCrtd
as it's not prefixed with CT_FLAG
but if there's a better place for it/ we want it in MsCrtd
, I'd be happy to move it.
documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md
Outdated
Show resolved
Hide resolved
documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md
Outdated
Show resolved
Hide resolved
documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md
Outdated
Show resolved
Hide resolved
conn.shell(:powershell) do |shell| | ||
registry_values[:certificate_mapping_methods] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\Schannel', 'CertificateMappingMethods') | ||
registry_values[:strong_certificate_binding_enforcement] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Kdc', 'StrongCertificateBindingEnforcement') | ||
|
||
active_policy_name = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'Active') | ||
registry_values[:disabled_extension_list] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'DisableExtensionList', active_policy_name) | ||
registry_values[:edit_flags] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'EditFlags', active_policy_name) | ||
end | ||
|
||
if registry_values[:strong_certificate_binding_enforcement] == '1' | ||
vprint_good('ESC9 could be exploitable using Kerberos due to StrongCertificateBindingEnforcement being set to 1') | ||
elsif registry_values[:strong_certificate_binding_enforcement] == '0' | ||
vprint_good('ESC10 could be exploitable using Kerberos due to StrongCertificateBindingEnforcement being set to 0') | ||
elsif registry_values[:strong_certificate_binding_enforcement] == '2' | ||
vprint_error('ESC9 and ESC10 cannot be exploited using Kerberos due to StrongCertificateBindingEnforcement being set to 2') | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of these values should really be converted to integers instead of being left as strings because they are REG_DWORD values AFAIK. That would make checking for flags easier later on.
users = [] | ||
enroll_sids.each do |sid| | ||
user_object = get_object_by_sid(sid.value) | ||
next unless user_object && user_object[:ntsecuritydescriptor] && user_object[:objectclass]&.include?('user') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is checking all of the users in the security descriptor that directly have enrollment rights. What this is missing are groups that have enrollment rights. So if a user has enrollment rights through a group that they're a member of, we're missing checking if we can write to that user object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great catch - in this test scenario user1 has write privs over user3 who is a member of group1 which is a member of group2 which can enroll into the template titled: ESC9-Template-Group-Inheritance-Enroll-Test
:
[+] Template: ESC9-Template-Group-Inheritance-Enroll-Test
[*] Distinguished Name: CN=ESC9-Template-Group-Inheritance-Enroll-Test,CN=Certificate Templates,CN=Public Key Services,CN=Services,CN=Configuration,DC=kerberos,DC=issue
[*] Manager Approval: Disabled
[*] Required Signatures: 0
[+] Vulnerable to: ESC9, ESC10
[*] Notes:
[*] * ESC9: The account: user1 has edit permission over the user: user3 which has enrollment rights for this template. Registry value: StrongCertificateBindingEnforcement=0.
[*] * ESC10: The account: user1 has edit permission over the user: user3 which has enrollment rights for this template. Registry values: StrongCertificateBindingEnforcement=0, CertificateMappingMethods=4.
Co-authored-by: Spencer McIntyre <[email protected]>
This PR adds detections for ESC9 and ESC10 to the
ldap_esc_vulnerable_cert_finder
module.TODO: Write documentation on how to create certificate templates vulnerable to both of these misconfiguration.Verification
List the steps needed to make sure this thing works
use auxiliary/gather/ldap_esc_vulnerable_cert_finder
set LDAPUsername <username>
set LDAPPassword <password>
set LDAPDomain <password>
set RHOSTS <target IP(s)>
set RPORT <target port>
if target port is non-default.set SSL true
if the target port is SSL enabled.run
Testing
ESC9
ESC10
ESC16
Scenario 1
Scenario 2