Detection rules › Kusto

Scheduled Task - Suspicious Network Connection

Group by
DeviceId, FileName, InitiatingProcessFileName, InitiatingProcessG1ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG3ParentFileName, InitiatingProcessParentFileName, ProcessCommandLine
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query performs process tree analysis for Scheduled Tasks on MDE/MDATP/M365D and displays anomalous trees. Then, it gets all network connections made by every single process in each anomalous process tree. Before using the query, do a quick analysis on commandlines of the processes spawned by Scheduled Tasks. There might be specific processes executing with a unique argument on each device. You need to whitelist them to get better results.

MITRE ATT&CK coverage

TacticTechniques
PersistenceNo specific technique

Event coverage

Rule body kusto

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://mergene.medium.com/hunting-for-the-behavior-scheduled-tasks-9efe0b8ade40
// Hypothesis: The goal of the persistence is keeping the C2 channel active.
// This query performs process tree analysis for Scheduled Tasks on MDE/MDATP/M365D and displays anomalous trees.
// Then, it gets all network connections made by every single process in each anomalous process tree.
// Before using the query, do a quick analysis on commandlines of the processes spawned by Scheduled Tasks. 
// There might be specific processes executing with a unique argument on each device. You need to whitelist them to get better results.
let timeframe=7d;
let whitelisted_cmdlines = dynamic(["put_whiltested_commandlines_here"]);
let whitelist_folderpath = dynamic(["put_whitelisted_folderpaths-here"]);
let _process_tree_data = materialize ( 
    DeviceProcessEvents
    | where Timestamp > ago(timeframe)
    | where InitiatingProcessFileName == "svchost.exe" and InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
    | where not( ProcessCommandLine  has_any  (whitelisted_cmdlines ))
    | where not (FolderPath has_any (whitelist_folderpath))
    | summarize dcount(DeviceId), count() by ProcessCommandLine, FileName
    | where dcount_DeviceId <= 5
    | join kind=inner (
        DeviceProcessEvents
        | where Timestamp > ago(timeframe)
        | where InitiatingProcessFileName == "svchost.exe" and InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
        | where not( ProcessCommandLine  has_any  (whitelisted_cmdlines ))
        | where not (FolderPath has_any (whitelist_folderpath))
        ) on ProcessCommandLine
        | project DeviceId,DeviceName, Timestamp,
              InitiatingProcessG3ParentFileName=FileName,InitiatingProcessG3ParentSHA1=SHA1,InitiatingProcessG3ParentId=ProcessId, InitiatingProcessG3ParentCommandLine=ProcessCommandLine,InitiatingProcessG3ParentCreationTime=todatetime(ProcessCreationTime),
              InitiatingProcessG4ParentFileName=InitiatingProcessFileName,InitiatingProcessG4ParentSHA1=InitiatingProcessSHA1,InitiatingProcessG4ParentId=InitiatingProcessId,InitiatingProcessG4ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG4ParentCreationTime=todatetime(InitiatingProcessCreationTime)
        // Start iteration
        // 1st iteration of join. From now on, query all processes, rename fields, and join accordingly
        | join kind=leftouter (
            DeviceProcessEvents
            | where Timestamp > ago(timeframe)
            | project DeviceId, InitiatingProcessG2ParentFileName=FileName,InitiatingProcessG2ParentFolderPath=FolderPath,InitiatingProcessG2ParentSHA1=SHA1, InitiatingProcessG2ParentId=ProcessId,  InitiatingProcessG2ParentCommandLine=ProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(ProcessCreationTime), 
                      InitiatingProcessG3ParentFileName=InitiatingProcessFileName,InitiatingProcessG3ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG3ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG3ParentId=InitiatingProcessId,  InitiatingProcessG3ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG3ParentCreationTime=todatetime(InitiatingProcessCreationTime)
            ) 
            on DeviceId , InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentCreationTime
            // 2nd iteration of join.
            | join kind=leftouter (
                DeviceProcessEvents
                | where Timestamp > ago(timeframe)
                | project DeviceId, InitiatingProcessG1ParentFileName=FileName,InitiatingProcessG1ParentFolderPath=FolderPath,InitiatingProcessG1ParentSHA1=SHA1, InitiatingProcessG1ParentId=ProcessId,  InitiatingProcessG1ParentCommandLine=ProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(ProcessCreationTime), 
                          InitiatingProcessG2ParentFileName=InitiatingProcessFileName,InitiatingProcessG2ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG2ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG2ParentId=InitiatingProcessId,  InitiatingProcessG2ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(InitiatingProcessCreationTime)
                ) 
                on DeviceId , InitiatingProcessG2ParentFileName , InitiatingProcessG2ParentId, InitiatingProcessG2ParentCreationTime
                // 3rd iteration of join.
                | join kind=leftouter (
                    DeviceProcessEvents
                    | where Timestamp > ago(timeframe)
                    | project DeviceId, InitiatingProcessParentFileName=FileName,InitiatingProcessParentFolderPath=FolderPath,InitiatingProcessParentSHA1=SHA1, InitiatingProcessParentId=ProcessId,  InitiatingProcessParentCommandLine=ProcessCommandLine, InitiatingProcessParentCreationTime=ProcessCreationTime, 
                              InitiatingProcessG1ParentFileName=InitiatingProcessFileName,InitiatingProcessG1ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG1ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG1ParentId=InitiatingProcessId,  InitiatingProcessG1ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(InitiatingProcessCreationTime)
                    ) 
                    on DeviceId , InitiatingProcessG1ParentFileName , InitiatingProcessG1ParentId, InitiatingProcessG1ParentCreationTime
                    // 4th iteration of join
                    | join kind=leftouter (
                        DeviceProcessEvents
                        | where Timestamp > ago(timeframe)
                        | project DeviceId, InitiatingProcessFileName=FileName,InitiatingProcessSHA1=SHA1, InitiatingProcessId=ProcessId,  InitiatingProcessCommandLine=ProcessCommandLine, InitiatingProcessCreationTime=ProcessCreationTime, 
                                  InitiatingProcessParentFileName=InitiatingProcessFileName,InitiatingProcessParentSHA1=InitiatingProcessSHA1, InitiatingProcessParentId=InitiatingProcessId,  InitiatingProcessParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessParentCreationTime=InitiatingProcessCreationTime
                        ) 
                        on DeviceId , InitiatingProcessParentFileName , InitiatingProcessParentId, InitiatingProcessParentCreationTime
                        // 5th iteration of join
                        | join kind=leftouter (
                            DeviceProcessEvents
                            | where Timestamp > ago(timeframe)
                            | project Timestamp, DeviceId, FileName,SHA1, ProcessId, ProcessCommandLine, ProcessCreationTime, 
                                      InitiatingProcessFileName,InitiatingProcessSHA1, InitiatingProcessId, InitiatingProcessCommandLine, InitiatingProcessCreationTime
                            ) 
                            on DeviceId , InitiatingProcessFileName , InitiatingProcessId, InitiatingProcessCreationTime
);
// Use the cached results and find the rare patterns based on process names.
// New fields "ProcessVersionInfoOriginalFileName" and "InitiatingProcessVersionInfoInternalFileName" can be used as well. 
_process_tree_data
| where Timestamp > ago(1d) // get only the trees from last 1d. 
// get the last occurence of the rare patterns. 
| summarize arg_max(Timestamp,*), pattern_count=count() by DeviceId, InitiatingProcessG3ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG1ParentFileName, InitiatingProcessParentFileName, InitiatingProcessFileName, FileName
// We need to put all process nodes to the same column so that we can apply join for each process node. 
| extend N_InitiatingProcessFileName = pack_array(InitiatingProcessG3ParentFileName,InitiatingProcessG2ParentFileName,InitiatingProcessG1ParentFileName,InitiatingProcessParentFileName,InitiatingProcessFileName,FileName),
         N_InitiatingProcessCommandLine = pack_array(InitiatingProcessG3ParentCommandLine, InitiatingProcessG2ParentCommandLine, InitiatingProcessG1ParentCommandLine, InitiatingProcessParentCommandLine, InitiatingProcessCommandLine, ProcessCommandLine),
         N_InitiatingProcessId = pack_array(InitiatingProcessG3ParentId,InitiatingProcessG2ParentId,InitiatingProcessG1ParentId,InitiatingProcessParentId,InitiatingProcessId,ProcessId),
         N_InitiatingProcessCreationTime = pack_array(InitiatingProcessG3ParentCreationTime,InitiatingProcessG2ParentCreationTime,InitiatingProcessG1ParentCreationTime,InitiatingProcessParentCreationTime,InitiatingProcessCreationTime,ProcessCreationTime)
