From 3637aa996cd41aeee3f72a49dbb740a820184b7f Mon Sep 17 00:00:00 2001 From: Sylvain Heiniger Date: Thu, 15 Feb 2024 14:27:20 +0000 Subject: [PATCH 1/4] Adds ESC13 support --- certipy/commands/find.py | 310 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 308 insertions(+), 2 deletions(-) diff --git a/certipy/commands/find.py b/certipy/commands/find.py index 8b23dd8..a07175d 100755 --- a/certipy/commands/find.py +++ b/certipy/commands/find.py @@ -90,6 +90,7 @@ def __init__( stdout: bool = False, output: str = None, enabled: bool = False, + oids: bool = False, vulnerable: bool = False, hide_admins: bool = False, dc_only: bool = False, @@ -106,6 +107,7 @@ def __init__( self.stdout = stdout self.output = output self.enabled = enabled + self.oids = oids self.vuln = vulnerable self.hide_admins = hide_admins self.dc_only = dc_only @@ -231,6 +233,45 @@ def find(self): ) ) + logging.info("Finding issuance policies") + + oids = self.get_issuance_policies() + + logging.info( + "Found %d issuance polic%s" + % ( + len(cas), + "ies" if len(cas) != 1 else "y", + ) + ) + + no_enabled_oids = 0 + for template in templates: + object_id = template.get("objectGUID").lstrip("{").rstrip("}") + issuance_policy = template.get_raw("msPKI-Certificate-Policy") + for oid in oids: + if oid.get_raw("msPKI-Cert-Template-OID") == issuance_policy: + no_enabled_oids += 1 + linked_group = b''.join(oid.get_raw("msDS-OIDToGroupLink")).decode() + if "templates" in oid["attributes"].keys(): + oid.get("templates").append(template.get("name")) + oid.get("templates_ids").append(object_id) + else: + oid.set("templates", [template.get("name")]) + oid.set("templates_ids", [object_id]) + oid.set("linked_group", linked_group) + template.set("issuance_policy", oid.get("displayName")) + template.set("issuance_policy_linked_group", linked_group) + + logging.info( + "Found %d OID%s linked to %s" + % ( + no_enabled_oids, + "s" if no_enabled_oids != 1 else "", + "templates" if no_enabled_oids != 1 else "a template", + ) + ) + for ca in cas: if self.dc_only: @@ -455,7 +496,7 @@ def find(self): self.output_bloodhound_data(prefix, templates, cas) if self.text or self.json or not_specified: - output = self.get_output_for_text_and_json(templates, cas) + output = self.get_output_for_text_and_json(templates, cas, oids) if self.text or not_specified: if self.stdout: @@ -481,6 +522,7 @@ def get_output_for_text_and_json( ): ca_entries = {} template_entries = {} + oids_entries ={} for template in templates: if self.enabled and template.get("enabled") is not True: @@ -516,6 +558,24 @@ def get_output_for_text_and_json( ca_entries[len(ca_entries)] = entry + if self.oids is True: + for oid in oids: + vulnerabilities = self.get_oid_vulnerabilities(oid) + if self.vuln and len(vulnerabilities) == 0: + continue + + entry = OrderedDict() + entry = self.get_oid_properties(oid, entry) + + permissions = self.get_oid_permissions(template) + if len(permissions) > 0: + entry["Permissions"] = permissions + + if len(vulnerabilities) > 0: + entry["[!] Vulnerabilities"] = vulnerabilities + + oids_entries[len(oids_entries)] = entry + output = {} if len(ca_entries) == 0: @@ -530,6 +590,13 @@ def get_output_for_text_and_json( else: output["Certificate Templates"] = template_entries + if len(oids_entries) == 0: + output[ + "Issuance Policies" + ] = "[!] Could not find any issuance policy" + else: + output["Issuance Policies"] = oids_entries + return output def output_bloodhound_data( @@ -731,6 +798,7 @@ def get_certificate_templates(self) -> List[LDAPEntry]: "msPKI-Enrollment-Flag", "msPKI-Private-Key-Flag", "msPKI-Certificate-Name-Flag", + "msPKI-Certificate-Policy", "msPKI-Minimal-Key-Size", "msPKI-RA-Signature", "pKIExtendedKeyUsage", @@ -742,6 +810,25 @@ def get_certificate_templates(self) -> List[LDAPEntry]: return templates + def get_issuance_policies(self) -> List[LDAPEntry]: + templates = self.connection.search( + "(objectclass=msPKI-Enterprise-Oid)", + search_base="CN=OID,CN=Public Key Services,CN=Services,%s" + % self.connection.configuration_path, + attributes=[ + "cn", + "name", + "displayName", + "msDS-OIDToGroupLink", + "msPKI-Cert-Template-OID", + "nTSecurityDescriptor", + "objectGUID", + ], + query_sd=True, + ) + + return templates + def get_certificate_authorities(self) -> List[LDAPEntry]: cas = self.connection.search( "(&(objectClass=pKIEnrollmentService))", @@ -830,7 +917,9 @@ def get_template_properties( "authorized_signatures_required": "Authorized Signatures Required", "validity_period": "Validity Period", "renewal_period": "Renewal Period", - "msPKI-Minimal-Key-Size": "Minimum RSA Key Length" + "msPKI-Minimal-Key-Size": "Minimum RSA Key Length", + "msPKI-Certificate-Policy": "Issuance Policy", + "issuance_policy_linked_group" : "Linked Group" } if template_properties is None: @@ -967,6 +1056,19 @@ def list_sids(sids: List[str]): enrollable_sids ) + # ESC13 + if ( + user_can_enroll + and template.get("client_authentication") + and template.get("msPKI-Certificate-Policy") + and template.get("issuance_policy_linked_group") + ): + vulnerabilities[ + "ESC13" + ] = "%s can enroll, template allows client authentication and issuance policy is linked to group %s" % (list_sids( + enrollable_sids + ), template.get("issuance_policy_linked_group")) + # ESC4 security = CertifcateSecurity(template.get("nTSecurityDescriptor")) owner_sid = security.owner @@ -1170,6 +1272,210 @@ def ca_has_vulnerable_acl(self, ca: LDAPEntry): return has_vulnerable_acl, vulnerable_acl_sids + def get_oid_properties( + self, oid: LDAPEntry, oid_properties: dict = None + ) -> dict: + properties_map = { + "cn": "Issuance Policy Name", + "displayName": "Display Name", + "templates": "Certificate Template(s) linking to this policy", + "linked_group" : "Linked Group" + } + + if oid_properties is None: + oid_properties = OrderedDict() + + for property_key, property_display in properties_map.items(): + property_value = oid.get(property_key) + if property_value is None: + continue + oid_properties[property_display] = property_value + + return oid_properties + + def get_oid_permissions(self, oid: LDAPEntry): + security = OIDSecurity(oid.get("nTSecurityDescriptor")) + permissions = {} + enrollment_permissions = {} + enrollment_rights = [] + all_extended_rights = [] + + for sid, rights in security.aces.items(): + if self.hide_admins and is_admin_sid(sid): + continue + + if ( + EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"] + ): + enrollment_rights.append(self.connection.lookup_sid(sid).get("name")) + if ( + EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"] + in rights["extended_rights"] + ): + all_extended_rights.append(self.connection.lookup_sid(sid).get("name")) + + if len(enrollment_rights) > 0: + enrollment_permissions["Enrollment Rights"] = enrollment_rights + + if len(all_extended_rights) > 0: + enrollment_permissions["All Extended Rights"] = all_extended_rights + + if len(enrollment_permissions) > 0: + permissions["Enrollment Permissions"] = enrollment_permissions + + object_control_permissions = {} + if not self.hide_admins or not is_admin_sid(security.owner): + object_control_permissions["Owner"] = self.connection.lookup_sid( + security.owner + ).get("name") + + rights_mapping = [ + (CERTIFICATE_RIGHTS.GENERIC_ALL, [], "Full Control Principals"), + (CERTIFICATE_RIGHTS.WRITE_OWNER, [], "Write Owner Principals"), + (CERTIFICATE_RIGHTS.WRITE_DACL, [], "Write Dacl Principals"), + ( + CERTIFICATE_RIGHTS.WRITE_PROPERTY, + [], + "Write Property Principals", + ), + ] + for sid, rights in security.aces.items(): + if self.hide_admins and is_admin_sid(sid): + continue + + rights = rights["rights"] + sid = self.connection.lookup_sid(sid).get("name") + + for (right, principal_list, _) in rights_mapping: + if right in rights: + principal_list.append(sid) + + for _, rights, name in rights_mapping: + if len(rights) > 0: + object_control_permissions[name] = rights + + if len(object_control_permissions) > 0: + permissions["Object Control Permissions"] = object_control_permissions + + return permissions + + def get_oid_vulnerabilities(self, oid: LDAPEntry): + def list_sids(sids: List[str]): + sids_mapping = list( + map( + lambda sid: repr(self.connection.lookup_sid(sid).get("name")), + sids, + ) + ) + if len(sids_mapping) == 1: + return sids_mapping[0] + + return ", ".join(sids_mapping[:-1]) + " and " + sids_mapping[-1] + + if template.get("vulnerabilities"): + return template.get("vulnerabilities") + + vulnerabilities = {} + + user_can_enroll, enrollable_sids = self.can_user_enroll_in_template(template) + + if ( + not template.get("requires_manager_approval") + and not template.get("authorized_signatures_required") > 0 + ): + # ESC1 + if ( + user_can_enroll + and template.get("enrollee_supplies_subject") + and template.get("client_authentication") + ): + vulnerabilities["ESC1"] = ( + "%s can enroll, enrollee supplies subject and template allows client authentication" + % list_sids(enrollable_sids) + ) + + # ESC2 + if user_can_enroll and template.get("any_purpose"): + vulnerabilities["ESC2"] = ( + "%s can enroll and template can be used for any purpose" + % list_sids(enrollable_sids) + ) + + # ESC3 + if user_can_enroll and template.get("enrollment_agent"): + vulnerabilities["ESC3"] = ( + "%s can enroll and template has Certificate Request Agent EKU set" + % list_sids(enrollable_sids) + ) + + # ESC9 + if user_can_enroll and template.get("no_security_extension"): + vulnerabilities[ + "ESC9" + ] = "%s can enroll and template has no security extension" % list_sids( + enrollable_sids + ) + + # ESC13 + if ( + user_can_enroll + and template.get("client_authentication") + and template.get("msPKI-Certificate-Policy") + and template.get("issuance_policy_linked_group") + ): + vulnerabilities[ + "ESC13" + ] = "%s can enroll, template allows client authentication and issuance policy is linked to group %s" % (list_sids( + enrollable_sids + ), template.get("issuance_policy_linked_group")) + + # ESC4 + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + owner_sid = security.owner + + if owner_sid in self.connection.get_user_sids(self.target.username): + vulnerabilities[ + "ESC4" + ] = "Template is owned by %s" % self.connection.lookup_sid(owner_sid).get( + "name" + ) + else: + # No reason to show if user is already owner + has_vulnerable_acl, vulnerable_acl_sids = self.template_has_vulnerable_acl( + template + ) + if has_vulnerable_acl: + vulnerabilities["ESC4"] = "%s has dangerous permissions" % list_sids( + vulnerable_acl_sids + ) + + return vulnerabilities + + def oid_has_vulnerable_acl(self, oid: LDAPEntry): + has_vulnerable_acl = False + + security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + aces = security.aces + vulnerable_acl_sids = [] + for sid, rights in aces.items(): + if sid not in self.connection.get_user_sids(self.target.username): + continue + + ad_rights = rights["rights"] + if any( + right in ad_rights + for right in [ + CERTIFICATE_RIGHTS.GENERIC_ALL, + CERTIFICATE_RIGHTS.WRITE_OWNER, + CERTIFICATE_RIGHTS.WRITE_DACL, + CERTIFICATE_RIGHTS.WRITE_PROPERTY, + ] + ): + vulnerable_acl_sids.append(sid) + has_vulnerable_acl = True + + return has_vulnerable_acl, vulnerable_acl_sids + def entry(options: argparse.Namespace) -> None: target = Target.from_options(options, dc_as_target=True) From d4d4afca9b15b94d0117ad971cbdc0ff23f9379a Mon Sep 17 00:00:00 2001 From: Sylvain Heiniger Date: Thu, 15 Feb 2024 15:10:57 +0000 Subject: [PATCH 2/4] Adds support for multiple issuance policies --- certipy/commands/find.py | 123 +++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 71 deletions(-) diff --git a/certipy/commands/find.py b/certipy/commands/find.py index a07175d..7ad70d3 100755 --- a/certipy/commands/find.py +++ b/certipy/commands/find.py @@ -248,9 +248,25 @@ def find(self): no_enabled_oids = 0 for template in templates: object_id = template.get("objectGUID").lstrip("{").rstrip("}") - issuance_policy = template.get_raw("msPKI-Certificate-Policy") + issuance_policies = template.get_raw("msPKI-Certificate-Policy") + if not isinstance(issuance_policies, list): + if issuance_policies is None: + issuance_policies = [] + else: + issuance_policies = [issuance_policies] + + issuance_policies = list(map(lambda x: x.decode(), issuance_policies)) + + issuance_policies = list( + map( + lambda x: OID_TO_STR_MAP[x] if x in OID_TO_STR_MAP else x, + issuance_policies, + ) + ) + template.set("issuance_policies", issuance_policies) + for oid in oids: - if oid.get_raw("msPKI-Cert-Template-OID") == issuance_policy: + if oid.get_raw("msPKI-Cert-Template-OID") in issuance_policies: no_enabled_oids += 1 linked_group = b''.join(oid.get_raw("msDS-OIDToGroupLink")).decode() if "templates" in oid["attributes"].keys(): @@ -260,8 +276,10 @@ def find(self): oid.set("templates", [template.get("name")]) oid.set("templates_ids", [object_id]) oid.set("linked_group", linked_group) - template.set("issuance_policy", oid.get("displayName")) - template.set("issuance_policy_linked_group", linked_group) + if "issuance_policies_linked_groups" in template["attributes"].keys(): + template.get("issuance_policies_linked_groups").append(linked_group) + else: + template.set("issuance_policies_linked_groups", [linked_group]) logging.info( "Found %d OID%s linked to %s" @@ -918,8 +936,8 @@ def get_template_properties( "validity_period": "Validity Period", "renewal_period": "Renewal Period", "msPKI-Minimal-Key-Size": "Minimum RSA Key Length", - "msPKI-Certificate-Policy": "Issuance Policy", - "issuance_policy_linked_group" : "Linked Group" + "msPKI-Certificate-Policy": "Issuance Policies", + "issuance_policies_linked_groups" : "Linked Groups" } if template_properties is None: @@ -1061,13 +1079,13 @@ def list_sids(sids: List[str]): user_can_enroll and template.get("client_authentication") and template.get("msPKI-Certificate-Policy") - and template.get("issuance_policy_linked_group") + and template.get("issuance_policies_linked_groups") ): vulnerabilities[ "ESC13" ] = "%s can enroll, template allows client authentication and issuance policy is linked to group %s" % (list_sids( enrollable_sids - ), template.get("issuance_policy_linked_group")) + ), template.get("issuance_policies_linked_groups")) # ESC4 security = CertifcateSecurity(template.get("nTSecurityDescriptor")) @@ -1294,70 +1312,33 @@ def get_oid_properties( return oid_properties def get_oid_permissions(self, oid: LDAPEntry): - security = OIDSecurity(oid.get("nTSecurityDescriptor")) - permissions = {} - enrollment_permissions = {} - enrollment_rights = [] - all_extended_rights = [] - - for sid, rights in security.aces.items(): - if self.hide_admins and is_admin_sid(sid): - continue - - if ( - EXTENDED_RIGHTS_NAME_MAP["Enroll"] in rights["extended_rights"] - ): - enrollment_rights.append(self.connection.lookup_sid(sid).get("name")) - if ( - EXTENDED_RIGHTS_NAME_MAP["All-Extended-Rights"] - in rights["extended_rights"] - ): - all_extended_rights.append(self.connection.lookup_sid(sid).get("name")) - - if len(enrollment_rights) > 0: - enrollment_permissions["Enrollment Rights"] = enrollment_rights - - if len(all_extended_rights) > 0: - enrollment_permissions["All Extended Rights"] = all_extended_rights - - if len(enrollment_permissions) > 0: - permissions["Enrollment Permissions"] = enrollment_permissions - - object_control_permissions = {} - if not self.hide_admins or not is_admin_sid(security.owner): - object_control_permissions["Owner"] = self.connection.lookup_sid( - security.owner - ).get("name") - - rights_mapping = [ - (CERTIFICATE_RIGHTS.GENERIC_ALL, [], "Full Control Principals"), - (CERTIFICATE_RIGHTS.WRITE_OWNER, [], "Write Owner Principals"), - (CERTIFICATE_RIGHTS.WRITE_DACL, [], "Write Dacl Principals"), - ( - CERTIFICATE_RIGHTS.WRITE_PROPERTY, - [], - "Write Property Principals", - ), - ] - for sid, rights in security.aces.items(): - if self.hide_admins and is_admin_sid(sid): - continue - - rights = rights["rights"] - sid = self.connection.lookup_sid(sid).get("name") + security = ActiveDirectorySecurity(oid.get("nTSecurityDescriptor")) + oid_permissions = {} + access_rights = {} + if security is not None: + if not self.hide_admins or not is_admin_sid(security.owner): + oid_permissions["Owner"] = self.connection.lookup_sid( + security.owner + ).get("name") - for (right, principal_list, _) in rights_mapping: - if right in rights: - principal_list.append(sid) + for sid, rights in security.aces.items(): + if self.hide_admins and is_admin_sid(sid): + continue + oid_rights = rights["rights"].to_list() + for oid_right in oid_rights: + if oid_right not in access_rights: + access_rights[ca_right] = [ + self.connection.lookup_sid(sid).get("name") + ] + else: + access_rights[ca_right].append( + self.connection.lookup_sid(sid).get("name") + ) - for _, rights, name in rights_mapping: - if len(rights) > 0: - object_control_permissions[name] = rights + oid_permissions["Access Rights"] = access_rights - if len(object_control_permissions) > 0: - permissions["Object Control Permissions"] = object_control_permissions + return oid_permissions - return permissions def get_oid_vulnerabilities(self, oid: LDAPEntry): def list_sids(sids: List[str]): @@ -1421,13 +1402,13 @@ def list_sids(sids: List[str]): user_can_enroll and template.get("client_authentication") and template.get("msPKI-Certificate-Policy") - and template.get("issuance_policy_linked_group") + and template.get("issuance_policy_linked_groups") ): vulnerabilities[ "ESC13" - ] = "%s can enroll, template allows client authentication and issuance policy is linked to group %s" % (list_sids( + ] = "%s can enroll, template allows client authentication and issuance policy is linked to group(s) %s" % (list_sids( enrollable_sids - ), template.get("issuance_policy_linked_group")) + ), template.get("issuance_policy_linked_groups")) # ESC4 security = CertifcateSecurity(template.get("nTSecurityDescriptor")) From 03dcedee38cd95210e123f7ab979c301409250e0 Mon Sep 17 00:00:00 2001 From: Sylvain Heiniger Date: Thu, 15 Feb 2024 15:47:50 +0000 Subject: [PATCH 3/4] Adds verbose output and detection of Issuance Policies anomalies --- certipy/commands/find.py | 121 ++++++++----------------------- certipy/commands/parsers/find.py | 5 ++ certipy/lib/constants.py | 6 ++ certipy/lib/security.py | 4 + 4 files changed, 46 insertions(+), 90 deletions(-) diff --git a/certipy/commands/find.py b/certipy/commands/find.py index 7ad70d3..930f90f 100755 --- a/certipy/commands/find.py +++ b/certipy/commands/find.py @@ -12,6 +12,7 @@ from asn1crypto import x509 from certipy.lib.constants import ( + ISSUANCE_POLICY_RIGHTS, CERTIFICATE_RIGHTS, CERTIFICATION_AUTHORITY_RIGHTS, EXTENDED_RIGHTS_MAP, @@ -29,6 +30,7 @@ from certipy.lib.security import ( ActiveDirectorySecurity, CertifcateSecurity, + IssuancePolicySecurity, is_admin_sid, ) from certipy.lib.target import Target @@ -248,25 +250,16 @@ def find(self): no_enabled_oids = 0 for template in templates: object_id = template.get("objectGUID").lstrip("{").rstrip("}") - issuance_policies = template.get_raw("msPKI-Certificate-Policy") + issuance_policies = template.get("msPKI-Certificate-Policy") if not isinstance(issuance_policies, list): if issuance_policies is None: issuance_policies = [] else: issuance_policies = [issuance_policies] - - issuance_policies = list(map(lambda x: x.decode(), issuance_policies)) - - issuance_policies = list( - map( - lambda x: OID_TO_STR_MAP[x] if x in OID_TO_STR_MAP else x, - issuance_policies, - ) - ) template.set("issuance_policies", issuance_policies) for oid in oids: - if oid.get_raw("msPKI-Cert-Template-OID") in issuance_policies: + if oid.get("msPKI-Cert-Template-OID") in issuance_policies: no_enabled_oids += 1 linked_group = b''.join(oid.get_raw("msDS-OIDToGroupLink")).decode() if "templates" in oid["attributes"].keys(): @@ -536,7 +529,7 @@ def find(self): ) def get_output_for_text_and_json( - self, templates: List[LDAPEntry], cas: List[LDAPEntry] + self, templates: List[LDAPEntry], cas: List[LDAPEntry], oids: List[LDAPEntry] ): ca_entries = {} template_entries = {} @@ -585,7 +578,7 @@ def get_output_for_text_and_json( entry = OrderedDict() entry = self.get_oid_properties(oid, entry) - permissions = self.get_oid_permissions(template) + permissions = self.get_oid_permissions(oid) if len(permissions) > 0: entry["Permissions"] = permissions @@ -608,12 +601,13 @@ def get_output_for_text_and_json( else: output["Certificate Templates"] = template_entries - if len(oids_entries) == 0: - output[ - "Issuance Policies" - ] = "[!] Could not find any issuance policy" - else: - output["Issuance Policies"] = oids_entries + if self.oids is True: + if len(oids_entries) == 0: + output[ + "Issuance Policies" + ] = "[!] Could not find any issuance policy" + else: + output["Issuance Policies"] = oids_entries return output @@ -1296,7 +1290,7 @@ def get_oid_properties( properties_map = { "cn": "Issuance Policy Name", "displayName": "Display Name", - "templates": "Certificate Template(s) linking to this policy", + "templates": "Certificate Template(s)", "linked_group" : "Linked Group" } @@ -1312,7 +1306,7 @@ def get_oid_properties( return oid_properties def get_oid_permissions(self, oid: LDAPEntry): - security = ActiveDirectorySecurity(oid.get("nTSecurityDescriptor")) + security = IssuancePolicySecurity(oid.get("nTSecurityDescriptor")) oid_permissions = {} access_rights = {} if security is not None: @@ -1327,11 +1321,11 @@ def get_oid_permissions(self, oid: LDAPEntry): oid_rights = rights["rights"].to_list() for oid_right in oid_rights: if oid_right not in access_rights: - access_rights[ca_right] = [ + access_rights[oid_right] = [ self.connection.lookup_sid(sid).get("name") ] else: - access_rights[ca_right].append( + access_rights[oid_right].append( self.connection.lookup_sid(sid).get("name") ) @@ -1353,80 +1347,27 @@ def list_sids(sids: List[str]): return ", ".join(sids_mapping[:-1]) + " and " + sids_mapping[-1] - if template.get("vulnerabilities"): - return template.get("vulnerabilities") + if oid.get("vulnerabilities"): + return oid.get("vulnerabilities") vulnerabilities = {} - user_can_enroll, enrollable_sids = self.can_user_enroll_in_template(template) - - if ( - not template.get("requires_manager_approval") - and not template.get("authorized_signatures_required") > 0 - ): - # ESC1 - if ( - user_can_enroll - and template.get("enrollee_supplies_subject") - and template.get("client_authentication") - ): - vulnerabilities["ESC1"] = ( - "%s can enroll, enrollee supplies subject and template allows client authentication" - % list_sids(enrollable_sids) - ) - - # ESC2 - if user_can_enroll and template.get("any_purpose"): - vulnerabilities["ESC2"] = ( - "%s can enroll and template can be used for any purpose" - % list_sids(enrollable_sids) - ) - - # ESC3 - if user_can_enroll and template.get("enrollment_agent"): - vulnerabilities["ESC3"] = ( - "%s can enroll and template has Certificate Request Agent EKU set" - % list_sids(enrollable_sids) - ) - - # ESC9 - if user_can_enroll and template.get("no_security_extension"): - vulnerabilities[ - "ESC9" - ] = "%s can enroll and template has no security extension" % list_sids( - enrollable_sids - ) - - # ESC13 - if ( - user_can_enroll - and template.get("client_authentication") - and template.get("msPKI-Certificate-Policy") - and template.get("issuance_policy_linked_groups") - ): - vulnerabilities[ - "ESC13" - ] = "%s can enroll, template allows client authentication and issuance policy is linked to group(s) %s" % (list_sids( - enrollable_sids - ), template.get("issuance_policy_linked_groups")) - - # ESC4 - security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + # ESC13 + security = IssuancePolicySecurity(oid.get("nTSecurityDescriptor")) owner_sid = security.owner if owner_sid in self.connection.get_user_sids(self.target.username): vulnerabilities[ - "ESC4" - ] = "Template is owned by %s" % self.connection.lookup_sid(owner_sid).get( + "ESC13" + ] = "Issuance Policy OID is owned by %s" % self.connection.lookup_sid(owner_sid).get( "name" ) else: - # No reason to show if user is already owner - has_vulnerable_acl, vulnerable_acl_sids = self.template_has_vulnerable_acl( - template + has_vulnerable_acl, vulnerable_acl_sids = self.oid_has_vulnerable_acl( + oid ) if has_vulnerable_acl: - vulnerabilities["ESC4"] = "%s has dangerous permissions" % list_sids( + vulnerabilities["ESC13"] = "%s has dangerous permissions" % list_sids( vulnerable_acl_sids ) @@ -1435,7 +1376,7 @@ def list_sids(sids: List[str]): def oid_has_vulnerable_acl(self, oid: LDAPEntry): has_vulnerable_acl = False - security = CertifcateSecurity(template.get("nTSecurityDescriptor")) + security = IssuancePolicySecurity(oid.get("nTSecurityDescriptor")) aces = security.aces vulnerable_acl_sids = [] for sid, rights in aces.items(): @@ -1446,10 +1387,10 @@ def oid_has_vulnerable_acl(self, oid: LDAPEntry): if any( right in ad_rights for right in [ - CERTIFICATE_RIGHTS.GENERIC_ALL, - CERTIFICATE_RIGHTS.WRITE_OWNER, - CERTIFICATE_RIGHTS.WRITE_DACL, - CERTIFICATE_RIGHTS.WRITE_PROPERTY, + ISSUANCE_POLICY_RIGHTS.GENERIC_ALL, + ISSUANCE_POLICY_RIGHTS.WRITE_OWNER, + ISSUANCE_POLICY_RIGHTS.WRITE_DACL, + ISSUANCE_POLICY_RIGHTS.WRITE_PROPERTY, ] ): vulnerable_acl_sids.append(sid) diff --git a/certipy/commands/parsers/find.py b/certipy/commands/parsers/find.py index 7e71bf6..30aff25 100755 --- a/certipy/commands/parsers/find.py +++ b/certipy/commands/parsers/find.py @@ -65,6 +65,11 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable action="store_true", help="Show only vulnerable certificate templates based on nested group memberships. Does not affect BloodHound output", ) + group.add_argument( + "-oids", + action="store_true", + help="Show OIDs (Issuance Policies) and their properties.", + ) group.add_argument( "-hide-admins", action="store_true", diff --git a/certipy/lib/constants.py b/certipy/lib/constants.py index 1b6d13e..771641c 100755 --- a/certipy/lib/constants.py +++ b/certipy/lib/constants.py @@ -268,6 +268,12 @@ def to_list(self): filtered_members.append(member) return filtered_members +class ISSUANCE_POLICY_RIGHTS(IntFlag): + GENERIC_READ = 131220 + GENERIC_ALL = 983551 + WRITE_OWNER = 524288 + WRITE_DACL = 262144 + WRITE_PROPERTY = 32 # https://github.com/GhostPack/Certify/blob/2b1530309c0c5eaf41b2505dfd5a68c83403d031/Certify/Domain/CertificateAuthority.cs#L11 class CERTIFICATION_AUTHORITY_RIGHTS(IntFlag): diff --git a/certipy/lib/security.py b/certipy/lib/security.py index cf2044d..0e9d66d 100755 --- a/certipy/lib/security.py +++ b/certipy/lib/security.py @@ -10,6 +10,7 @@ ACTIVE_DIRECTORY_RIGHTS, CERTIFICATE_RIGHTS, CERTIFICATION_AUTHORITY_RIGHTS, + ISSUANCE_POLICY_RIGHTS, ) @@ -59,6 +60,9 @@ class CASecurity(ActiveDirectorySecurity): class CertifcateSecurity(ActiveDirectorySecurity): RIGHTS_TYPE = CERTIFICATE_RIGHTS +class IssuancePolicySecurity(ActiveDirectorySecurity): + RIGHTS_TYPE = ISSUANCE_POLICY_RIGHTS + def is_admin_sid(sid: str): return ( From a30428ba6aa0448c91956f00a039c0a51a2b1c51 Mon Sep 17 00:00:00 2001 From: Sylvain Heiniger Date: Wed, 27 Nov 2024 17:22:07 +0100 Subject: [PATCH 4/4] Adds check for empty msDS-OIDToGroupLink See https://github.com/ly4k/Certipy/pull/196#issuecomment-2504164153 --- certipy/commands/find.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/certipy/commands/find.py b/certipy/commands/find.py index 930f90f..0dc9bdd 100755 --- a/certipy/commands/find.py +++ b/certipy/commands/find.py @@ -268,11 +268,12 @@ def find(self): else: oid.set("templates", [template.get("name")]) oid.set("templates_ids", [object_id]) - oid.set("linked_group", linked_group) - if "issuance_policies_linked_groups" in template["attributes"].keys(): - template.get("issuance_policies_linked_groups").append(linked_group) - else: - template.set("issuance_policies_linked_groups", [linked_group]) + if linked_group: + oid.set("linked_group", linked_group) + if "issuance_policies_linked_groups" in template["attributes"].keys(): + template.get("issuance_policies_linked_groups").append(linked_group) + else: + template.set("issuance_policies_linked_groups", [linked_group]) logging.info( "Found %d OID%s linked to %s"