Query Details
// =========================================================
// HUNT-15 | AD-ServiceAccount-Kerberos-Ticket-Abuse-30d
// Description : Profiles Kerberos ticket (TGT 4768 and TGS
// 4769) requests for service accounts over
// 30 days. Identifies requests from unusual
// clients, off-hours usage, mixed encryption
// types, and service accounts with abnormally
// broad TGS request patterns — all common
// indicators of compromised service accounts.
// Period : 30 days
// Use Case : Compromised service account hunting,
// Kerberoasting aftermath, credential reuse
// Tables : SecurityEvent
// =========================================================
let Period = 30d;
let BaselineDays = 21d;
let HuntDays = 7d;
// Service account candidates: accounts with SPNs (proxied via TGS requests)
// Mark accounts ending in svc, service, sa, _s, admin_ as likely service accounts
let ServiceAccountPattern = dynamic(["svc", "service", "_sa", "sa_", "-svc", "svc-",
"_service", "service_", "srvc", "appsvc",
"sqlsvc", "iissvc", "bsvc", "websvc"]);
// TGT requests per service account
let TGTRequests = SecurityEvent
| where TimeGenerated > ago(Period)
| where EventID == 4768
| where TargetUserName !endswith "$"
| where TargetUserName !in~ ("krbtgt")
| extend
AccountNorm = tolower(TargetUserName),
ClientHost = ClientAddress,
EncType = tostring(column_ifexists("TicketEncryptionType", "")),
IsRC4 = tostring(column_ifexists("TicketEncryptionType", "")) in ("0x17", "0x18"),
IsAES = tostring(column_ifexists("TicketEncryptionType", "")) in ("0x11", "0x12"),
IsOffHours = hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) >= 20,
IsBaseline = TimeGenerated < ago(HuntDays),
IsHunt = TimeGenerated >= ago(HuntDays);
// TGS requests per service account as *target* (someone requesting ticket for their SPN)
let TGSAsTarget = SecurityEvent
| where TimeGenerated > ago(Period)
| where EventID == 4769
| where ServiceName !endswith "$"
| extend
ServiceNorm = tolower(ServiceName),
RequesterNorm = tolower(SubjectUserName),
EncType = tostring(column_ifexists("TicketEncryptionType", "")),
IsRC4 = tostring(column_ifexists("TicketEncryptionType", "")) == "0x17",
IsOffHours = hourofday(TimeGenerated) < 7 or hourofday(TimeGenerated) >= 20,
IsHunt = TimeGenerated >= ago(HuntDays);
// Service account baseline (21d)
let SvcBaseline = TGTRequests
| where IsBaseline
| extend IsLikelySvc = AccountNorm has_any (ServiceAccountPattern)
| where IsLikelySvc
| summarize
BaselineClients = make_set(ClientHost, 50),
BaselineRC4 = countif(IsRC4),
BaselineTotal = count()
by AccountNorm;
// Service account hunt window (7d)
let SvcHunt = TGTRequests
| where IsHunt
| extend IsLikelySvc = AccountNorm has_any (ServiceAccountPattern)
| where IsLikelySvc
| summarize
HuntClients = make_set(ClientHost, 50),
HuntRC4 = countif(IsRC4),
HuntTotal = count(),
OffHoursCount = countif(IsOffHours)
by AccountNorm;
// TGS requests against service accounts with RC4 in hunt window
let SvcKerberoasted = TGSAsTarget
| where IsHunt and IsRC4
| summarize
KerberoastRC4Count = count(),
KerberoastingBy = make_set(RequesterNorm, 10)
by ServiceNorm;
// Correlate
SvcHunt
| join kind=leftouter (SvcBaseline) on AccountNorm
| extend
NewClients = set_difference(HuntClients, BaselineClients),
RC4Ratio = round(todouble(HuntRC4) / todouble(HuntTotal), 2)
| join kind=leftouter (SvcKerberoasted) on $left.AccountNorm == $right.ServiceNorm
| extend
IsKerberoasted = isnotnull(KerberoastRC4Count) and KerberoastRC4Count > 0
| extend
RiskScore = (array_length(NewClients) * 20)
+ (HuntRC4 * 10)
+ (OffHoursCount * 5)
+ iff(IsKerberoasted, 40, 0),
RiskLevel = case(
IsKerberoasted and array_length(NewClients) > 0,
"Critical - Kerberoasted_And_New_Client",
IsKerberoasted,
"High - Service_Account_Kerberoasted",
array_length(NewClients) >= 2,
"High - Multiple_New_Client_Hosts",
RC4Ratio > 0.8,
"Medium - High_RC4_Ratio",
array_length(NewClients) >= 1,
"Medium - New_Client_Observed",
"Low"
)
| project
AccountNorm,
RiskLevel,
RiskScore,
IsKerberoasted,
KerberoastingBy,
HuntTotal,
HuntRC4,
RC4Ratio,
OffHoursCount,
NewClients,
HuntClients,
BaselineClients
| order by RiskScore desc
This query is designed to detect potential abuse of Kerberos tickets by service accounts over a 30-day period. It focuses on identifying unusual patterns that may indicate compromised accounts. Here's a simplified breakdown:
Time Frame: The query analyzes data from the past 30 days, with a specific focus on the last 7 days for hunting suspicious activities and the prior 21 days for establishing a baseline of normal behavior.
Service Account Identification: It identifies likely service accounts based on naming patterns (e.g., names ending with "svc", "service", etc.).
Ticket Requests Analysis:
Baseline vs. Hunt Comparison:
Kerberoasting Detection: Identifies service accounts that have been targeted with RC4-encrypted TGS requests during the hunt period, indicating potential Kerberoasting attempts.
Risk Assessment:
Output: The query outputs a list of service accounts with their associated risk levels, risk scores, and details about their ticket request patterns, helping security teams prioritize investigations into potentially compromised accounts.

David Alonso
Released: March 24, 2026
Tables
Keywords
Operators