Detection rules › Kusto
Potentially Relayed NTLM Authentication - Microsoft Sentinel
The below query detects Kerberos logons of computer accounts where there isn't any ticket request in the last 12h (10h is the default ticket expiration) coming from the same IpAddress with the same TargetUserName. The query can be enriched further if needed.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | No specific technique |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 4624 | An account was successfully logged on. |
| Security-Auditing | Event ID 4769 | A Kerberos service ticket was requested. |
Rule body kusto
// Author : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/detecting-kerberos-relaying-e6be66fa647c
//
// Description: This query detects Kerberos logons of computer accounts where there isn't any ticket request in the last 12h (10h is the default ticket expiration) coming from the same IpAddress with the same TargetUserName. The query can be enriched further if needed.
//
// Query parameters:
//
let Ticket_Requests = materialize (
SecurityEvent
| where TimeGenerated > ago(12h)
| where EventID == 4769
| where EventData has '<Data Name="Status">0x0</Data>'
| where EventData !has'<Data Name="IpAddress">::1</Data>'
| parse EventData with * 'TargetUserName">' TargetUserName '</Data' * 'TargetDomainName">' TargetDomainName '</Data' * 'ServiceName">' ServiceName '<' * 'IpAddress">::ffff:' IpAddress '<' * 'Status">' Status '<' *
| where TargetUserName !has ServiceName
| where TargetUserName contains "$"
| where ServiceName has "$"
| project TimeGenerated, TargetUserName=tolower(TargetUserName), TargetDomainName, ServiceName=tolower(replace_string(ServiceName, '$', '')), IpAddress, Status
)
;
let Suspicious_Logons =
Ticket_Requests
| join kind=rightanti (
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4624
| where AuthenticationPackageName == "Kerberos"
| where IpAddress !in ('-', '::1', '127.0.0.1')
| where IpAddress !startswith "169.254."
| where Account endswith_cs "$"
| project TimeGenerated, Computer = tolower(replace_regex(Computer, @'(\w+)\..*', @'\1')), Account, TargetUserName=tolower(TargetUserName), IpAddress
| where TargetUserName !has Computer
) on IpAddress, $left.ServiceName==$right.Computer
;
Suspicious_Logons
| join kind=leftouter (
Ticket_Requests
| extend TargetUserName = replace_regex(TargetUserName, @'(\w+\$)@.*', @'\1')
) on IpAddress, TargetUserName
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), count(), dcount(ServiceName) by TargetUserName, IpAddress
// Filter results
// we don't expect a successful ticket request coming from the rogue(attacker) device befor the relaying attack.
// If there is at least one ticket request coming from the suspicious IP with the same TargetUserName, assume it's a legitimate activity.
| where isempty(dcount_ServiceName)
Stages and Predicates
Stage 0: let
let Ticket_Requests = materialize(<inlined as stages below>);
let Suspicious_Logons = Ticket_Requests <inlined as stages below>;
The stages below define let Suspicious_Logons (the rule's main pipeline source).
Stage 1: source
let Ticket_Requests
Stage 2: source
let Suspicious_Logons
Stage 3: source
SecurityEvent
Stage 4: where
| where TimeGenerated > ago(12h)
Stage 5: where
| where EventID == 4769
Stage 6: where
| where EventData has '<Data Name="Status">0x0</Data>'
Stage 7: where
| where EventData !has'<Data Name="IpAddress">::1</Data>'
Stage 8: parse
| parse EventData with * 'TargetUserName">' TargetUserName '</Data' * 'TargetDomainName">' TargetDomainName '</Data' * 'ServiceName">' ServiceName '<' * 'IpAddress">::ffff:' IpAddress '<' * 'Status">' Status '<' *
Stage 9: where
| where TargetUserName !has ServiceName
Stage 10: where
| where TargetUserName contains "$"
Stage 11: where
| where ServiceName has "$"
Stage 12: project
| project TimeGenerated, TargetUserName=tolower(TargetUserName), TargetDomainName, ServiceName=tolower(replace_string(ServiceName, '$', '')), IpAddress, Status
Stage 13: join (negated)
| join kind=rightanti (
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4624
| where AuthenticationPackageName == "Kerberos"
| where IpAddress !in ('-', '::1', '127.0.0.1')
| where IpAddress !startswith "169.254."
| where Account endswith_cs "$"
| project TimeGenerated, Computer = tolower(replace_regex(Computer, @'(\w+)\..*', @'\1')), Account, TargetUserName=tolower(TargetUserName), IpAddress
| where TargetUserName !has Computer
) on IpAddress, $left.ServiceName==$right.Computer
The stages below run on Suspicious_Logons (the outer pipeline).
Stage 14: join
Suspicious_Logons
| join kind=leftouter (
Ticket_Requests
| extend TargetUserName = replace_regex(TargetUserName, @'(\w+\$)@.*', @'\1')
) on IpAddress, TargetUserName
Stage 15: summarize
| summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated), count(), dcount(ServiceName) by TargetUserName, IpAddress
Stage 16: where
| where isempty(dcount_ServiceName)
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
EventData | match | <Data Name="IpAddress">::1</Data> |
TargetUserName | match | ServiceName |
Account | ends_with | $ |
AuthenticationPackageName | eq | Kerberos |
EventID | eq | 4624 |
EventData | match | <Data Name="IpAddress">::1</Data> |
TargetUserName | match | ServiceName |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
EventData | match |
|
EventID | eq |
|
IpAddress | in |
|
IpAddress | starts_with |
|
ServiceName | match |
|
TargetUserName | contains |
|
TargetUserName | match |
|
dcount_ServiceName | is_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
FirstSeen | summarize |
IpAddress | summarize |
LastSeen | summarize |
TargetUserName | summarize |