Query Details
// =========================================================
// 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
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:
Define the Time Period: The query looks at data from the last 30 days.
Identify Domain Controllers (DCs): It creates a list of domain controllers by checking for specific event IDs related to DC activities.
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).
Analyze Certificate Requests:
Establish a Baseline: It creates a baseline of normal certificate request patterns for each account based on the past 7 days.
Identify Anomalies: Compares current requests against the baseline to find new templates or CA hosts that haven't been used before by the requester.
Calculate Risk Scores and Levels:
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.
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.

David Alonso
Released: March 24, 2026
Tables
Keywords
Operators