// apply mv-expand so that all process nodes are put into the same columns.
| mv-expand N_InitiatingProcessFileName, N_InitiatingProcessCommandLine, N_InitiatingProcessId, N_InitiatingProcessCreationTime
// generate a key for the join with DeviceNetworkEvents, remove rows if the process info is null(mv-expand results in some null values)
| extend join_key = strcat(DeviceId,'-',N_InitiatingProcessFileName,'-',N_InitiatingProcessId,'-',tostring(N_InitiatingProcessCreationTime))
| where isnotnull(N_InitiatingProcessId)
// generate join key for the DeviceNetworkEvents and apply join and exclude internal trafic(you may want to check internal traffic for lateral movement)
| join kind=inner (DeviceNetworkEvents | where Timestamp > ago(timeframe)| extend join_key = strcat(DeviceId,'-', InitiatingProcessFileName,'-', InitiatingProcessId,'-', tostring(InitiatingProcessCreationTime)) ) on join_key
| where RemoteIP !in ("::1","127.0.0.1","::ffff:127.0.0.1") and RemoteIPType <> "Private"
| where not(RemoteUrl has_any("corel.com","ocsp.digicert.com","avast.com"))
| where ActionType != "ListeningConnectionCreated"
| project-reorder pattern_count, RemoteUrl, RemoteIP, RemotePort

