Query Details

HUNT 15 AD Service Account Kerberos Ticket Abuse 30d

Query

// =========================================================
// 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

Explanation

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:

  1. 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.

  2. Service Account Identification: It identifies likely service accounts based on naming patterns (e.g., names ending with "svc", "service", etc.).

  3. Ticket Requests Analysis:

    • TGT (Ticket Granting Ticket) Requests: It examines requests for TGTs (Event ID 4768) by service accounts, noting the client machines making requests, encryption types used, and whether requests occur during off-hours.
    • TGS (Ticket Granting Service) Requests: It looks at TGS requests (Event ID 4769) where service accounts are the target, focusing on requests using RC4 encryption, which is often associated with Kerberoasting attacks.
  4. Baseline vs. Hunt Comparison:

    • Establishes a baseline of normal client machines and request patterns for each service account over the initial 21 days.
    • Compares this baseline to the last 7 days to identify new client machines, increased use of RC4 encryption, and off-hours activity.
  5. Kerberoasting Detection: Identifies service accounts that have been targeted with RC4-encrypted TGS requests during the hunt period, indicating potential Kerberoasting attempts.

  6. Risk Assessment:

    • Calculates a risk score for each service account based on factors like new client machines, RC4 usage, off-hours activity, and Kerberoasting evidence.
    • Assigns a risk level (Critical, High, Medium, Low) based on the risk score and specific conditions, such as being Kerberoasted or having multiple new client hosts.
  7. 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.

Details

David Alonso profile picture

David Alonso

Released: March 24, 2026

Tables

SecurityEvent

Keywords

SecurityEvent

Operators

letagodynamicwhere!endswith!in~extendtolowertostringcolumn_ifexistsinor<>=summarizemake_setcountifbyhas_anycountjoinkind=leftouterset_differenceroundtodoubleisnotnullarray_lengthiffcaseprojectorder bydesc

Actions