Detection rules › Kusto

Potentially Relayed NTLM Authentication - Microsoft Sentinel

Group by
Computer, IpAddress, ServiceName, TargetUserName
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

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

TacticTechniques
Credential AccessNo specific technique

Event coverage

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.

FieldKindExcluded values
EventDatamatch<Data Name="IpAddress">::1</Data>
TargetUserNamematchServiceName
Accountends_with$
AuthenticationPackageNameeqKerberos
EventIDeq4624
EventDatamatch<Data Name="IpAddress">::1</Data>
TargetUserNamematchServiceName

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.

FieldKindValues
EventDatamatch
  • <Data Name="Status">0x0</Data> transforms: term
EventIDeq
  • 4769 transforms: cased corpus 10 (splunk 6, kusto 4)
IpAddressin
  • - transforms: cased
  • 127.0.0.1 transforms: cased
  • ::1 transforms: cased
IpAddressstarts_with
  • 169.254.
ServiceNamematch
  • $ transforms: term
TargetUserNamecontains
  • $
TargetUserNamematch
  • Computer transforms: term
dcount_ServiceNameis_null
  • (no value, null check)

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.

FieldSource
FirstSeensummarize
IpAddresssummarize
LastSeensummarize
TargetUserNamesummarize