Stages and Predicates

Parameters

let timeframe = 7d;
let whitelisted_cmdlines = dynamic(["put_whiltested_commandlines_here"]);
let whitelist_folderpath = dynamic(["put_whitelisted_folderpaths-here"]);

The stages below define let _process_tree_data (the rule's main pipeline source).

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where Timestamp > ago(timeframe)

Stage 3: where

| where InitiatingProcessFileName == "svchost.exe" and InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"

Stage 4: where

| where not( ProcessCommandLine  has_any  (whitelisted_cmdlines ))

Stage 5: where

| where not (FolderPath has_any (whitelist_folderpath))

Stage 6: summarize

| summarize dcount(DeviceId), count() by ProcessCommandLine, FileName

Stage 7: where

| where dcount_DeviceId <= 5

Stage 8: join

| join kind=inner (
        DeviceProcessEvents
        | where Timestamp > ago(timeframe)
        | where InitiatingProcessFileName == "svchost.exe" and InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
        | where not( ProcessCommandLine  has_any  (whitelisted_cmdlines ))
        | where not (FolderPath has_any (whitelist_folderpath))
        ) on ProcessCommandLine

Stage 9: project

| project DeviceId,DeviceName, Timestamp,
              InitiatingProcessG3ParentFileName=FileName,InitiatingProcessG3ParentSHA1=SHA1,InitiatingProcessG3ParentId=ProcessId, InitiatingProcessG3ParentCommandLine=ProcessCommandLine,InitiatingProcessG3ParentCreationTime=todatetime(ProcessCreationTime),
              InitiatingProcessG4ParentFileName=InitiatingProcessFileName,InitiatingProcessG4ParentSHA1=InitiatingProcessSHA1,InitiatingProcessG4ParentId=InitiatingProcessId,InitiatingProcessG4ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG4ParentCreationTime=todatetime(InitiatingProcessCreationTime)

Stage 10: join

| join kind=leftouter (
            DeviceProcessEvents
            | where Timestamp > ago(timeframe)
            | project DeviceId, InitiatingProcessG2ParentFileName=FileName,InitiatingProcessG2ParentFolderPath=FolderPath,InitiatingProcessG2ParentSHA1=SHA1, InitiatingProcessG2ParentId=ProcessId,  InitiatingProcessG2ParentCommandLine=ProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(ProcessCreationTime), 
                      InitiatingProcessG3ParentFileName=InitiatingProcessFileName,InitiatingProcessG3ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG3ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG3ParentId=InitiatingProcessId,  InitiatingProcessG3ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG3ParentCreationTime=todatetime(InitiatingProcessCreationTime)
            ) 
            on DeviceId , InitiatingProcessG3ParentFileName, InitiatingProcessG3ParentId, InitiatingProcessG3ParentCreationTime

Stage 11: join

| join kind=leftouter (
                DeviceProcessEvents
                | where Timestamp > ago(timeframe)
                | project DeviceId, InitiatingProcessG1ParentFileName=FileName,InitiatingProcessG1ParentFolderPath=FolderPath,InitiatingProcessG1ParentSHA1=SHA1, InitiatingProcessG1ParentId=ProcessId,  InitiatingProcessG1ParentCommandLine=ProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(ProcessCreationTime), 
                          InitiatingProcessG2ParentFileName=InitiatingProcessFileName,InitiatingProcessG2ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG2ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG2ParentId=InitiatingProcessId,  InitiatingProcessG2ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG2ParentCreationTime=todatetime(InitiatingProcessCreationTime)
                ) 
                on DeviceId , InitiatingProcessG2ParentFileName , InitiatingProcessG2ParentId, InitiatingProcessG2ParentCreationTime

Stage 12: join

| join kind=leftouter (
                    DeviceProcessEvents
                    | where Timestamp > ago(timeframe)
                    | project DeviceId, InitiatingProcessParentFileName=FileName,InitiatingProcessParentFolderPath=FolderPath,InitiatingProcessParentSHA1=SHA1, InitiatingProcessParentId=ProcessId,  InitiatingProcessParentCommandLine=ProcessCommandLine, InitiatingProcessParentCreationTime=ProcessCreationTime, 
                              InitiatingProcessG1ParentFileName=InitiatingProcessFileName,InitiatingProcessG1ParentFolderPath=InitiatingProcessFolderPath,InitiatingProcessG1ParentSHA1=InitiatingProcessSHA1, InitiatingProcessG1ParentId=InitiatingProcessId,  InitiatingProcessG1ParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessG1ParentCreationTime=todatetime(InitiatingProcessCreationTime)
                    ) 
                    on DeviceId , InitiatingProcessG1ParentFileName , InitiatingProcessG1ParentId, InitiatingProcessG1ParentCreationTime

Stage 13: join

| join kind=leftouter (
                        DeviceProcessEvents
                        | where Timestamp > ago(timeframe)
                        | project DeviceId, InitiatingProcessFileName=FileName,InitiatingProcessSHA1=SHA1, InitiatingProcessId=ProcessId,  InitiatingProcessCommandLine=ProcessCommandLine, InitiatingProcessCreationTime=ProcessCreationTime, 
                                  InitiatingProcessParentFileName=InitiatingProcessFileName,InitiatingProcessParentSHA1=InitiatingProcessSHA1, InitiatingProcessParentId=InitiatingProcessId,  InitiatingProcessParentCommandLine=InitiatingProcessCommandLine, InitiatingProcessParentCreationTime=InitiatingProcessCreationTime
                        ) 
                        on DeviceId , InitiatingProcessParentFileName , InitiatingProcessParentId, InitiatingProcessParentCreationTime

Stage 14: join

| join kind=leftouter (
                            DeviceProcessEvents
                            | where Timestamp > ago(timeframe)
                            | project Timestamp, DeviceId, FileName,SHA1, ProcessId, ProcessCommandLine, ProcessCreationTime, 
                                      InitiatingProcessFileName,InitiatingProcessSHA1, InitiatingProcessId, InitiatingProcessCommandLine, InitiatingProcessCreationTime
                            ) 
                            on DeviceId , InitiatingProcessFileName , InitiatingProcessId, InitiatingProcessCreationTime

The stages below run on _process_tree_data (the outer pipeline).

Stage 15: where

_process_tree_data
| where Timestamp > ago(1d)

Stage 16: summarize

| summarize arg_max(Timestamp,*), pattern_count=count() by DeviceId, InitiatingProcessG3ParentFileName, InitiatingProcessG2ParentFileName, InitiatingProcessG1ParentFileName, InitiatingProcessParentFileName, InitiatingProcessFileName, FileName

Stage 17: extend

| extend N_InitiatingProcessFileName = pack_array(InitiatingProcessG3ParentFileName,InitiatingProcessG2ParentFileName,InitiatingProcessG1ParentFileName,InitiatingProcessParentFileName,InitiatingProcessFileName,FileName),
         N_InitiatingProcessCommandLine = pack_array(InitiatingProcessG3ParentCommandLine, InitiatingProcessG2ParentCommandLine, InitiatingProcessG1ParentCommandLine, InitiatingProcessParentCommandLine, InitiatingProcessCommandLine, ProcessCommandLine),
         N_InitiatingProcessId = pack_array(InitiatingProcessG3ParentId,InitiatingProcessG2ParentId,InitiatingProcessG1ParentId,InitiatingProcessParentId,InitiatingProcessId,ProcessId),
         N_InitiatingProcessCreationTime = pack_array(InitiatingProcessG3ParentCreationTime,InitiatingProcessG2ParentCreationTime,InitiatingProcessG1ParentCreationTime,InitiatingProcessParentCreationTime,InitiatingProcessCreationTime,ProcessCreationTime)

Stage 18: mv-expand

| mv-expand N_InitiatingProcessFileName, N_InitiatingProcessCommandLine, N_InitiatingProcessId, N_InitiatingProcessCreationTime

Stage 19: extend

| extend join_key = strcat(DeviceId,'-',N_InitiatingProcessFileName,'-',N_InitiatingProcessId,'-',tostring(N_InitiatingProcessCreationTime))

Stage 20: where

| where isnotnull(N_InitiatingProcessId)

Stage 21: join

| join kind=inner (DeviceNetworkEvents | where Timestamp > ago(timeframe)| extend join_key = strcat(DeviceId,'-', InitiatingProcessFileName,'-', InitiatingProcessId,'-', tostring(InitiatingProcessCreationTime)) ) on join_key

Stage 22: where

| where RemoteIP !in ("::1","127.0.0.1","::ffff:127.0.0.1") and RemoteIPType <> "Private"

Stage 23: where

| where not(RemoteUrl has_any("corel.com","ocsp.digicert.com","avast.com"))

Stage 24: where

| where ActionType != "ListeningConnectionCreated"

Stage 25: project-reorder

| project-reorder pattern_count, RemoteUrl, RemoteIP, RemotePort

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
ProcessCommandLinematchwhitelisted_cmdlines
FolderPathmatchwhitelist_folderpath
FolderPathmatchwhitelist_folderpath
ProcessCommandLinematchwhitelisted_cmdlines
RemoteIPin127.0.0.1, ::1, ::ffff:127.0.0.1
RemoteUrlmatchcorel.com, ocsp.digicert.com, avast.com

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)
InitiatingProcessCommandLineeq
  • svchost.exe -k netsvcs -p -s Schedule transforms: cased corpus 2 (kusto 2)
InitiatingProcessFileNameeq
  • svchost.exe transforms: cased corpus 13 (elastic 6, splunk 5, kusto 2)
N_InitiatingProcessIdis_not_null
  • (no value, null check)
RemoteIPTypene
  • Private transforms: cased
dcount_DeviceIdle
  • 5 transforms: cased corpus 2 (kusto 2)

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
DeviceIdsummarize
FileNamesummarize
InitiatingProcessFileNamesummarize
InitiatingProcessG1ParentFileNamesummarize
InitiatingProcessG2ParentFileNamesummarize
InitiatingProcessG3ParentFileNamesummarize
InitiatingProcessParentFileNamesummarize
pattern_countsummarize
N_InitiatingProcessCommandLineextend
N_InitiatingProcessCreationTimeextend
N_InitiatingProcessFileNameextend
N_InitiatingProcessIdextend
join_keyextend