Multi-Stage Channels
Adversaries may create multiple stages for command and control that are employed under different conditions or for certain functions. Use of multiple stages may obfuscate the command and control channel to make detection more difficult. Remote access tools will call back to the first-stage command and control server for instructions. The first stage may have automated capabilities to collect basic host information, update tools, and upload additional files. A second remote access tool (RAT) could be uploaded at that point to redirect the host to the second-stage command and control server. The second stage will likely be more fully featured and allow the adversary to interact with the system through a reverse shell and additional RAT features. The different stages will likely be hosted separately with no overlapping infrastructure. The loader may also have backup first-stage callbacks or Fallback Channels in case the original first-stage communication path is discovered and blocked. Known real-world examples include APT3 using SOCKS5 to proxy through 192.157.198[.]103 before connecting to a second IP on TCP/81, Lazarus Group injecting later stages into separate processes, Bazar loader downloading the Bazar backdoor as a second-stage implant, and LunarWeb using one URL for initial host profiling and two additional URLs for command retrieval.
// T1104 Multi-Stage Channels
// Primary indicator: non-browser process connects to 2+ distinct external IPs
// suggesting first-stage C2 redirection to second-stage infrastructure
let ExcludedProcs = dynamic([
"chrome.exe", "msedge.exe", "firefox.exe", "iexplore.exe", "opera.exe", "brave.exe",
"onedrive.exe", "dropbox.exe", "teams.exe", "outlook.exe", "thunderbird.exe",
"svchost.exe", "MsMpEng.exe", "DiagTrack.exe", "wuauclt.exe", "WaaSMedicAgent.exe",
"SearchIndexer.exe", "backgroundTaskHost.exe", "RuntimeBroker.exe"
]);
let MultiStageConns = DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| where not(InitiatingProcessFileName has_any (ExcludedProcs))
| where InitiatingProcessIntegrityLevel in ("Medium", "High", "System")
| summarize
UniqueRemoteIPs = dcount(RemoteIP),
RemoteIPList = make_set(RemoteIP, 15),
RemotePorts = make_set(RemotePort, 10),
ConnectionCount = count(),
FirstContact = min(Timestamp),
LastContact = max(Timestamp)
by DeviceName,
AccountName = InitiatingProcessAccountName,
ProcessFileName = InitiatingProcessFileName,
ProcessCommandLine = InitiatingProcessCommandLine,
ProcessId = InitiatingProcessId
| where UniqueRemoteIPs >= 2
| extend DurationMinutes = datetime_diff('minute', LastContact, FirstContact)
| extend StrongIndicator = UniqueRemoteIPs >= 3
| project
LastContact, DeviceName, AccountName, ProcessFileName, ProcessCommandLine,
UniqueRemoteIPs, RemoteIPList, RemotePorts, ConnectionCount, DurationMinutes, StrongIndicator
| sort by UniqueRemoteIPs desc, LastContact desc;
// Secondary indicator: parent process connects to one external IP, spawns child that connects to a DIFFERENT external IP
let ParentNetConns = DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| where not(InitiatingProcessFileName has_any (ExcludedProcs))
| summarize ParentIPSet = make_set(RemoteIP, 10)
by DeviceName, ParentProcId = InitiatingProcessId, ParentFileName = InitiatingProcessFileName;
let ChildHandoff = DeviceNetworkEvents
| where Timestamp > ago(24h)
| where RemoteIPType == "Public"
| where not(InitiatingProcessFileName has_any (ExcludedProcs))
| join kind=inner (ParentNetConns) on DeviceName,
$left.InitiatingProcessParentId == $right.ParentProcId
| where not(set_has_element(ParentIPSet, RemoteIP))
| project
Timestamp, DeviceName,
ChildFileName = InitiatingProcessFileName,
ChildCommandLine = InitiatingProcessCommandLine,
ChildRemoteIP = RemoteIP, ChildRemotePort = RemotePort,
ParentFileName, ParentIPSet
| extend StrongIndicator = true
| sort by Timestamp desc;
union
(MultiStageConns | extend DetectionType = "SingleProcessMultiStageC2"),
(ChildHandoff | extend DetectionType = "ParentChildC2Handoff", AccountName = "",
ProcessFileName = ChildFileName, ProcessCommandLine = ChildCommandLine,
UniqueRemoteIPs = 2, RemoteIPList = ParentIPSet, RemotePorts = dynamic([]),
ConnectionCount = 1, DurationMinutes = 0, LastContact = Timestamp)
| sort by LastContact desc Data Sources
Required Tables
False Positives
- Update managers and package tools (e.g., npm, pip, choco) that sequentially contact CDNs and registries during install — these appear as single process connecting to multiple external IPs
- Security agents and EDR tools that phone home to health endpoints and telemetry endpoints at different IP addresses as part of normal heartbeat operations
- Development toolchains that pull dependencies from multiple distinct external hosts (e.g., cargo, go get, Maven) during build operations from developer workstations
- Remote monitoring and management (RMM) agents such as ConnectWise or NinjaRMM that maintain connections to multiple infrastructure IPs for load balancing and failover
- Backup agents (Veeam, Acronis) that contact licensing servers, cloud repositories, and update endpoints sequentially as part of a backup job
References (9)
- https://attack.mitre.org/techniques/T1104/
- https://www.fireeye.com/blog/threat-research/2014/11/operation_doubletap.html
- https://www.welivesecurity.com/en/eset-research/lunar-toolset-undocumented-backdoors-turla/
- https://unit42.paloaltonetworks.com/valak-malware/
- https://www.cybereason.com/blog/a-bazar-of-tricks-following-team9s-development-cycles
- https://blog.morphisec.com/snip3-an-investigation-into-new-crypter-as-a-service
- https://blog.talosintelligence.com/muddywater/
- https://blog.talosintelligence.com/salt-typhoon-analysis/
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1104/T1104.md
Unlock Pro Content
Get the full detection package for T1104 including response playbook, investigation guide, and atomic red team tests.