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

Telemetry coverage

Rule body

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

let Ticket_Requests and let Suspicious_Logons are inlined into the numbered stages below.

Stage 0: let

let Ticket_Requests = materialize(<inlined as stages below>);
let Suspicious_Logons = Ticket_Requests <inlined as stages below>;

Stages 1 to 13 define let Suspicious_Logons (the rule's main pipeline source); stages 14 to 16 run on it.

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

| 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

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

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EventDatamatch<Data Name="IpAddress">::1</Data>excludes:EventData
IpAddressin-, 127.0.0.1, ::1excludes:IpAddress field:"IpAddress" value:"-" field:"IpAddress" value:"127.0.0.1" field:"IpAddress" value:"::1"
IpAddressstarts_with169.254.excludes:IpAddress field:"IpAddress" value:"169.254."

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Accountends_with
  • $
field:"Account" kind:ends_with value:"$"
AuthenticationPackageNameeq
  • Kerberos
field:"AuthenticationPackageName" kind:eq value:"Kerberos"
EventDatamatch
  • <Data Name="Status">0x0</Data> transforms: term
field:"EventData" kind:match
EventIDeq
  • 4624 corpus 29 (splunk 13, kusto 11, chronicle 4, elastic 1)
  • 4769 corpus 11 (splunk 6, kusto 4, elastic 1)
field:"EventID" kind:eq
ServiceNamematch
  • $ transforms: term
field:"ServiceName" kind:match value:"$"
TargetUserNamecontains
  • $
field:"TargetUserName" kind:contains value:"$"
TargetUserNamecross_field_compare
  • Computer transforms: op:match
  • ServiceName transforms: op:match corpus 2 (kusto 2)
field:"TargetUserName" kind:cross_field_compare
dcount_ServiceNameis_null
  • (no value, null check)
field:"dcount_ServiceName" kind:is_null

Output fields

These fields are emitted when the rule matches.

FieldSource
FirstSeensummarize
IpAddresssummarize
LastSeensummarize
TargetUserNamesummarize