PowerShell remoting leaves its source in the network events, not in the process event's remote-session fields.
PowerShell remoting is how Windows administrators manage machines they aren't sitting at, and it is how attackers move between them once they hold an administrator's credentials. MITRE tracks it as T1021.006, Windows Remote Management. The traffic is ordinary, the tooling is built in, and the commands run under a signed Microsoft process. A hunt for it has to answer three questions: who ran it, what ran, and where the session came from.
Defender for Endpoint's process table looks as if it answers the third question directly. DeviceProcessEvents carries three columns with promising names: IsInitiatingProcessRemoteSession, InitiatingProcessRemoteSessionDeviceName and InitiatingProcessRemoteSessionIP. Filter on them, the reasoning goes, and every remotely started process names its source.
Read Microsoft's definitions before relying on them. The first column "indicates whether the initiating process was run under a remote desktop protocol (RDP) session". The other two are the device name and IP address "from which the initiating process's RDP session was initiated". They describe Remote Desktop. A WinRM session isn't an RDP session, so a PowerShell remoting session is not what those columns record, and a hunt that filters on them for remoting comes back empty and looks clean.
This post covers where remoting does show up, how far the network events can take you toward a source and where they stop, the same hunt in Splunk and Sigma, and a test plan to check all of it in your own lab.
What a remoting session leaves on the target
On the machine receiving the session, WinRM spawns a process running as the connecting user to host PowerShell, as Microsoft's remoting security page puts it, and that host is wsmprovhost.exe. An executable the session launches, such as whoami.exe or cmd.exe, has that host as its parent. A cmdlet such as Get-Service is PowerShell code running inside the host and starts no process of its own, so process telemetry sees the executables a session launches and nothing else. The host process is the foundation of both public detections worth knowing:
- SigmaHQ's
proc_creation_win_winrm_remote_powershell_session_processrule flagswsmprovhost.exeas either the image or the parent image, and says it indicates an active remote PowerShell session. - Elastic's "Incoming Execution via PowerShell Remoting" rule pairs two events on the same host: an inbound connection to port 5985 or 5986 from anything other than loopback, then a process whose parent is
wsmprovhost.exe, within 30 seconds.
Start with the process half and check what the remote-session columns say about it in your own tenant:
// Table: DeviceProcessEvents (Defender for Endpoint, advanced hunting)
// Hypothesis: processes started by PowerShell remoting carry wsmprovhost.exe as their parent,
// and the RDP-defined remote-session columns should say nothing about them.
DeviceProcessEvents
| where Timestamp > ago(30d)
| where InitiatingProcessFileName =~ "wsmprovhost.exe"
| summarize Processes = count(),
Accounts = dcount(AccountName),
Devices = dcount(DeviceId),
FlaggedRemote = countif(IsInitiatingProcessRemoteSession == true),
WithSourceDevice = countif(isnotempty(InitiatingProcessRemoteSessionDeviceName)),
WithSourceIP = countif(isnotempty(InitiatingProcessRemoteSessionIP))
// Read the last three counts against Processes. If they are zero, the columns can't name a WinRM source here
// and the source has to come from DeviceNetworkEvents, below.Those counts are the first finding: most estates have a small, stable set of accounts running remoting against known machines, and a new account or a new target is the lead.
The account in the row is who authenticated, not who typed
The account on a wsmprovhost.exe child is the account the session authenticated as. If the administrator ran the command, that is the right account. If someone else holds the administrator's password, it is the same account. The process events can tell you who and what. They can't tell you which keyboard the session came from, and remoting traffic from the administrators' usual workstation means something different from the same commands arriving from a laptop that has never managed anything.
That is why the source matters, and why it has to come from a different table.
RDP and WinRM leave different evidence
None of this makes the three columns useless. They answer the question they were built for: a process started inside a Remote Desktop session carries the name and address of the machine the session came from. The mistake is reaching for them when the question is about WinRM. The two kinds of session leave evidence in different places:
The same question, two kinds of session, different tables.
On the logon side, Defender's DeviceLogonEvents names RDP as remote interactive and records a network logon for shared resources; Windows event 4624 numbers them 10 and 3. Microsoft notes that 4624's address fields depend on the protocol: Kerberos network logons are likely to carry no workstation name, and NTLM logons no TCP/IP details. A logon event narrows the source; it doesn't always name it.
Getting the source from the inbound connection
The target accepted a connection on the WinRM port before the session's processes started. Defender records it in DeviceNetworkEvents as an inbound accepted connection, and RemoteIP on that row is the address the connection came from. Pair the two the way Elastic's rule does, same device, the connection first, the process within 30 seconds, and treat the result as a candidate source: time proximity is correlation, and the query below keeps every candidate rather than choosing one.
// Tables: DeviceNetworkEvents, DeviceProcessEvents (Defender for Endpoint, advanced hunting)
// Hypothesis: a process started by the WinRM host shortly after the device accepted a connection on a
// WinRM port. RemoteIP on that connection is a candidate source; more than one candidate means ambiguous.
let Window = 30s; // the same maxspan as Elastic's sequence rule; widen it if your sessions start slowly
let Inbound = DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "InboundConnectionAccepted"
| where LocalPort in (5985, 5986) // WinRM 2.0 default listeners; add any port you configured yourself
| where RemoteIP !in ("127.0.0.1", "::1") // loopback sessions are local tooling, as in Elastic's rule
| project DeviceId, ConnTime = Timestamp, RemoteIP;
DeviceProcessEvents
| where Timestamp > ago(7d)
| where InitiatingProcessFileName =~ "wsmprovhost.exe"
// The five console helpers Elastic's rule excludes: sessions start them constantly and they say nothing
| where FileName !in~ ("chcp.com", "conhost.exe", "hostname.exe", "netstat.exe", "quser.exe")
| join kind=inner Inbound on DeviceId
| where Timestamp between (ConnTime .. (ConnTime + Window))
// One process can fall inside the window of several connections; keep every candidate and label the row
| summarize Candidates = make_set(RemoteIP, 5) by DeviceName, AccountName, Timestamp, ProcessCommandLine
| extend Attribution = iff(array_length(Candidates) == 1, "single candidate", "ambiguous")
| summarize Processes = count(), FirstSeen = min(Timestamp), LastSeen = max(Timestamp),
Commands = make_set(ProcessCommandLine, 25)
by DeviceName, AccountName, Sources = tostring(Candidates), Attribution
| order by Processes desc
// Expect admin workstations and management servers as Sources. An unfamiliar address is the lead;
// an ambiguous row needs the logon events or the source side before anyone names a machine.What this can and can't attribute
The paired query is a narrowing tool. Before its output goes into a hunt record, these limits belong next to it:
- Cmdlets leave no process. Commands that run inside
wsmprovhost.exenever reachDeviceProcessEvents. A session that only ran cmdlets is visible as a connection and a logon, and its content is in PowerShell script block logging (event 4104) or nowhere. - Concurrent sessions blur the window. Two connections to one device inside 30 seconds give one process two candidates. The query labels those rows ambiguous; it can't resolve them.
- Persistent sessions outlive the window. A
PSSessionstays available until it is deleted or times out, and later commands run in it. Whether a command sent ten minutes later produces a fresh inbound connection is something to test in your build, not assume; if it doesn't, the 30-second pairing misses it, and widening the window adds false pairs. - An address isn't a machine. NAT, a jump host or a proxy appears as the source. Resolve
RemoteIPagainstDeviceNetworkInfoor DHCP for the time of the session.
The same hunt without Defender
On hosts where you collect Sysmon, event 1 records process creation with the parent image, and event 3 records network connections with source and destination address and port. Event 3 is disabled by default, so check that your configuration turns it on before you rely on the network half.
sourcetype="XmlWinEventLog:Microsoft-Windows-Sysmon/Operational"
((EventCode=3 DestinationPort IN (5985, 5986) NOT SourceIp IN ("127.0.0.1", "::1"))
OR (EventCode=1 ParentImage="*\\wsmprovhost.exe"
NOT Image IN ("*\\chcp.com", "*\\conhost.exe", "*\\HOSTNAME.EXE", "*\\NETSTAT.EXE", "*\\quser.exe")))
| eval conn_src=if(EventCode=3, SourceIp, null())
| sort 0 _time
| streamstats time_window=30s values(conn_src) as session_src by host
| where EventCode=1 AND isnotnull(session_src)
| stats count as processes, min(_time) as first_seen, values(CommandLine) as commands
by host, User, session_srcThe streamstats window carries every WinRM source from the last 30 seconds on the same host onto each child process, so a session_src with more than one value is the same ambiguity the KQL labels. Hosts without event 3 return nothing, which is a gap to fix, not a clean result.
For anything you convert from Sigma, the process half with the same exclusions:
title: Process Started by the WinRM Host, Console Helpers Excluded
status: experimental
description: A process whose parent is wsmprovhost.exe, the host for incoming PowerShell remoting sessions, without the console helpers every session starts.
tags:
- attack.lateral-movement
- attack.t1021.006
- attack.execution
- attack.t1059.001
logsource:
category: process_creation
product: windows
detection:
selection:
ParentImage|endswith: '\wsmprovhost.exe'
filter_helpers:
Image|endswith:
- '\chcp.com'
- '\conhost.exe'
- '\hostname.exe'
- '\netstat.exe'
- '\quser.exe'
condition: selection and not filter_helpers
falsepositives:
- Administrators and monitoring tools using PowerShell remoting; baseline the accounts and sources before alerting
level: mediumTreat it as a hunt or a baseline before it is an alert: remoting is normal administration, so the signal is change.
IsInitiatingProcessRemoteSession, the hunt answered whether anyone used Remote Desktop. Record which table answered the source question, or record that nothing did.
Reading what the sessions ran
Administrative remoting is repetitive: the same commands, the same few accounts, the same groups of machines, mostly in working hours. What deserves a closer look is anything outside that routine: reading credential stores or registry hives, compressing directories, downloading content, creating services or scheduled tasks, adding local group members, a command seen once on one machine, or an account appearing at an hour it never works.
The view from the source
If the source is onboarded too, it saw the session from the other end: an outbound connection from powershell.exe or pwsh.exe to the target's WinRM port. That is the corroboration an ambiguous row needs, and the fastest answer to which machines a workstation has been managing:
// Table: DeviceNetworkEvents (Defender for Endpoint, advanced hunting)
// Hypothesis: a device opened WinRM connections from PowerShell; the targets are what it administered.
DeviceNetworkEvents
| where Timestamp > ago(7d)
| where ActionType == "ConnectionSuccess"
| where RemotePort in (5985, 5986)
| where InitiatingProcessFileName in~ ("powershell.exe", "pwsh.exe")
| summarize Connections = count(), Targets = dcount(RemoteIP), TargetList = make_set(RemoteIP, 50)
by DeviceName, InitiatingProcessAccountName
| order by Targets desc
// A workstation reaching many targets, or an account that normally manages a handful reaching fifty,
// is the shape of lateral movement and worth reading against the target-side hunt above.Test it before you trust it
Every claim above can be checked in a lab with one source machine and one target, both onboarded to Defender, with Sysmon network events and script block logging on; replace TARGET with the target's name. The expected column comes from the documentation cited here. Run each action, record what actually arrived, and treat any difference as the finding.
| # | Action from the source | Expected on the target | What it tests |
|---|---|---|---|
| 1 | Invoke-Command -ComputerName TARGET -ScriptBlock { whoami } | Inbound accept on 5985 from the source; whoami.exe with parent wsmprovhost.exe; remote-session columns empty; a network logon | The paired query names the source |
| 2 | Invoke-Command -ComputerName TARGET -ScriptBlock { Get-Service } | Inbound accept and logon; no child process for the cmdlet; the script block in event 4104 | Process hunting misses cmdlets |
| 3 | New-PSSession, wait ten minutes, then Invoke-Command -Session $s -ScriptBlock { whoami } | whoami.exe under wsmprovhost.exe; whether a new inbound accept precedes it is the open question | The 30-second window and persistent sessions |
| 4 | Two sources run test 1 against the target within 30 seconds | Each child process shows two candidates | Ambiguous rows are flagged, not guessed |
| 5 | RDP to the target, then run whoami in the session | Remote interactive logon; remote-session columns carry the source | The columns work for what they describe |
| 6 | Run whoami locally on the target | No wsmprovhost.exe parent; the paired query returns nothing | No false positive on local activity |
Write down the environment with the results: Windows build on both machines, PowerShell version, Defender sensor version, Sysmon configuration, and whether the session used HTTP or HTTPS. A result without them can't be compared with anyone else's.
Where to read more
- Microsoft Learn, the
DeviceProcessEventstable reference, for the definitions of the three remote-session columns quoted above. - Microsoft Learn, "Installation and configuration for Windows Remote Management", for the default listener ports and how listeners are configured.
- Elastic Security, the "Incoming Execution via PowerShell Remoting" prebuilt rule, for the sequence logic and its exclusions.
- SigmaHQ,
proc_creation_win_winrm_remote_powershell_session_process.yml, the process-only rule. - Microsoft Learn, "Security considerations for PowerShell Remoting using WinRM", for how WinRM hosts sessions and authenticates.
- Microsoft Learn,
about_PSSessions, for persistent sessions, and theDeviceLogonEventsand event 4624 references for logon types and their address fields. - MITRE ATT&CK, T1021.006 Windows Remote Management and T1059.001 PowerShell.
What to do this week
- Run the column check query against 30 days of
DeviceProcessEvents. If the three remote-session counts are zero whileProcessesisn't, any rule that filters on those columns for remoting is answering the RDP question. - Search your saved hunts and analytics rules for
IsInitiatingProcessRemoteSession, and note which were written to catch PowerShell remoting. - Run the paired query over seven days and list every source it returns, with the ambiguous rows set aside until the source side or the logon events settle them.
- Check where WinRM listens with
winrm enumerate winrm/config/listeneron a few servers, and add any custom port to the filter. - Run the six tests in your lab and fill in what actually arrived.
The empty columns aren't a broken sensor; they answer a question about Remote Desktop. Run the test plan in your own lab, record your environment next to your results, and compare them with the expected column. Where your telemetry disagrees with this post, your telemetry is right, and the difference is the most useful thing the exercise will produce.