Detection rules › Kusto

Suspicious Network Connections - Supply Chain Attack

Group by
DeviceName, RemoteIP, RemotePort
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

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

TacticTechniques
Command & ControlNo specific technique

Event coverage

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.

FieldKindExcluded values
RemoteIPstarts_with::ffff:
RemoteIPTypeeqFourToSixMapping
RemoteIPTypeinLoopback, 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.

FieldKindValues
ActionTypene
  • ListeningConnectionCreated transforms: cased corpus 3 (kusto 3)
DeviceNamein
  • ServersWithThirdPartyApps transforms: cased
Prevalenceis_null
  • (no value, null check)
Prevalencelt
  • 6 transforms: cased
RemoteIPin
  • Destinations transforms: cased

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.

FieldSource
DeviceNamesummarize