Skip to content

Fix ESC1 false negatives in template enumeration - #317

Merged
ly4k merged 2 commits into
ly4k:mainfrom
vilacham:fix-esc1-false-negatives
Sep 30, 2025
Merged

ly4k merged 2 commits into
ly4k:mainfrom
vilacham:fix-esc1-false-negatives

Conversation

@vilacham

@vilacham vilacham commented Sep 5, 2025

Copy link
Copy Markdown
Contributor

Summary

This PR improves ESC1 detection in Certipy by addressing false negatives observed in specific template configurations. The change refines the logic that determines whether a template is exploitable for ESC1.

Background

According to Microsoft documentation (MS-CRTD: Certificate Templates Structure) enrollment permissions for a certificate template are evaluated based on two processing rules:

First rule

  • The requester SID matches the SID associated with the ACE.
  • The ACE type is ACCESS_ALLOWED_OBJECT_ACE.
  • The access mask has the bit 0x00000100 set (Control Access).
  • The ObjectType field corresponds to the Enroll GUID (0e10c968-78fb-11d2-90d4-00c04f79dc55).

Second rule

  • The requester SID matches the SID associated with the ACE.
  • The ACE type is ACCESS_ALLOWED_ACE.
  • The access mask has the bit 0x00000100 set.

These rules define how Active Directory evaluates whether a principal can enroll for a certificate. In addition to Microsoft’s specification, further practical details and lab-based examples can be found in my previous articles The Schrödinger’s ESC1 Vulnerability and The Schrödinger’s ESC1 Vulnerability: Benchmark Update, where these conditions were analyzed across different tool implementations.

Problem

In some scenarios, Certipy failed to flag templates as ESC1-vulnerable even when they were exploitable. This was reproducible in a controlled lab with eight dedicated templates (four vulnerable, four non-vulnerable).

Root Cause

In certipy/commands/find.py, the function can_user_enroll_in_template does not implement both enrollment-processing rules defined by Microsoft’s specification (MS-CRTD). The current logic effectively checks only Rule 1 by validating the presence of the Enroll (or All-Extended-Rights) object-specific extended right. Because an ACE with an ObjectType is, by definition, an ACCESS_ALLOWED_OBJECT_ACE, the function implicitly assumes the ACE type and does not verify it explicitly.

As a result, Rule 2 is not considered. This omission leads to false negatives in cases where enrollment is legitimately granted through a standard ACE with Control Access.

Solution

Key changes

  • certipy/lib/security.py (ActiveDirectorySecurity parser)

    • Record a new flag has_standard_control_access when a standard ACCESS_ALLOWED_ACE has the Control Access bit (0x00000100) set.
    • Continue collecting object-specific extended-rights GUIDs (including Enroll and All-Extended-Rights).
  • certipy/commands/find.py (can_user_enroll_in_template)

    • Apply Rule 1 (MS-CRTD): grant enrollment if there is an object ACE with Control Access whose ObjectType is the Enroll GUID (detected via presence of Enroll in extended_rights and the EXTENDED_RIGHT bit).
    • Preserve existing handling of All-Extended-Rights.
    • Apply Rule 2 (MS-CRTD): grant enrollment if there is a standard ACE with the Control Access bit set (via the new has_standard_control_access flag).
    • Keep GENERIC_ALL as an additional grant condition.

What this means

Enrollment rights are now determined exactly per MS-CRTD:

  • Rule 1: ACCESS_ALLOWED_OBJECT_ACE + Control Access + Enroll GUID.
  • Rule 2: ACCESS_ALLOWED_ACE (standard) + Control Access bit.
  • Still accepts All-Extended-Rights and Generic All as valid grants.

This aligns Certipy’s behavior with the spec and eliminates the observed false negatives for templates that rely on a standard Control Access grant.

Testing

  • Environment: Terraform + GCP lab consisting of a Domain Controller, a Certificate Authority, a Windows Server (for .exe and .ps1 tools), and an Ubuntu machine (for Python tools).

  • Vulnerable templates: Four vulnerable certificate templates, with screenshots of their security descriptors and the corresponding vulnerable ACEs included.

    • TestCase1

      • ACE Type: ACCESS_ALLOWED_OBJECT_ACE
      • Principal: Domain Users group
      • Access Mask: 0x00000130
      • Object GUID: 0e10c968–78fb-11d2–90d4–00c04f79dc55
    • TestCase2

      • ACE Type: ACCESS_ALLOWED_OBJECT_ACE
      • Principal: Specific low-privileged user (alice)
      • Access Mask: 0x00000130
      • Object GUID: 0e10c968–78fb-11d2–90d4–00c04f79dc55
    • TestCase3

      • ACE Type: ACCESS_ALLOWED_ACE
      • Principal: Domain Users group
      • Access Mask: 0x00000100
    • TestCase4

      • ACE Type: ACCESS_ALLOWED_OBJECT_ACE
      • Principal: Specific low-privileged user (alice)
      • Access Mask: 0x00000100
  • Reproduction:

    1. Run certipy find using the modified version of Certipy (this branch).
      2025-09-05 01_39_27
      2025-09-05 01_40_11
      2025-09-05 01_40_49
      2025-09-05 01_41_24
      2025-09-05 01_41_46
      2025-09-05 01_42_20

    2. Run the same command using the original Certipy release for comparison.
      2025-09-05 01_44_42
      2025-09-05 01_46_46
      2025-09-05 01_47_12
      2025-09-05 01_47_33

    3. For the vulnerable cases where the original Certipy failed to detect ESC1 (false negatives), privilege escalation using certipy req and certipy auth was demonstrated to confirm that the templates were indeed exploitable.
      2025-09-05 01_53_34
      2025-09-05 01_55_10

Result

All four vulnerable templates are now correctly reported as ESC1 by the modified Certipy.

@ly4k

ly4k commented Sep 5, 2025

Copy link
Copy Markdown
Owner

Absolutely incredible PR and code change, thank you! The code fails a formatting check - I'll be very happy to merge once formatted with Black! :)

@vilacham

vilacham commented Sep 5, 2025

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I’ve formatted the code with Black, and the PR should now pass the formatting check.

@ly4k

ly4k commented Sep 30, 2025

Copy link
Copy Markdown
Owner

Thank you! :) Apologies for the delay

@ly4k
ly4k merged commit a80fe7c into ly4k:main Sep 30, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants