Detection rules › Kusto

Potentially Relayed NTLM Authentication - Microsoft Sentinel

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

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

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-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 NTLMRelayCandidates is inlined into the numbered stages below.

Let binding: SNAT_Subnets used in Stage 18

let SNAT_Subnets = datatable (subnet: string) [
    "1.0.0.0/26", "1.1.1.1/32"
];

Let binding: Computers used in Stages 2, 20

let Computers = NTLMRelayCandidates| summarize make_set(Computer);

Let binding: Accounts used in Stages 3, 20

let Accounts = NTLMRelayCandidates| summarize make_set(TargetUserName);

Stages 1 to 19 define let NTLMRelayCandidates (the rule's main pipeline source); stages 20 to 22 run on it.

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)

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

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
Computersummarize
IpAddresssummarize
WorkstationNamesummarize
Originextend
RelayingDeviceIPextend
Targetextend