Detection rules › Kusto
Suspicious Network Connections - Supply Chain Attack
Below query detects unusual network conenctions from servers that have 3rd party software installed.
You can further improve the query by using a list of servers that have privileges across the whole domain.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Command & Control | No specific technique |
Event coverage
| Provider | Event/ActionType | Title |
|---|---|---|
| Sysmon | Event ID 3 | Network connection |
| Security-Auditing | Event ID 5156 | The Windows Filtering Platform has permitted a connection. |
| Defender-DeviceNetworkEvents | any | Network activity (any) |
Rule body kusto
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Query parameters:
let lookback = 14d;
// Generate list of all Servers
let server_list =
DeviceInfo
| where Timestamp > ago(lookback)
| where isnotempty(OSPlatform)
| where DeviceType <> "Workstation" and OSPlatform <> "macOS"
| summarize make_set(DeviceName)
;
// Generate list of servers that have 3rd party software installed.
// Criteria: if a software is installed on less than 10 servers, it's probably a 3rd party software.
// There are probably(hopefully) just a few servers or server groups that have privileges across the whole domain.
// You can change the threshold according to your environment.
let ServersWithThirdPartyApps = materialize (
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where DeviceName in (server_list)
| where ActionType <> "ListeningConnectionCreated"
| where RemoteIPType !in ("Private","Loopback") and (not (RemoteIPType == "FourToSixMapping" and RemoteIP startswith "::ffff:"))
| summarize dcount(DeviceName) by InitiatingProcessVersionInfoCompanyName
| where dcount_DeviceName < 10
| join kind=inner (
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where DeviceName in (server_list)
| where ActionType <> "ListeningConnectionCreated"
| where RemoteIPType !in ("Private","Loopback") and (not (RemoteIPType == "FourToSixMapping" and RemoteIP startswith "::ffff:"))
)
on InitiatingProcessVersionInfoCompanyName
| summarize make_set(DeviceName)
)
;
// Get network connection statistics
let baseline = materialize (
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where DeviceName in (ServersWithThirdPartyApps)
| where ActionType <> "ListeningConnectionCreated"
| where RemoteIPType !in ("Private","Loopback") and (not (RemoteIPType == "FourToSixMapping" and RemoteIP startswith "::ffff:"))
| summarize hint.strategy=shuffle Count=count(), starttime = min(Timestamp), endtime = max(Timestamp) by DeviceName, RemoteIP, RemotePort
)
;
// Get destination IP
let Destinations = baseline | summarize make_set(RemoteIP);
// Filter connections that was not seen before last 1d
// Generate prevalence info, URL info(if available) and enrich results
// Filter based on prevalence and URL information and display everything by hostname
baseline
| where starttime > ago(1d)
| lookup kind=leftouter (
DeviceNetworkEvents
| where Timestamp > ago(5d)
| where RemoteIP in (Destinations)
| summarize hint.strategy=shuffle Prevalence = dcount(DeviceId), URLs=make_set(RemoteUrl) by RemoteIP
)
on RemoteIP
| where Prevalence < 6 or isempty( Prevalence)
// If you want to see all the events in distinct rows, remove the below 2 lines
// filter out results based on trusted URLs if you like.
| extend Details = pack('RemoteIP',RemoteIP, 'RemotePort',RemotePort, 'Count',Count, 'Prevalence',Prevalence, 'URLs',URLs)
| summarize make_set(Details) by DeviceName
Stages and Predicates
Parameters
let lookback = 14d;
Let binding: server_list
let server_list = DeviceInfo
| where Timestamp > ago(lookback)
| where isnotempty(OSPlatform)
| where DeviceType <> "Workstation" and OSPlatform <> "macOS"
| summarize make_set(DeviceName);
Derived from lookback.
Let binding: ServersWithThirdPartyApps
let ServersWithThirdPartyApps = materialize (
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where DeviceName in (server_list)
| where ActionType <> "ListeningConnectionCreated"
| where RemoteIPType !in ("Private","Loopback") and (not (RemoteIPType == "FourToSixMapping" and RemoteIP startswith "::ffff:"))
| summarize dcount(DeviceName) by InitiatingProcessVersionInfoCompanyName
| where dcount_DeviceName < 10
| join kind=inner (
DeviceNetworkEvents
| where Timestamp > ago(lookback)
| where DeviceName in (server_list)
| where ActionType <> "ListeningConnectionCreated"
| where RemoteIPType !in ("Private","Loopback") and (not (RemoteIPType == "FourToSixMapping" and RemoteIP startswith "::ffff:"))
)
on InitiatingProcessVersionInfoCompanyName
| summarize make_set(DeviceName)
);
Derived from lookback, server_list.
Let binding: Destinations
let Destinations = baseline | summarize make_set(RemoteIP);
Derived from baseline.
The stages below define let baseline (the rule's main pipeline source).
Stage 1: source
DeviceNetworkEvents
Stage 2: where
| where Timestamp > ago(lookback)
Stage 3: where
| where DeviceName in (ServersWithThirdPartyApps)
References ServersWithThirdPartyApps (defined above).
Stage 4: where
| where ActionType <> "ListeningConnectionCreated"
Stage 5: where
| where RemoteIPType !in ("Private","Loopback") and (not (RemoteIPType == "FourToSixMapping" and RemoteIP startswith "::ffff:"))
Stage 6: summarize
| summarize hint.strategy=shuffle Count=count(), starttime = min(Timestamp), endtime = max(Timestamp) by DeviceName, RemoteIP, RemotePort
The stages below run on baseline (the outer pipeline).
Stage 7: where
baseline
| where starttime > ago(1d)
Stage 8: kusto:lookup
| lookup kind=leftouter (
DeviceNetworkEvents
| where Timestamp > ago(5d)
| where RemoteIP in (Destinations)
| summarize hint.strategy=shuffle Prevalence = dcount(DeviceId), URLs=make_set(RemoteUrl) by RemoteIP
)
on RemoteIP
Stage 9: where
| where Prevalence < 6 or isempty( Prevalence)
Stage 10: extend
| extend Details = pack('RemoteIP',RemoteIP, 'RemotePort',RemotePort, 'Count',Count, 'Prevalence',Prevalence, 'URLs',URLs)
Stage 11: summarize
| summarize make_set(Details) by DeviceName
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
RemoteIP | starts_with | ::ffff: |
RemoteIPType | eq | FourToSixMapping |
RemoteIPType | in | Loopback, Private |
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 |
|---|---|---|
ActionType | ne |
|
DeviceName | in |
|
Prevalence | is_null | |
Prevalence | lt |
|
RemoteIP | in |
|
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 |
|---|---|
DeviceName | summarize |