Query Details

HUNT 20 AD Certificate Enrollment Anomaly 30d

Query

// =========================================================
// HUNT-20 | AD-Certificate-Enrollment-Anomaly-30d
// Description : Full audit of all certificate requests
//               (EventID 4886 - request received, 4887 -
//               issued, 4888 - denied) from the CA audit
//               log. Surfaces accounts enrolling from
//               unexpected hosts, high-volume enrollments,
//               certificates with alternate SANs (ESC1),
//               enrollment agent requests (ESC3), and DC
//               certificate requests by non-DCs (ESC8).
// Period      : 30 days
// Use Case    : ADCS abuse, ESC1-ESC8 pattern hunting,
//               certificate theft post-mortem
// Tables      : SecurityEvent
// =========================================================

let Period = 30d;

// Build DC list
let DCList = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID == 4768
    | summarize DCs = make_set(toupper(Computer));

// Certificate requests (4886) and issuances (4887)
let CertRequests = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID in (4886, 4887, 4888)
    | extend RequesterName = tostring(column_ifexists("RequesterName", ""))
    | extend
        RequesterNorm   = tolower(RequesterName),
        CAHost          = Computer,
        Template        = tostring(column_ifexists("CertificateTemplateName", "")),
        EventType       = case(
            EventID == 4886, "Requested",
            EventID == 4887, "Issued",
            "Denied"
        )
    | extend
        // Detect SAN/requester mismatch (ESC1 indicator)
        HasSANOverride  = tostring(column_ifexists("SubjectAlternativeNames", "")) has "@"
                           or tostring(column_ifexists("SubjectAlternativeNames", "")) !has toupper(RequesterName),
        // Detect enrollment agent usage (ESC3)
        IsEnrollAgent   = RequesterName endswith "EA"
                           or Template =~ "Enrollment Agent"
                           or Template =~ "CEP Encryption",
        // Detect DC certificate by non-DC requester (ESC8)
        IsDCTemplate    = Template has_any ("DomainController", "KerberosAuthentication",
                                             "DirectoryEmailReplication", "DC_Auth"),
        IsOffHours      = hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) >= 20;

// Enrollment baseline per account → template pairing
let EnrollBaseline = CertRequests
    | where TimeGenerated < ago(7d)
    | summarize
        BaselineTemplates = make_set(Template, 20),
        BaselineHosts     = make_set(CAHost, 10)
      by RequesterNorm;

// Hunt window
CertRequests
| join kind=leftouter (EnrollBaseline) on RequesterNorm
| extend
    IsNewTemplate = not(BaselineTemplates has Template),
    IsNewCAHost   = not(BaselineHosts has CAHost)
// Determine if the requester is expected to request DC certs
| extend Ctx = toscalar(DCList | project DCs)
| extend IsDCRequester = set_has_element(Ctx, toupper(Computer))
| extend
    ESC1Risk = HasSANOverride and EventType == "Issued",
    ESC3Risk = IsEnrollAgent  and EventType == "Issued",
    ESC8Risk = IsDCTemplate   and not(IsDCRequester) and EventType == "Issued"
| summarize
    TotalRequests    = count(),
    Issued           = countif(EventType == "Issued"),
    Denied           = countif(EventType == "Denied"),
    ESC1Events       = countif(ESC1Risk),
    ESC3Events       = countif(ESC3Risk),
    ESC8Events       = countif(ESC8Risk),
    OffHoursRequests = countif(IsOffHours),
    NewTemplates     = make_set_if(Template, IsNewTemplate, 10),
    AllTemplates     = make_set(Template, 20),
    CAHosts          = make_set(CAHost, 10),
    LastRequest      = max(TimeGenerated),
    FirstRequest     = min(TimeGenerated)
  by RequesterNorm
| extend
    RiskScore = (ESC1Events * 50)
              + (ESC3Events * 40)
              + (ESC8Events * 60)
              + (array_length(NewTemplates) * 15)
              + (OffHoursRequests * 5),
    RiskLevel = case(
        ESC8Events >= 1, "Critical - ESC8_DC_Cert_Non-DC_Requester",
        ESC1Events >= 1, "Critical - ESC1_SAN_Mismatch_Issued",
        ESC3Events >= 1, "High - ESC3_EnrollAgent_Certificate",
        array_length(NewTemplates) >= 2, "High - Multiple_New_Templates",
        array_length(NewTemplates) >= 1, "Medium - New_Template_Enrollment",
        OffHoursRequests > 3, "Medium - OffHours_Cert_Requests",
        "Low"
    ),
    ExposedESC = case(
        ESC8Events >= 1, "ESC8",
        ESC1Events >= 1, "ESC1",
        ESC3Events >= 1, "ESC3",
        "None_Confirmed"
    )
| project
    RequesterNorm,
    RiskLevel,
    RiskScore,
    ExposedESC,
    TotalRequests,
    Issued,
    ESC1Events,
    ESC3Events,
    ESC8Events,
    OffHoursRequests,
    NewTemplates,
    AllTemplates,
    CAHosts,
    FirstRequest,
    LastRequest
| order by RiskScore desc

Explanation

This query is designed to analyze and audit certificate requests from a Certificate Authority (CA) audit log over the past 30 days. It focuses on identifying unusual or potentially risky certificate enrollment activities. Here's a simplified breakdown of what the query does:

  1. Define the Time Period: The query looks at data from the last 30 days.

  2. Identify Domain Controllers (DCs): It creates a list of domain controllers by checking for specific event IDs related to DC activities.

  3. Collect Certificate Request Data: It gathers data on certificate requests, issuances, and denials by looking for specific event IDs (4886 for requests, 4887 for issuances, and 4888 for denials).

  4. Analyze Certificate Requests:

    • SAN Mismatch (ESC1): Checks if the Subject Alternative Name (SAN) in the certificate doesn't match the requester, which could indicate a security issue.
    • Enrollment Agent Usage (ESC3): Identifies if an enrollment agent is used, which could be a sign of misuse.
    • DC Certificate Requests by Non-DCs (ESC8): Detects if non-DCs are requesting DC certificates, which is unusual and potentially risky.
    • Off-Hours Requests: Flags requests made outside of typical working hours (before 7 AM or after 8 PM).
  5. Establish a Baseline: It creates a baseline of normal certificate request patterns for each account based on the past 7 days.

  6. Identify Anomalies: Compares current requests against the baseline to find new templates or CA hosts that haven't been used before by the requester.

  7. Calculate Risk Scores and Levels:

    • Assigns risk scores based on the presence of ESC1, ESC3, and ESC8 events, new template usage, and off-hours requests.
    • Categorizes the risk level (e.g., Critical, High, Medium, Low) based on the risk score and specific conditions.
  8. Summarize Results: Provides a summary of the findings for each requester, including the total number of requests, issued certificates, and details about any identified risks.

  9. Order by Risk: The results are sorted by risk score, with the highest risk activities listed first.

Overall, this query helps in identifying and prioritizing potentially suspicious or unauthorized certificate activities, which could indicate attempts to abuse Active Directory Certificate Services (ADCS) or compromise security.

Details

David Alonso profile picture

David Alonso

Released: March 24, 2026

Tables

SecurityEvent

Keywords

SecurityEvent

Operators

letagowheresummarizemake_settoupperinextendtostringcolumn_ifexiststolowercasehasendswith=~has_anyhourofdayjoinkind=leftouternottoscalarprojectset_has_elementcountcountifmake_set_ifmaxminarray_lengthorder by

Actions