Detection rules › Kusto
NTLM Relay Attack
This query searches for successful NTLM network logins where the device name contained in the NTLM authentication message contains a device that is known to MDE, but the source IP address is different from the known source IP address for that specific device. This could indicate an attacker is relaying the NTLM authentication information. To remove false positives, this query also searches for an outgoing network connection from the initiator to the attacker.
Known false positives
- None expected.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
References
Telemetry coverage
Rule body
let timeframe = 2*1d;
// Extract a list of known local IPs per device. Note that the DeviceNetworkEvents table is used for this, since this is faster than the
// DeviceNetworkInfo table where IP addresses are stored inside a JSON structure that requires additional parsing.
let DeviceIPs=(
DeviceNetworkEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "ConnectionAttempt" or ActionType == "ConnectionSuccess"
| distinct DeviceName, LocalIP
| extend DeviceName=tolower(split(DeviceName,".")[0])
);
// Find potential NTLM relay attack by looking for NTLM logins from devices that are known in MDE, but are from a source IP that does not match any known IP addresses for the device.
let PotentialNTLMRelayLogins=materialize (
DeviceLogonEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "LogonSuccess"
| where LogonType == "Network"
| where Protocol=="NTLM"
| where isnotempty(RemoteDeviceName) and isnotempty(RemoteIP)
| where RemoteIPType <> "Loopback"
| extend RemoteDeviceName=tolower(RemoteDeviceName)
| where RemoteDeviceName in ((DeviceIPs | project DeviceName)) // The remote device is known in MDE.
| join kind=leftanti DeviceIPs on $left.RemoteIP == $right.LocalIP, $left.RemoteDeviceName == $right.DeviceName // The Remote IP does not match any known IP for the device.
| project-reorder Timestamp, RemoteIP, RemoteDeviceName, AccountDomain, AccountName
);
// Filter the potential NTLM relay events by checking there was an outgoing SMB connection from the source device to the relay IP address.
DeviceNetworkEvents
| where ingestion_time() >= ago(timeframe)
| where RemotePort in (445, 80, 9389)
| where RemoteIP in ((PotentialNTLMRelayLogins | project RemoteIP))
| extend ShortDeviceName=tolower(split(DeviceName,".")[0])
| where ShortDeviceName in ((PotentialNTLMRelayLogins | project RemoteDeviceName))
| lookup kind=inner PotentialNTLMRelayLogins on $left.ShortDeviceName == $right.RemoteDeviceName, $left.RemoteIP == $right.RemoteIP
| extend HostName=tostring(split(DeviceName,".")[0]),DnsDomain=iif(DeviceName contains ".", substring(DeviceName, indexof(DeviceName, ".") + 1, strlen(DeviceName)),"")
// Begin environment-specific filter.
// End environment-specific filter.
Stages and Predicates
Parameters
let timeframe = 2*1d;
Let binding: DeviceIPs
let DeviceIPs = (
DeviceNetworkEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "ConnectionAttempt" or ActionType == "ConnectionSuccess"
| distinct DeviceName, LocalIP
| extend DeviceName=tolower(split(DeviceName,".")[0])
);
Let binding: PotentialNTLMRelayLogins
let PotentialNTLMRelayLogins = materialize (
DeviceLogonEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "LogonSuccess"
| where LogonType == "Network"
| where Protocol=="NTLM"
| where isnotempty(RemoteDeviceName) and isnotempty(RemoteIP)
| where RemoteIPType <> "Loopback"
| extend RemoteDeviceName=tolower(RemoteDeviceName)
| where RemoteDeviceName in ((DeviceIPs | project DeviceName))
| join kind=leftanti DeviceIPs on $left.RemoteIP == $right.LocalIP, $left.RemoteDeviceName == $right.DeviceName
| project-reorder Timestamp, RemoteIP, RemoteDeviceName, AccountDomain, AccountName
);
Stage 1: source
let DeviceIPs
Stage 2: source
let PotentialNTLMRelayLogins
Stage 3: source
DeviceNetworkEvents
Stage 4: where
where ...
Stage 5: where
where RemotePort in~ (445, 80, 9389)
Stage 6: where
where /* macro: (RemoteIP in <NestedQuery>) */
Stage 7: extend
extend ShortDeviceName
Stage 8: where
where /* macro: (ShortDeviceName in <NestedQuery>) */
Stage 9: kusto:lookup
lookup kind=inner (PotentialNTLMRelayLogins) on ShortDeviceName, RemoteDeviceName, RemoteIP, RemoteIP
Stage 10: extend
extend DnsDomain, HostName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq |
LogonType | eq |
| field:"LogonType" kind:eq value:"Network" |
Protocol | eq |
| field:"Protocol" kind:eq value:"NTLM" |
RemoteDeviceName | is_not_null | field:"RemoteDeviceName" kind:is_not_null | |
RemoteIP | is_not_null | field:"DestinationIp" kind:is_not_null | |
RemoteIPType | ne |
| field:"RemoteIPType" kind:ne value:"Loopback" |
RemotePort | in |
| field:"DestinationPort" kind:in |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ShortDeviceName | extend |
DnsDomain | extend |
HostName | extend |