Detection rules › Kusto
Potentially Relayed NTLM Authentication - Microsoft Sentinel
The below query detects NTLM logons where Network Address in the logon event doesn't match the Workstation Name's IP. This indicates potentially relayed NTLM authentication. It analyzes only the logons with domain accounts having admin privileges.
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 4672 | Special privileges assigned to new logon. |
Rule body kusto
// Author : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/detecting-ntlm-relay-attacks-d92e99e68fb9
//
// Description: This query detects NTLM logons where Network Address in the NTLM logon event doesn't match the Workstation Name's IP.
// This indicates potentially relayed NTLM authentication. The query analyzes only the logons with domain accounts having admin privileges.
//
// Query parameters:
//
let ingestion_delay = 2h;
let rule_frequency = 2h;
let lookback = 1d;
// Specify domains in NETBIOS name and full domain format
let domains = dynamic(["PUT YOUR AD DOMAINS HERE!", "contoso","contoso.local"]);
// Exclude authentications coming from device performing SNAT.
// Exclude devices that always perform NTLM authentication
let SNAT_Subnets = datatable (subnet: string) [
"1.0.0.0/26", "1.1.1.1/32"
];
// Get NTLM relay candidates
let NTLMRelayCandidates = materialize (
SecurityEvent
| where TimeGenerated > ago(rule_frequency + ingestion_delay)
| where EventID == 4624
| where AccountType == "User"
| where AuthenticationPackageName == "NTLM"
| where LogonType == 3
| where TargetDomainName in~ (domains)
| where isnotempty(IpAddress) and IpAddress !in ('-', '::1', '127.0.0.1')
| where isnotempty(WorkstationName) and WorkstationName <> '-'
| where IpPort <> 0 and Computer !has WorkstationName
| where ElevatedToken <> '%%1843'// exclude non-admin logon sessions
| extend delay = ingestion_time() - TimeGenerated
| summarize hint.strategy=shuffle arg_max(TimeGenerated, *) by Computer, Account, IpAddress, WorkstationName
// Machine logon events have the IP address of the machine, exclude results where the IPAddress in the NTLM logon matches the IPAddress in Machine logon event
| join hint.strategy=shuffle kind=leftanti
(
SecurityEvent
| where TimeGenerated > ago(lookback)
| where EventID == 4624
| where AccountType == "Machine"
| where isnotempty(IpAddress) and IpAddress !in ('-', '::1', '127.0.0.1')
| distinct TargetUserName, IpAddress
| extend TargetUserName = toupper(replace(@'([A-z0-9-]+)\.?.*', @'\1', TargetUserName))
)
on $left.WorkstationName == $right.TargetUserName, IpAddress // filter condition
// Filter out excluded IP subnets.
| evaluate ipv4_lookup(SNAT_Subnets, IpAddress, subnet, return_unmatched = true)
| where isempty(subnet) // remove results that matched a SNAT subnet.
)
;
// Windows 2012 doesn't have elevated token info in NTLM logon events.
// Filter relayed authentications where the session has admin privileges
let Computers=
NTLMRelayCandidates| summarize make_set(Computer);
//
let Accounts =
NTLMRelayCandidates| summarize make_set(TargetUserName);
// There must be a 4672 event for an admin logon with the same logon id
NTLMRelayCandidates
| join hint.strategy=shuffle kind=inner
(
SecurityEvent
| where TimeGenerated > ago(rule_frequency + ingestion_delay)
| where Computer in (Computers)
| where SubjectUserName in (Accounts)
| where EventID == 4672
| where AccountType == "User"
| project Computer, Account, SubjectLogonId, PrivilegeList
)
on $left.TargetLogonId==$right.SubjectLogonId, Account, Computer
| extend Origin = WorkstationName, RelayingDeviceIP = IpAddress, Target = Computer
| project-reorder TimeGenerated, Computer, Origin, RelayingDeviceIP, Target, Account, PrivilegeList1
// more filtering can be done based on the privilege list, specific computers or accounts.
Stages and Predicates
Parameters
let ingestion_delay = 2h;
let rule_frequency = 2h;
let lookback = 1d;
let domains = dynamic(["PUT YOUR AD DOMAINS HERE!", "contoso","contoso.local"]);
Let binding: SNAT_Subnets
let SNAT_Subnets = datatable (subnet: string) [
"1.0.0.0/26", "1.1.1.1/32"
];
Let binding: Computers
let Computers = NTLMRelayCandidates| summarize make_set(Computer);
Derived from NTLMRelayCandidates.
Let binding: Accounts
let Accounts = NTLMRelayCandidates| summarize make_set(TargetUserName);
Derived from NTLMRelayCandidates.
The stages below define let NTLMRelayCandidates (the rule's main pipeline source).
Stage 1: source
let NTLMRelayCandidates
Stage 2: source
let Computers
Stage 3: source
let Accounts
Stage 4: source
SecurityEvent
Stage 5: where
| where TimeGenerated > ago(rule_frequency + ingestion_delay)
Stage 6: where
| where EventID == 4624
Stage 7: where
| where AccountType == "User"
Stage 8: where
| where AuthenticationPackageName == "NTLM"
Stage 9: where
| where LogonType == 3
Stage 10: where
| where TargetDomainName in~ (domains)
Stage 11: where
| where isnotempty(IpAddress) and IpAddress !in ('-', '::1', '127.0.0.1')
Stage 12: where
| where isnotempty(WorkstationName) and WorkstationName <> '-'
Stage 13: where
| where IpPort <> 0 and Computer !has WorkstationName
Stage 14: where
| where ElevatedToken <> '%%1843'
Stage 15: extend
| extend delay = ingestion_time() - TimeGenerated
Stage 16: summarize
| summarize hint.strategy=shuffle arg_max(TimeGenerated, *) by Computer, Account, IpAddress, WorkstationName
Stage 17: join (negated)
| join hint.strategy=shuffle kind=leftanti
(
SecurityEvent
| where TimeGenerated > ago(lookback)
| where EventID == 4624
| where AccountType == "Machine"
| where isnotempty(IpAddress) and IpAddress !in ('-', '::1', '127.0.0.1')
| distinct TargetUserName, IpAddress
| extend TargetUserName = toupper(replace(@'([A-z0-9-]+)\.?.*', @'\1', TargetUserName))
)
on $left.WorkstationName == $right.TargetUserName, IpAddress
Stage 18: evaluate
| evaluate ipv4_lookup(SNAT_Subnets, IpAddress, subnet, return_unmatched = true)
Stage 19: where
| where isempty(subnet)
The stages below run on NTLMRelayCandidates (the outer pipeline).
Stage 20: join
NTLMRelayCandidates
| join hint.strategy=shuffle kind=inner
(
SecurityEvent
| where TimeGenerated > ago(rule_frequency + ingestion_delay)
| where Computer in (Computers)
| where SubjectUserName in (Accounts)
| where EventID == 4672
| where AccountType == "User"
| project Computer, Account, SubjectLogonId, PrivilegeList
)
on $left.TargetLogonId==$right.SubjectLogonId, Account, Computer
Stage 21: extend
| extend Origin = WorkstationName, RelayingDeviceIP = IpAddress, Target = Computer
Stage 22: project-reorder
| project-reorder TimeGenerated, Computer, Origin, RelayingDeviceIP, Target, Account, PrivilegeList1
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
IpAddress | in | -, 127.0.0.1, ::1 |
Computer | match | WorkstationName |
AccountType | eq | Machine |
EventID | eq | 4624 |
IpAddress | is_not_null |
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 |
|---|---|---|
AccountType | eq |
|
AuthenticationPackageName | eq |
|
Computer | in |
|
ElevatedToken | ne |
|
EventID | eq |
|
IpAddress | in |
|
IpAddress | is_not_null | |
IpPort | ne |
|
LogonType | eq |
|
SubjectUserName | in |
|
TargetDomainName | in |
|
WorkstationName | is_not_null | |
WorkstationName | ne |
|
subnet | 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 |
|---|---|
Account | summarize |
Computer | summarize |
IpAddress | summarize |
WorkstationName | summarize |
Origin | extend |
RelayingDeviceIP | extend |
Target | extend |