Skip to content

[Bug]: ADCS ESC1/ESC2 detectors emit edges for disabled and unpublished certificate templates → false-positive T0 attack paths #16

Description

@FLX-0x00

ADscan version

ADscan launcher: v11.2.0, adscan/adscan-lite:latest

Host OS

Arch linux-lts

Docker version

Docker version 29.7.2, build a7dcaa6fdb

Command and sanitized output

start_auth

Expected behavior

Summary

adscan reports attack paths of the form Authenticated Users@WELLKNOWN → ADCSESC1 → Domain Admins → ... → DCSync (Direct, T0/Domain Breaker) that are not exploitable. Tracing the edge back through the inventory data and the detector source shows that the ESC1 (and ESC2) edges are emitted against certificate templates that are not enabled and - in the worst case - not published on any CA. Enrollment against such templates is rejected server-side, so the entire attack path is a false positive.

Environment

  • adscan 11.2.0 (adscan-lite container)
  • Single-forest AD lab, one enterprise CA (IssuingCA), 51 templates, 15 enabled
  • Cross-checked with certipy find -enabled -json (v5.1.0)

Evidence

1. The reported edge targets a disabled, unpublished template

The flagged template in inventory/adcs_attack_steps.json (sanitized):

{
 "relation": "ADCSESC1",
 "source_object_id": "S-1-5-11",
 "source_name": "Authenticated Users@WELLKNOWN",
 "target_kind": "CertTemplate",
 "target_name": "LEGACYAPP@CORP.LOCAL"
}

The same template in inventory/adcs_templates.json:

"enabled": null,
"properties": {
  "mspki_certificate_name_flag": 1,
  "mspki_enrollment_flag": 8,
  "mspki_ra_signature": 0,
  "pki_extended_key_usage": ["1.3.6.1.5.5.7.3.2"]
}

Note "enabled": null - the collector does not record publish/enabled state at all.

2. Ground truth from Certipy

Template Name   : LegacyApp
Enabled         : False
Certificate Authorities : []          <-- published on NO CA
Enrollee Supplies Subject : True
Client Authentication     : True
Requires Manager Approval : False
Enrollment Rights         : ... Domain Admins, Authenticated Users

Enrollment attempts against it fail (template not published on the enterprise CA), so no certificate can ever be minted → no PKINIT → no DA.

3. Code tracing

Collector (adscan_internal/services/collector/adcs_collector.py / adcs_templates.json output): template objects are created without evaluating the template↔CA publication relationship; enabled is left null.

Detector (adscan_internal/services/collector/adcs_detectors/esc1.py, detect_esc1()):

def detect_esc1(*, template_node, template_acl_edges, domain) -> list[CollectorEdge]:
    if template_node.kind != "CertTemplate":
        return []

    name_flag = template_int_property(template_node, "mspki_certificate_name_flag", ...)
    if not (name_flag & ENROLLEE_SUPPLIES_SUBJECT):
        return []

    enrollment_flag = template_int_property(template_node, "mspki_enrollment_flag", ...)
    if enrollment_flag & PEND_ALL_REQUESTS:
        return []
    ...
    return [make_adcs_edge(principal_sid=sid, template_node=template_node, esc="1")
            for sid in get_enroll_principal_sids(template_acl_edges)]

There is no check for:

  1. enabled / CA publication state of the template, or
  2. whether the CA referenced by the template exists/is reachable.

The same applies to esc2.py (identical shape, also flagged the disabled template for Authenticated Users).

Consequence in the attack graph: Authenticated Users@WELLKNOWN (SID S-1-5-11) gains an ADCSESC1 transition against a T0 template, the path engine then expands it to → Domain Admins → Administrators → DCSync and surfaces it as a ready-to-execute T0/Domain-Breaker path with "Execution Readiness: 1 ready".

Expected behavior

ESC detectors (esc1.py … at minimum the enrollment-based family: ESC1/ESC2/ESC4/ESC5) should be gated on template publication state:

  • Skip templates with no entry in the CA↔template publish relationship (Certificate Authorities: None), or
  • emit the edge but mark it as disabled / not_published so the attack-path engine (and the "Execute this attack path now?" prompt) filters it out.

At minimum the collector should persist the enabled/published flag instead of null.

Impact

False-positive T0 paths directly erode trust in the "Ready" attack-path ranking and the interactive execution prompt - the highest-severity finding in this particular engagement was unexploitable on manual verification, while the genuinely real ADCS issue (ESC8 web enrollment over HTTP) ranked lower.

Workaround (pentester-side)

Cross-check every ADCSESC* finding against certipy find -enabled -json before executing, and discard edges whose target template shows Enabled: false or an empty Certificate Authorities list.

Lab or environment context

No response

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions