diff --git a/docs/metasploit-framework.wiki/ad-certificates/overview.md b/docs/metasploit-framework.wiki/ad-certificates/overview.md index ddf2342b45a81..66c7174901bb6 100644 --- a/docs/metasploit-framework.wiki/ad-certificates/overview.md +++ b/docs/metasploit-framework.wiki/ad-certificates/overview.md @@ -52,79 +52,4 @@ Microsoft provides a very useful [training module](https://learn.microsoft.com/e that covers the fundamentals of AD CS and as well as examples which cover the management of certificate enrollment, certificate revocation and certificate trusts. ## Setting up A Vulnerable AD CS Server -The following steps assume that you have installed an AD CS on either a new or existing domain controller. -### Installing AD CS -1. Open the Server Manager -2. Select Add roles and features -3. Select "Active Directory Certificate Services" under the "Server Roles" section -4. When prompted add all of the features and management tools -5. On the AD CS "Role Services" tab, leave the default selection of only "Certificate Authority" -6. Completion the installation and reboot the server -7. Reopen the Server Manager -8. Go to the AD CS tab and where it says "Configuration Required", hit "More" then "Configure Active Directory Certificate..." -9. Select "Certificate Authority" in the Role Services tab -10. Select "Enterprise CA" in the "Setup Type" tab (the user must be a Domain Administrator for this option to be available) -11. Keep all of the default settings, noting the value of the "Common name for this CA" on the "CA Name" tab (this value corresponds to the `CA` datastore option) -12. Accept the rest of the default settings and complete the configuration - -### Setting up a ESC1 Vulnerable Certificate Template -1. Open up the run prompt and type in `certsrv`. -2. In the window that appears you should see your list of certification authorities under `Certification Authority (Local)`. Right click on the folder in the drop down marked `Certificate Templates` and then click `Manage`. -3. Scroll down to the `User` certificate. Right click on it and select `Duplicate Template`. -4. From here you can refer to the following [Active-Directory-Certificate-Services-abuse](https://github.com/RayRRT/Active-Directory-Certificate-Services-abuse/blob/3da1d59f1b66dd0e381b2371b8fb42d87e2c9f82/ADCS.md) documentation for screenshots. -5. Select the `General` tab and rename this to something meaningful like `ESC1-Template`, then click the `Apply` button. -6. In the `Subject Name` tab, select `Supply in the request` and click `Ok` on the security warning that appears. Then click the `Apply` button. -7. Scroll to the `Extensions` tab and under `Application Policies` ensure that `Client Authentication`, `Server Authentication`, `KDC Authentication`, or `Smart Card Logon` is listed. Then click the `Apply` button. -8. Under the `Security` tab make sure that `Domain Users` group listed and the `Enroll` permissions is marked as allowed for this group. -9. Under `Issuance Requirements` tab, ensure that under `Require the following for enrollment` that the `CA certificate manager approval` box is unticked, as is the `This number of authorized signatures` box. -10. Click `Apply` and then `Ok` -11. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder. Then click `New` followed by `Certificate Template to Issue`. -12. Scroll down and select the `ESC1-Template` certificate, or whatever you named the ESC1 template you created, and select `OK`. The certificate should now be available to be issued by the CA server. - -### Setting up a ESC2 Vulnerable Certificate Template -1. Open up `certsrv` -2. Scroll down to `Certificate Templates` folder, right click on it and select `Manage`. -3. Find the `ESC1` certificate template you created earlier and right click on that, then select `Duplicate Template`. -4. Select the `General` tab, and then name the template `ESC2-Template`. Then click `Apply`. -5. Go to the `Subject Name` tab and select `Build from this Active Directory Information` and select `Fully distinguished name` under the `Subject Name Format`. The main idea of setting this option is to prevent being able to supply the subject name in the request as this is more what makes the certificate vulnerable to ESC1. The specific options here I don't think will matter so much so long as the `Supply in the request` option isn't ticked. Then click `Apply`. -6. Go the to `Extensions` tab and click on `Application Policies`. Then click on `Edit`. -7. Delete all the existing application policies by clicking on them one by one and clicking the `Remove` button. -8. Click the `Add` button and select `Any Purpose` from the list that appears. Then click the `OK` button. -9. Click the `Apply` button, and then `OK`. The certificate should now be created. -10. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder. Then click `New` followed by `Certificate Template to Issue`. -11. Scroll down and select the `ESC2-Template` certificate, or whatever you named the ESC2 template you created, and select `OK`. The certificate should now be available to be issued by the CA server. - -### Setting up a ESC3 Template 1 Vulnerable Certificate Template -1. Follow the instructions above to duplicate the ESC2 template and name it `ESC3-Template1`, then click `Apply`. -2. Go to the `Extensions` tab, click the Application Policies entry, click the `Edit` button, and remove the `Any Purpose` policy and replace it with `Certificate Request Agent`, then click `OK`. -3. Click `Apply`. -4. Go to `Issuance Requirements` tab and double check that both `CA certificate manager approval` and `This number of authorized signatures` are unchecked. -5. Click `Apply` if any changes were made or the button is not grey'd out, then click `OK` to create the certificate. -6. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder. Then click `New` followed by `Certificate Template to Issue`. -7. Scroll down and select the `ESC3-Template1` certificate, or whatever you named the ESC3 template number 1 template you just created, and select `OK`. The certificate should now be available to be issued by the CA server. - -### Setting up a ESC3 Template 2 Vulnerable Certificate Template -1. Follow the instructions above to duplicate the ESC2 template and name it `ESC3-Template2`, then click `Apply`. -2. Go to the `Extensions` tab, click the Application Policies entry, click the `Edit` button, and remove the `Any Purpose` policy and replace it with `Client Authentication`, then click `OK`. -3. Click `Apply`. -4. Go to `Issuance Requirements` tab and double check that both `CA certificate manager approval` is unchecked. -5. Check the `This number of authorized signatures` checkbox and ensure the value specified is 1, and that the `Policy type required in signature` is set to `Application Policy`, and that the `Application policy` value is `Certificate Request Agent`. -6. Click `Apply` and then click `OK` to issue the certificate. -7. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder. Then click `New` followed by `Certificate Template to Issue`. -8. Scroll down and select the `ESC3-Template2` certificate, or whatever you named the ESC3 template number 2 template you just created, and select `OK`. The certificate should now be available to be issued by the CA server. - -### Setting up a ESC8 Vulnerable Host -1. Follow instructions for creating an AD CS enabled server -2. Select Add Roles and Features -3. Under "Select Server Roles" expand Active Directory Certificate Services and add `Certificate Enrollment Policy Web Service`, `Certificate Enrollment Web Service`, and `Certificate Authority Web Enrollment`. -4. For each selection, accept the default for any pop-up. -5. Accept the default features and install. -6. When the installation is complete, click on the warning in the Dashboard for post-deployment configuration. -7. Under Credentials, accept the default -8. Under Role Services, select `Certificate Authority Web Enrollment`, `Certificate Enrollment Web Service`, and `Certificate Enrollment Policy Web Service` -9. In CA for CES, accept the defaults -10. In Authentication Types, accept the default integrated authentication -11. In Service account for CES, select `Use built-in application pool identity` -12. Accept default integrated authentication for CEP -13. Select the domain certificate in Server Certificate (the one that starts with the domain name by default) if more than one appears. -14. Accept the remaining defaults. \ No newline at end of file +The steps for setting up a vulnerable AD CS server are covered in the [[Installing AD CS|./ldap_esc_vulnerable_cert_finder.md]] section. diff --git a/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md b/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md index fecfda84343bc..134fc1efc5b21 100644 --- a/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md +++ b/documentation/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.md @@ -90,6 +90,55 @@ 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 Subject Name tab and select `Build from this Active Directory Information`, under the `Subject Name Format` section select `User Principal Name (UPN)`. +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. Enable advanced features to access the security tab by checking "View" > "Advanced Features" +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 +Set-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Services\Kdc\" -Name StrongCertificateBindingEnforcement -Value 0 +Get-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Services\Kdc\" -Name StrongCertificateBindingEnforcement +``` + +### Setting up a ESC10 Vulnerable Certificate Template +1. Follow the first 14 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 +Set-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Services\Kdc\" -Name StrongCertificateBindingEnforcement -Value 0 +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 +Set-ItemProperty -Path "HKLM:SYSTEM\CurrentControlSet\Control\SecurityProviders\Schannel\" -Name CertificateMappingMethods -Value 4 +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. @@ -131,6 +180,52 @@ a normal user account by analyzing the objects in LDAP. 1. Go back to the `certsrv` screen and right click on the `Certificate Templates` folder and ensure `WebServer` is listed, if it's not, add it. 1. The certificate should now be available to be issued by the CA server. +### Setting up a ESC16 Vulnerable Certificate Template +#### Configuring Windows to be Vulnerable to ESC16 +1. There are two ECS16 scenarios and both depend on the CA having the OID: `1.3.6.1.4.1.311.25.2` being present in its `policy\DisableExtensionList` +1. Run the following Powershell snippet to add the OID to the `DisableExtensionList` if it is not already present: +```powershell +$activePolicyName = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules" -Name "Active" | Select-Object -ExpandProperty Active +$disableExtensionList = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules\$activePolicyName" -Name "DisableExtensionList" | Select-Object -ExpandProperty DisableExtensionList + +if (-not ($disableExtensionList -contains "1.3.6.1.4.1.311.25.2")) { + $updatedList = $disableExtensionList + @("1.3.6.1.4.1.311.25.2") + Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules\$activePolicyName" -Name "DisableExtensionList" -Value $updatedList + Write-Output "OID 1.3.6.1.4.1.311.25.2 has been added to the DisableExtensionList." +} else { + Write-Output "OID 1.3.6.1.4.1.311.25.2 is already present in the DisableExtensionList." +} +``` +#### ESC16 Scenario 1 +When a CA has the OID `1.3.6.1.4.1.311.25.2` added to its `policy\DisableExtensionList` registry setting every certificate issued by this CA will lack this SID security extension. +This effectively makes all templates published by this CA behave as if they were individually configured with the `CT_FLAG_NO_SECURITY_EXTENSION` flag (as seen in ESC9). +So if `StrongCertificateBindingEnforcement` is not set to `2` we can exploit this weak mapping. + +In order to create a template vulnerable to ESC16 scenario 1, follow the first 15 steps in `Setting up a ESC9 Vulnerable Certificate Template`, +which is all the steps up to and excluding the `msPKI-Enrollment-Flag", 0x80000` powershell step which is how you set the `CT_FLAG_NO_SECURITY_EXTENSION`. +Ensure that `StrongCertificateBindingEnforcement` is set to `0` or `1` (not `2`) by running the following command listed in `Configuring Windows to be Vulnerable to ESC9` + +### ESC16 Scenario 2 +When a CA has the OID `1.3.6.1.4.1.311.25.2` added to its `policy\DisableExtensionList` and `StrongCertificateBindingEnforcement` is set to `2`, there is still a way to exploit the template. +If the policy module's `EditFlags` has the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag set (which is essentially ESC6), then the template is vulnerable to ESC16 scenario 2. + +Ensure the `EDITF_ATTRIBUTESUBJECTALTNAME2` flag is set by running following PowerShell command: +```powershell +$EDITF_ATTRIBUTESUBJECTALTNAME2 = 0x00040000 +$activePolicyName = (Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules" -Name "Active").Active +$editFlagsPath = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\*\PolicyModules\$activePolicyName" +$editFlags = (Get-ItemProperty -Path $editFlagsPath -Name "EditFlags").EditFlags + +if ($editFlags -band $EDITF_ATTRIBUTESUBJECTALTNAME2) { + Write-Output "The EDITF_ATTRIBUTESUBJECTALTNAME2 flag is already enabled." +} else { + # Enable the flag by setting it in the EditFlags value + $newEditFlags = $editFlags -bor $EDITF_ATTRIBUTESUBJECTALTNAME2 + Set-ItemProperty -Path $editFlagsPath -Name "EditFlags" -Value $newEditFlags + Write-Output "The EDITF_ATTRIBUTESUBJECTALTNAME2 flag has been enabled." +} +``` + ## Module usage 1. Do: Start msfconsole diff --git a/lib/rex/proto/crypto_asn1/o_i_ds.rb b/lib/rex/proto/crypto_asn1/o_i_ds.rb index b49fa1faa4bc3..8927aa397cdd6 100644 --- a/lib/rex/proto/crypto_asn1/o_i_ds.rb +++ b/lib/rex/proto/crypto_asn1/o_i_ds.rb @@ -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) diff --git a/lib/rex/proto/ms_crtd.rb b/lib/rex/proto/ms_crtd.rb index 8b7e4c69e9813..83846b85e34b4 100644 --- a/lib/rex/proto/ms_crtd.rb +++ b/lib/rex/proto/ms_crtd.rb @@ -36,6 +36,7 @@ module MsCrtd CT_FLAG_ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000 CT_FLAG_ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000 CT_FLAG_SKIP_AUTO_RENEWAL = 0x00040000 + CT_FLAG_NO_SECURITY_EXTENSION = 0x80000 # [2.27 msPKI-Private-Key-Flag Attribute](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/f6122d87-b999-4b92-bff8-f465e8949667) CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001 diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 8e779f3c1dd55..c102f228f4f58 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -1,3 +1,4 @@ +require 'winrm' class MetasploitModule < Msf::Auxiliary include Msf::Auxiliary::Report @@ -6,6 +7,10 @@ class MetasploitModule < Msf::Auxiliary include Rex::Proto::MsDnsp include Rex::Proto::Secauthz include Rex::Proto::LDAP + include Rex::Proto::CryptoAsn1 + include Rex::Proto::MsCrtd + + class LdapWhoamiError < StandardError; end ADS_GROUP_TYPE_BUILTIN_LOCAL_GROUP = 0x00000001 ADS_GROUP_TYPE_GLOBAL_GROUP = 0x00000002 @@ -13,13 +18,19 @@ class MetasploitModule < Msf::Auxiliary ADS_GROUP_TYPE_SECURITY_ENABLED = 0x80000000 ADS_GROUP_TYPE_UNIVERSAL_GROUP = 0x00000008 + # https://learn.microsoft.com/en-us/defender-for-identity/security-assessment-edit-vulnerable-ca-setting + EDITF_ATTRIBUTESUBJECTALTNAME2 = 0x00040000 + REFERENCES = { 'ESC1' => [ SiteReference.new('URL', 'https://posts.specterops.io/certified-pre-owned-d95910965cd2') ], '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') ] + 'ESC15' => [ SiteReference.new('URL', 'https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc') ], + 'ESC16' => [ SiteReference.new('URL', 'https://github.com/ly4k/Certipy/wiki/06-%E2%80%90-Privilege-Escalation') ] }.freeze SID = Struct.new(:value, :name) do @@ -57,11 +68,14 @@ def initialize(info = {}) Currently the module is capable of checking for certificates that are vulnerable to ESC1, ESC2, ESC3, ESC4, ESC13, and ESC15. 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. + + The module can also check for ESC9, ESC10 and ESC16 but this requires an Administrative WinRM session to be + established to definitively check for these techniques. }, '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', @@ -82,6 +96,7 @@ def initialize(info = {}) OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']), OptBool.new('REPORT_NONENROLLABLE', [true, 'Report nonenrollable certificate templates', false]), OptBool.new('REPORT_PRIVENROLLABLE', [true, 'Report certificate templates restricted to domain and enterprise admins', false]), + OptBool.new('RUN_REGISTRY_CHECKS', [true, 'Authenticate to WinRM to query the registry values to enhance reporting for ESC9 and ESC10. Must be a privleged user in order to query successfully', false]), ]) end @@ -328,106 +343,306 @@ def find_esc3_vuln_cert_templates end def find_esc4_vuln_cert_templates - # Determine who we are authenticating with. Retrieve the username and user SID - whoami_response = '' + # Obtain the authenticated user information to check if they have write permissions on the certificate templates 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}.") + authenticated_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 + esc_raw_filter = '(objectclass=pkicertificatetemplate)' + attributes = ['cn', 'description', 'ntSecurityDescriptor'] + esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: CERTIFICATE_TEMPLATES_BASE) + + return if esc_entries.empty? + + esc_entries.each do |entry| + certificate_symbol = entry[:cn][0].to_sym + next if @certificate_details[certificate_symbol][:enroll_sids].empty? + + security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(entry[:ntsecuritydescriptor].first) + if user_can_write?(authenticated_user_info, security_descriptor) + @certificate_details[certificate_symbol][:techniques] << 'ESC4' + @certificate_details[certificate_symbol][:notes] << "ESC4: The account: #{authenticated_user_info[:samaccountname].first} has edit permissions over the template #{certificate_symbol}." + end end + end - sam_account_name = whoami_response.split('\\')[1] - user_raw_filter = "(sAMAccountName=#{sam_account_name})" - attributes = ['DN', 'objectSID', 'objectClass', 'primarygroupID'] - our_account = query_ldap_server(user_raw_filter, attributes)&.first - if our_account.nil? - print_warning("Unable to determine the User SID for #{sam_account_name}, ESC4 detection can't continue") - return + 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_object_by_samaccountname(samaccountname) + object = @ldap_objects.find { |o| o['sAMAccountName']&.first == samaccountname } + + if object.nil? + object = query_ldap_server("(sAMAccountName=#{ldap_escape_filter(samaccountname)})", nil)&.first + @ldap_objects << object if object end - user_sid = map_sids_to_names([Rex::Proto::MsDtyp::MsDtypSid.read(our_account[:objectsid].first).value]).first - domain_sid = user_sid.value.to_s.rpartition('-').first - user_groups = [] + object + end + + def get_authenticated_user_info + if (@whoami ||= @ldap.ldapwhoami).present? + sam_account_name = @whoami.split('\\').last + cached_user_object = @ldap_objects.find { |o| o[:samaccountname]&.first == sam_account_name } + return cached_user_object if cached_user_object + end - if our_account[:primarygroupID] - user_groups << "#{domain_sid}-#{our_account[:primarygroupID]&.first}" + if @whoami.blank? + raise LdapWhoamiError, 'Unable to retrieve the username using ldapwhoami.' end - # Authenticated Users includes all users and computers with identities that have been authenticated. - # Authenticated Users doesn't include Guest even if the Guest account has a password. - unless sam_account_name == 'Guest' - user_groups << Rex::Proto::Secauthz::WellKnownSids::SECURITY_AUTHENTICATED_USER_SID + sam_account_name = @whoami.split('\\').last + user_object = get_object_by_samaccountname(sam_account_name) + if user_object.nil? || user_object[:objectsid].nil? + raise LdapWhoamiError, 'Unable to determine the SID for the authenticated user.' end - # Perform an LDAP query to get the groups the user is a part of - # Use LDAP_MATCHING_RULE_IN_CHAIN OID in order to walk the chain of ancestry of groups. - # https://learn.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax?redirectedfrom=MSDN - filter_with_user = "(|(member:1.2.840.113556.1.4.1941:=#{our_account[:dn].first})" - user_groups.each do |sid| - obj = get_object_by_sid(sid) - print_error('Failed to lookup SID.') unless obj + # Walk the chain of ancestry of groups using LDAP_MATCHING_RULE_IN_CHAIN + filter_with_user = "(|(member:1.2.840.113556.1.4.1941:=#{user_object[:dn].first})" + user_groups = user_object[:memberof] || [] + user_groups.each do |group_dn| + group_object = get_object_by_dn(group_dn) + next unless group_object - filter_with_user << "(member:1.2.840.113556.1.4.1941:=#{obj[:dn].first})" if obj + filter_with_user << "(member:1.2.840.113556.1.4.1941:=#{group_object[:dn].first})" end filter_with_user << ')' attributes = ['cn', 'objectSID'] - esc_entries = query_ldap_server(filter_with_user, attributes) + group_entries = query_ldap_server(filter_with_user, attributes) - esc_entries.each do |entry| - group_sid = Rex::Proto::MsDtyp::MsDtypSid.read(entry['ObjectSid'].first).value - user_groups << group_sid + # Extract group SIDs and add them to the user_object + group_sids = group_entries.map { |entry| Rex::Proto::MsDtyp::MsDtypSid.read(entry[:objectsid].first).value } + user_object[:memberof] ||= [] + user_object[:memberof].concat(group_sids) + + @ldap_objects << user_object + end + + def user_can_write?(authenticated_user_info, security_descriptor) + write_sids = get_sids_for_write(security_descriptor.dacl) + user_sid = Rex::Proto::MsDtyp::MsDtypSid.read(authenticated_user_info[:objectsid].first).value + + # Extract group SIDs from :memberof + group_sids = authenticated_user_info[:memberof]&.map do |group_dn| + group_object = get_object_by_dn(group_dn) + next unless group_object && group_object[:objectsid] + + Rex::Proto::MsDtyp::MsDtypSid.read(group_object[:objectsid].first).value + end&.compact || [] + + (write_sids.map(&:value) & ([user_sid] + group_sids)).any? + end + + def parse_registry_output(output, property_name) + return nil if output.stderr.present? + + stdout = output.stdout if output.stdout.present? + return nil unless stdout + + line_with_property = stdout.lines.find { |line| line.strip.match(/^#{Regexp.escape(property_name)}\s*:/) } + return nil unless line_with_property + + line_with_property.split(':', 2).last&.strip + end + + def run_registry_command(shell, path, property_name, dynamic_value = nil) + full_path = dynamic_value ? "#{path}\\#{dynamic_value}" : path + command = "Get-ItemProperty -Path '#{full_path}' -Name #{property_name}" + output = shell.run(command) + value = parse_registry_output(output, property_name) + if value.nil? + print_error("Registry property '#{property_name}' not found at path '#{full_path}'.") end - user_groups = map_sids_to_names(user_groups) + value + end - # Determine what Certificate Templates are available to us - esc_raw_filter = '(objectclass=pkicertificatetemplate)' + def enum_registry_values + @registry_values ||= {} - attributes = ['cn', 'description', 'ntSecurityDescriptor'] - esc_entries = query_ldap_server(esc_raw_filter, attributes, base_prefix: CERTIFICATE_TEMPLATES_BASE) + endpoint = "http://#{datastore['RHOST']}:5985/wsman" + user = datastore['LDAPUsername'] + pass = datastore['LDAPPassword'] - return if esc_entries.empty? + conn = WinRM::Connection.new( + endpoint: endpoint, + user: user, + password: pass, + transport: :negotiate + ) - # Determine if the user we've authenticated with has the ability to edit - esc_entries.each do |entry| - certificate_symbol = entry[:cn][0].to_sym - next if @certificate_details[certificate_symbol][:enroll_sids].empty? + begin + conn.shell(:powershell) do |shell| + @registry_values[:certificate_mapping_methods] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\Schannel', 'CertificateMappingMethods').to_i + @registry_values[:strong_certificate_binding_enforcement] = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Kdc', 'StrongCertificateBindingEnforcement').to_i + + active_policy_name = run_registry_command(shell, 'HKLM:\\SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\*\\PolicyModules', 'Active') + @registry_values[:disable_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).to_i + end + rescue StandardError => e + vprint_warning("Failed to query registry values: #{e.message}") + end + + @registry_values + end - # SIDs that can edit the template - write_priv_sids = @certificate_details[certificate_symbol][:write_sids] - next if write_priv_sids.empty? + def resolve_group_memberships(user_dn) + filter = "(member:1.2.840.113556.1.4.1941:=#{ldap_escape_filter(user_dn)})" + attributes = ['distinguishedName', 'objectSID', 'sAMAccountName'] + groups = query_ldap_server(filter, attributes) + + groups.map do |group| + { + dn: group[:distinguishedname].first, + sid: Rex::Proto::MsDtyp::MsDtypSid.read(group[:objectsid].first).value, + name: group[:samaccountname].first + } + end + end - # Check if the user has been give access to edit the template - user_can_edit = user_sid if write_priv_sids.include?(user_sid) + def fetch_group_members(group_dn) + filter = "(distinguishedName=#{ldap_escape_filter(group_dn)})" + attributes = ['member'] # Fetch the 'member' attribute which contains the group members - # Check if any groups the user is a part of can edit the template - group_can_edit = write_priv_sids & user_groups + group_entry = query_ldap_server(filter, attributes)&.first + return [] unless group_entry && group_entry[:member] - # SIDs that can edit the template that the user we've authenticated with are also a part of - user_write_priv_sids = [] - notes = [] + group_entry[:member] + end - # Main reason for splitting user_can_edit and group_can_edit is so "note" can be more descriptive - if user_can_edit - user_write_priv_sids << user_can_edit - notes << "ESC4: The account: #{sam_account_name} has edit permissions over the template #{certificate_symbol} making it vulnerable to ESC4" + def find_users_with_write_and_enroll_rights(authenticated_user_info, enroll_sids) + users = [] + enroll_sids.each do |sid| + user_object = get_object_by_sid(sid.value) + if user_object && user_object[:objectclass]&.include?('user') + if user_object[:ntsecuritydescriptor] + security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(user_object[:ntsecuritydescriptor].first) + if user_can_write?(authenticated_user_info, security_descriptor) + users << user_object[:samaccountname].first + end + end + next end + group_object = get_object_by_sid(sid.value) + next unless group_object && group_object[:objectclass]&.include?('group') + + group_memberships = resolve_group_memberships(group_object[:dn].first) + group_memberships.each do |group| + members_of_group = fetch_group_members(group[:dn]) + members_of_group.each do |member_dn| + member_object = get_object_by_dn(member_dn) + next unless member_object && member_object[:objectclass]&.include?('user') + + next unless member_object[:ntsecuritydescriptor] - if group_can_edit.any? - user_write_priv_sids.concat(group_can_edit) - notes << "ESC4: The account: #{sam_account_name} is a part of the following groups: (#{group_can_edit.map(&:name).join(', ')}) which have edit permissions over the template object" + security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(member_object[:ntsecuritydescriptor].first) + if user_can_write?(authenticated_user_info, security_descriptor) + users << member_object[:samaccountname].first + end + end end + end - next unless user_write_priv_sids.any? + users&.uniq + end - @certificate_details[certificate_symbol][:techniques] << 'ESC4' - @certificate_details[certificate_symbol][:notes].concat(notes) + 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_info = get_authenticated_user_info + 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) + esc9_templates.each do |template| + certificate_symbol = template[:cn][0].to_sym + enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] + + users = find_users_with_write_and_enroll_rights(authenticated_user_info, enroll_sids) + + next if users.empty? + + user_plural = users.size > 1 ? 'accounts' : 'account' + has_plural = users.size > 1 ? 'have' : 'has' + + note = "ESC9: The account: #{authenticated_user_info[:samaccountname].first} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." + if @registry_values[:strong_certificate_binding_enforcement].present? + note += " Registry value: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}." + end + @certificate_details[certificate_symbol][:techniques] << 'ESC9' + @certificate_details[certificate_symbol][:notes] << note + end + 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_info = get_authenticated_user_info + 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) + esc10_templates.each do |template| + certificate_symbol = template[:cn][0].to_sym + enroll_sids = @certificate_details[certificate_symbol][:enroll_sids] + users = find_users_with_write_and_enroll_rights(authenticated_user_info, enroll_sids) + + next if users.empty? + + user_plural = users.size > 1 ? 'accounts' : 'account' + has_plural = users.size > 1 ? 'have' : 'has' + + note = "ESC10: The account: #{authenticated_user_info[:samaccountname].first} has edit permission over the #{user_plural}: #{users.join(', ')} which #{has_plural} enrollment rights for this template." + + if @registry_values[:strong_certificate_binding_enforcement].present? && @registry_values[:certificate_mapping_methods].present? + note += " Registry values: StrongCertificateBindingEnforcement=#{@registry_values[:strong_certificate_binding_enforcement]}, CertificateMappingMethods=#{@registry_values[:certificate_mapping_methods]}." + end + + @certificate_details[certificate_symbol][:techniques] << 'ESC10' + @certificate_details[certificate_symbol][:notes] << note end end @@ -520,6 +735,39 @@ def find_esc15_vuln_cert_templates query_ldap_server_certificates(esc_raw_filter, 'ESC15', notes: notes) end + def find_esc16_vuln_cert_templates + esc16_raw_filter = '(&'\ + '(|'\ + "(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})"\ + "(mspki-certificate-name-flag:1.2.840.113556.1.4.804:=#{CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME})"\ + ')'\ + '(objectclass=pkicertificatetemplate)'\ + '(!(mspki-enrollment-flag:1.2.840.113556.1.4.804:=2))'\ + '(|(mspki-ra-signature=0)(!(mspki-ra-signature=*)))'\ + '(pkiextendedkeyusage=*)'\ + ')' + + esc_entries = query_ldap_server(esc16_raw_filter, CERTIFICATE_ATTRIBUTES, base_prefix: CERTIFICATE_TEMPLATES_BASE) + return if esc_entries.empty? + + if @registry_values[:strong_certificate_binding_enforcement] && (@registry_values[:strong_certificate_binding_enforcement] == 0 || @registry_values[:strong_certificate_binding_enforcement] == 1) + # Scenario 1 - StrongCertificateBindingEnforcement = 1 or 0 then it's same same as ESC9 - mark them all as vulnerable + 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 StrongCertificateBindingEnforcement = #{@registry_values[:strong_certificate_binding_enforcement]} and the CA's disabled policy extension list includes: 1.3.6.1.4.1.311.25.2." + end + elsif @registry_values[:edit_flags] & EDITF_ATTRIBUTESUBJECTALTNAME2 != 0 + # Scenario 2 - StrongCertificateBindingEnforcement = 2 (or nil) but if EditFlags in the active policy module has EDITF_ATTRIBUTESUBJECTALTNAME2 set then ESC6 is essentially re-enabled and we mark them all as vulnerable + 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: EDITF_ATTRIBUTESUBJECTALTNAME2 set (which is essentially ESC6) combined with the CA\'s disabled policy extension list including: 1.3.6.1.4.1.311.25.2.' + 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 @@ -648,7 +896,19 @@ 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(', ')}") + + if @registry_values.present? + print_good(" Vulnerable to: #{techniques.join(', ')}") + else + 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 4)') + end + end + if hash[:notes].present? && hash[:notes].length == 1 print_status(" Notes: #{hash[:notes].first}") elsif hash[:notes].present? && hash[:notes].length > 1 @@ -729,7 +989,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 @@ -796,6 +1056,7 @@ def run # Define our instance variables real quick. @base_dn = nil @ldap_objects = [] + @registry_values = {} @fqdns = {} @certificate_details = {} # Initialize to empty hash since we want to only keep one copy of each certificate template along with its details. @@ -821,12 +1082,30 @@ def run @certificate_details[certificate_symbol] = build_certificate_details(template) end + registry_values = enum_registry_values if datastore['RUN_REGISTRY_CHECKS'] + find_esc1_vuln_cert_templates find_esc2_vuln_cert_templates find_esc3_vuln_cert_templates find_esc4_vuln_cert_templates + + if registry_values.blank? + find_esc9_vuln_cert_templates + find_esc10_vuln_cert_templates + else + if registry_values[:strong_certificate_binding_enforcement] != 2 + find_esc9_vuln_cert_templates + end + if registry_values[:strong_certificate_binding_enforcement] == 1 || registry_values[:certificate_mapping_methods] & 4 > 0 + find_esc10_vuln_cert_templates + end + end + find_esc13_vuln_cert_templates find_esc15_vuln_cert_templates + if registry_values && registry_values[:disable_extension_list]&.include?('1.3.6.1.4.1.311.25.2') + find_esc16_vuln_cert_templates + end find_enrollable_vuln_certificate_templates print_vulnerable_cert_info