Windows Server for beginners. Part 14: How do WinRM and PowerShell Remoting work?

All parts of the series “Windows Server for beginners”

In part 10 we ran Set-DnsClientServerAddress -CimSession dc1 and Install-WindowsFeature -ComputerName dc2 from a workstation, and the commands simply worked. Behind them was WinRM — a service that has been enabled on every Windows Server since 2012 and that carries almost all remote management: modules with -ComputerName, Server Manager, Windows Admin Center, System Center and, above all, PowerShell Remoting. This part is about how it's built, how it authenticates, what it encrypts, where its limits are and how to get around them properly.


WS-Management

WinRM is Microsoft's implementation of the open WS-Management standard (DMTF DSP0226): managing systems through SOAP messages over HTTP. The client sends POST /wsman with an XML body containing the operation (Get, Put, Enumerate, Invoke, Create) and the resource address; the server replies with XML. Resources can be WMI classes (that's how Get-CimInstance -ComputerName works), and they can be a shell: PowerShell Remoting is the resource http://schemas.microsoft.com/powershell/Microsoft.PowerShell, inside which the PSRP protocol (PowerShell Remoting Protocol, MS-PSRP) travels in SOAP envelopes — serialized objects, commands and output streams.

The WinRM service listens through HTTP.sys — the same driver as in part 2, so ports 5985 (HTTP) and 5986 (HTTPS) belong to the System process rather than to the service, and an anonymous request is answered by HTTP.sys itself with an offer to authenticate:

PS C:\> winrm enumerate winrm/config/listener
Listener
    Address = *
    Transport = HTTP
    Port = 5985
    Hostname
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint
    ListeningOn = 127.0.0.1, 192.168.1.10, ::1, 2001:db8:1::10, fe80::…

PS C:\> Get-NetTCPConnection -LocalPort 5985 -State Listen | Select-Object LocalAddress, LocalPort, OwningProcess, @{n='Process'; e={(Get-Process -Id $_.OwningProcess).Name}}

LocalAddress LocalPort OwningProcess Process
------------ --------- ------------- -------
::                5985             4 System

PS C:\> $r = Invoke-WebRequest -Method Post -Uri http://dc1:5985/wsman -ContentType 'application/soap+xml;charset=UTF-8' -Body '<x/>' -SkipHttpErrorCheck
PS C:\> $r.StatusCode; $r.Headers['Server']; $r.Headers['WWW-Authenticate']
401
Microsoft-HTTPAPI/2.0
Negotiate
Kerberos

PS C:\> Get-ChildItem WSMan:\localhost\Service

   WSManConfig: Microsoft.WSMan.Management\WSMan::localhost\Service

Type            Name                           SourceOfValue   Value
----            ----                           -------------   -----
System.String   RootSDDL                                       O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)
System.String   MaxConcurrentOperations                        4294967295
System.String   MaxConcurrentOperationsPerUser                 1500
System.String   EnumerationTimeoutms                           240000
System.String   MaxConnections                                 300
System.String   MaxPacketRetrievalTimeSeconds                  120
System.String   AllowUnencrypted                               false
Container       Auth
Container       DefaultPorts
System.String   IPv4Filter                                     *
System.String   IPv6Filter                                     *
System.String   EnableCompatibilityHttpListener                false
System.String   EnableCompatibilityHttpsListener               false
System.String   CertificateThumbprint
System.String   AllowRemoteAccess                              true

All the configuration lives in the WSMan: provider (reading and changing it takes administrator rights): listeners, service, client and authentication settings, shell limits (WSMan:\localhost\Shell: memory per shell, shells per user, idle timeout). The older interface to the same thing is winrm get winrm/config.


Enable-PSRemoting

On Windows Server everything is already on: the service is running, an HTTP listener exists, the "Windows Remote Management (HTTP-In)" firewall rules are enabled — unrestricted for the Domain and Private profiles, local subnet only for Public. On Windows 10 and 11 there's no listener by default, and to accept connections you run:

PS C:\> Enable-PSRemoting -Force

What the command does: starts the WinRM service and sets it to start automatically; creates an HTTP listener on all addresses; enables the firewall rule for 5985 (on client Windows — for the Domain and Private profiles, and if any network adapter is in the Public profile the command refuses until you add -SkipNetworkProfileCheck, and then the Public rule allows only the local subnet); registers session configurations and grants access to them to administrators and the Remote Management Users group; and restarts the WinRM service. The older equivalent is winrm quickconfig.

Which configurations appear depends on where the command runs. In Windows PowerShell 5.1 they are Microsoft.PowerShell, Microsoft.PowerShell.Workflow and, on a 64-bit system, Microsoft.PowerShell32. In PowerShell 7 they are two endpoints for pwsh, a short PowerShell.7 and a full one with the version number, for example PowerShell.7.4.6; it doesn't touch the Windows PowerShell endpoints.

To connect, nothing needs to be enabled on the client; the service is only needed to store client settings such as TrustedHosts, covered below.

A separate caveat concerns local accounts. User Account Control (UAC) gives a local administrator who signs in over the network a filtered token without administrator rights; domain administrators aren't affected. It's lifted with the LocalAccountTokenFilterPolicy = 1 registry value, which removes the restriction for every user of the machine at once, which is why in a domain remote sign-ins use domain accounts.

Session configurations are the endpoints a client connects to. It's easiest to look at them from the side being connected to:

PS C:\> Invoke-Command dc1 { Get-PSSessionConfiguration } | Format-Table Name, PSVersion, Permission -AutoSize

Name                                     PSVersion Permission
----                                     --------- ----------
microsoft.powershell                     5.1       NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
microsoft.powershell.workflow            5.1       BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
microsoft.powershell32                   5.1       NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed, BUILTIN\Remote Management Users AccessAllowed
microsoft.windows.servermanagerworkflows 3.0       NT AUTHORITY\INTERACTIVE AccessAllowed, BUILTIN\Administrators AccessAllowed

This is a fresh Windows Server 2025 with PowerShell 7 installed: there are no PowerShell.7 endpoints until Enable-PSRemoting has been run on it from pwsh. Note that the list came back from a Windows PowerShell 5.1 session: by default the client goes to microsoft.powershell, whatever version it connects from, and that runs Windows PowerShell. And Get-PSSessionConfiguration itself, run in pwsh on the server, shows only PowerShell 7 endpoints — an empty list on this server. To reach PowerShell 7 on the server you connect with -ConfigurationName PowerShell.7. An endpoint of your own with its own rights and restrictions is JEA, the last section of this part.


Sessions

There are three ways to run a command on another server.

Interactively, like SSH:

PS C:\> Enter-PSSession -ComputerName dc1
[dc1]: PS C:\Users\alice.admin\Documents> Get-Service NTDS, DNS | Select-Object Name, Status

Name Status
---- ------
DNS  Running
NTDS Running

[dc1]: PS C:\Users\alice.admin\Documents> Exit-PSSession
PS C:\>

The prompt shows where you are; every command runs on dc1 in a wsmprovhost.exe process under your account. It's convenient for one-off actions and useless for scripts: Enter-PSSession inside a script doesn't carry the following lines into the session.

In batch, on one server or many:

PS C:\> Invoke-Command -ComputerName dc1, dc2, dc3 -ScriptBlock { Get-Service NTDS | Select-Object Status, @{n='Uptime'; e={(Get-Date) - (Get-Process lsass).StartTime}} }

Status  Uptime             PSComputerName RunspaceId
------  ------             -------------- ----------
Running 1.10:20:16.4246245 dc3            78d7553c-62c1-4019-a961-cd7e7d3606fb
Running 08:28:38.6201064   dc1            82bba145-11b2-44fa-a802-1b2f161bf6a7
Running 1.10:21:24.7876073 dc2            a9b705ea-cb1f-46c6-bf9b-ec2c7b9b0778

The block runs on all servers in parallel (up to 32 at a time, -ThrottleLimit), results arrive in the order they finish rather than the order listed, and each one gets PSComputerName and RunspaceId attached. Local variables aren't visible inside the block: they're passed with -ArgumentList and param() or with $using:name:

PS C:\> $days = 7
PS C:\> Invoke-Command -ComputerName dc1, dc2 -ScriptBlock { Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=(Get-Date).AddDays(-$using:days)} | Measure-Object | Select-Object Count }

Count PSComputerName RunspaceId
----- -------------- ----------
   16 dc2            8582f99b-9b17-41db-b05c-20d261290186
   42 dc1            e3c63bd3-ee68-4cad-b5b7-0ed8db7b1a98

Without $using: the $days variable would be empty inside the block. -FilePath .\script.ps1 sends a local script file and runs it on each server; -AsJob returns control at once, creating a RemoteJob on the client with a child job per server, and the result is collected later with Receive-Job.

Through a persistent session, when there are many commands and state between them must be kept:

PS C:\> $s = New-PSSession -ComputerName dc1, dc2
PS C:\> Invoke-Command -Session $s -ScriptBlock { $before = (Get-Service W32Time).Status }
PS C:\> Invoke-Command -Session $s -ScriptBlock { Restart-Service W32Time; "$before -> $((Get-Service W32Time).Status)" }
Running -> Running
Running -> Running
PS C:\> Copy-Item .\fix-time.ps1 -Destination C:\Temp\ -ToSession $s[0]
PS C:\> Remove-PSSession $s

The $before variable created in the first call is alive in the second: it's the same process on the server. Copy-Item -ToSession and -FromSession transfer files over the same channel without SMB; the destination folder must exist, otherwise the answer is "The directory name is invalid.". A session can be disconnected and reconnected later, even from another machine: a long task on the server keeps running while the client is closed:

PS C:\> $s = New-PSSession -ComputerName dc2 -Name nightly
PS C:\> Disconnect-PSSession $s

 Id Name            Transport ComputerName    ComputerType    State         ConfigurationName     Availability
 -- ----            --------- ------------    ------------    -----         -----------------     ------------
  1 nightly         WSMan     dc2             RemoteMachine   Disconnected  Microsoft.PowerShell          None

PS C:\> Get-PSSession -ComputerName dc2        # even from another machine

 Id Name            Transport ComputerName    ComputerType    State         ConfigurationName     Availability
 -- ----            --------- ------------    ------------    -----         -----------------     ------------
  1 nightly         WSMan     dc2             RemoteMachine   Disconnected  Microsoft.PowerShell          None

PS C:\> $s = Connect-PSSession -ComputerName dc2 -Name nightly

Disconnected sessions appeared in PowerShell 3.0; you can disconnect a session from New-PSSession or Invoke-Command -InDisconnectedSession, but not an interactive Enter-PSSession session.

Deserialization. Objects come back from a session not live: PSRP serializes them to XML and rebuilds them on the client as objects of type Deserialized.System.ServiceProcess.ServiceController — with all properties as of serialization time but without methods (only GetType and ToString remain). Hence a trap: Invoke-Command dc1 { Get-Service W32Time } | Stop-Service won't stop the service on dc1. Stop-Service doesn't recognize a service in the snapshot, takes its Name property and stops the service with that name on the local machine — you can check it with -WhatIf:

PS C:\> Invoke-Command dc1 { Get-Service W32Time } | Stop-Service -WhatIf
What if: Performing the operation "Stop-Service" on target "Windows Time (W32Time)".

Whatever needs to be done with an object is done inside the block. The second consequence: Format-Table inside the block returns formatting objects (FormatStartData, FormatEntryData and so on) rather than data. They still display on screen, but they can't be filtered, sorted or exported to CSV — format outside the block.


Authentication

The default is Negotiate: Kerberos if possible, otherwise NTLM. Kerberos needs what we covered in part 6: client and server in a domain or in trusting domains, access by name, clocks within five minutes. The client requests a ticket for the SPN HTTP/dc1.corp.example.com; the computer account has no such SPN of its own, but the KDC maps HTTP to HOST through the sPNMappings attribute in the configuration partition and issues a ticket for the dc1$ account. On Windows Server 2025 and Windows 11 24H2 the WinRM client has the setting spn_prefix = HOST in WSMan:\localhost\Client, and it asks for HOST/dc1.corp.example.com directly — that's what klist shows after connecting. The WinRM service also registers WSMAN/dc1 and WSMAN/dc1.corp.example.com on the computer account (we saw them in setspn -L), but the PowerShell client doesn't ask for them by default. If HTTP/dc1.corp.example.com is explicitly registered on another account, for example an IIS application pool, Kerberos to WinRM breaks: the ticket is encrypted with the wrong key; then New-PSSessionOption -IncludePortInSPN helps, with which the SPN is HTTP/dc1.corp.example.com:5985. When the conditions are met, everything works without parameters and without typing a password: the ticket comes from the logon session.

NTLM kicks in when Kerberos is impossible: the server is in a workgroup, it's accessed by IP address, the client isn't in the domain. And here the WinRM client refuses to connect:

PS C:\> Enter-PSSession -ComputerName 192.168.1.20 -Credential CORP\alice.admin
Enter-PSSession: Connecting to remote server 192.168.1.20 failed with the following error message : The WinRM client cannot process the request. Default authentication may be used with an IP address under the following conditions: the transport is HTTPS or the destination is in the TrustedHosts list, and explicit credentials are provided. Use winrm.cmd to configure TrustedHosts. Note that computers in the TrustedHosts list might not be authenticated. For more information on how to set TrustedHosts run the following command: winrm help config. For more information, see the about_Remote_Troubleshooting Help topic.

The reason is mutual authentication. Kerberos proves to the client that the server is genuine (the AP-REP from part 6). NTLM doesn't: the client will send its challenge response to anyone claiming to be 192.168.1.20, and if that's an impostor, it gets an NTLM response it can relay. TrustedHosts is the list of names and addresses for which the client's administrator accepts that risk:

PS C:\> Set-Item WSMan:\localhost\Client\TrustedHosts -Value 'srv-wg.example.net,192.168.1.20' -Concatenate
PS C:\> Get-Item WSMan:\localhost\Client\TrustedHosts | Select-Object -ExpandProperty Value
srv-wg.example.net,192.168.1.20

The value * allows NTLM with everyone, and it gets set in labs without realizing it means "I trust anyone who answers at this address". The right alternatives: join the server to the domain; or use HTTPS, where a certificate proves the server's identity and TrustedHosts isn't needed; or client certificate authentication.

Other schemes are -Authentication Basic (name and password in clear text; WSMan:\localhost\Service\Auth\Basic is false by default, and without encryption the service won't accept it unless AllowUnencrypted is allowed, so in practice — HTTPS only), Certificate (a client certificate mapped to a local account on the server) and CredSSP, covered in the section on the second hop.

Encryption. Port 5985 is HTTP, which is confusing, but the content over it is encrypted: Negotiate yields not only authentication but also a session key, and WinRM encrypts the SOAP body with it (in HTTP this looks like Content-Type: multipart/encrypted;protocol="application/HTTP-SPNEGO-session-encrypted"; with an explicit -Authentication Kerberos — HTTP-Kerberos-session-encrypted, with CredSSP — HTTP-CredSSP-session-encrypted). The service doesn't accept unencrypted exchanges (AllowUnencrypted = false). HTTPS is needed for Basic, for trusting a server outside the domain and for policies that formally require TLS.


The HTTPS listener

You need a certificate in the computer store with the Server Authentication purpose and the server name in the CN or SAN, neither expired nor revoked. In an organization it's issued by the certification authority from part 25. winrm quickconfig -transport:https picks a certificate itself and won't take a self-signed one; a listener with an explicitly given thumbprint can be created with a self-signed one too, which is fine for a lab:

PS C:\> $cert = New-SelfSignedCertificate -DnsName dc1.corp.example.com -CertStoreLocation Cert:\LocalMachine\My
PS C:\> New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbprint $cert.Thumbprint -Force
PS C:\> New-NetFirewallRule -DisplayName 'WinRM HTTPS' -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow -Profile Domain, Private
PS C:\> netsh http show sslcert ipport=0.0.0.0:5986

    IP:port                      : 0.0.0.0:5986
    Certificate Hash             : 3f1a…c9e2
    Application ID               : {afebb9ad-9b97-4a91-9ab5-daf4d59122f6}

The service bound the certificate to the port in HTTP.sys itself — this is the same netsh http sslcert as in part 2, and Application ID identifies the application that created the binding. The firewall rule for 5986 has to be created by hand: Enable-PSRemoting opens only 5985.

Client: Enter-PSSession -ComputerName dc1.corp.example.com -UseSSL. With a self-signed certificate it will refuse on the trust chain; to get around it temporarily — -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck), or put the certificate into the trusted roots on the client.


The second hop

We connect to srv1 and from there try to open a folder on dc1:

PS C:\> Enter-PSSession srv1
[srv1]: PS C:\Users\alice.admin\Documents> Get-ChildItem \\dc1\SYSVOL
Get-ChildItem: Cannot find path '\\dc1\SYSVOL' because it does not exist.
[srv1]: PS C:\Users\alice.admin\Documents> klist

Current LogonId is 0:0xf4cf32

Cached Tickets: (1)

#0>	Client: alice.admin @ CORP.EXAMPLE.COM
	Server: HOST/srv1.corp.example.com @ CORP.EXAMPLE.COM
	KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
	Ticket Flags 0x40a10000 -> forwardable renewable pre_authent name_canonicalize
	…
	Cache Flags: 0x8 -> ASC

The path does exist, of course, and this isn't a permissions error in the usual sense. The session on srv1 holds only the ticket we came with (the ASC flag — an accepted security context) and no TGT at all; to reach cifs/dc1 on our behalf, srv1 needs our TGT or a delegation right, and it has neither. The SMB client on srv1 has nothing to authenticate to dc1 with on our behalf, and PowerShell reports "does not exist" — a misleading message for this situation. This is the second hop problem from part 6, and it has four solutions, from worst to best.

CredSSP. The client sends the server the password (encrypted), and the server signs in with it anywhere. It's enabled on both sides:

PS C:\> Enable-WSManCredSSP -Role Client -DelegateComputer srv1.corp.example.com -Force     # on the workstation
PS C:\> Enable-WSManCredSSP -Role Server -Force                                              # on srv1
PS C:\> Enter-PSSession srv1 -Authentication CredSSP -Credential CORP\alice.admin

It always works, workgroups included (except for members of the Protected Users group), and that's exactly why it's dangerous: a compromised srv1 gets the administrator's password, and an administrator who signed in to it with CredSSP handed that password over himself. Not suitable for servers that aren't trusted unconditionally.

Credentials inside the session. Pass $cred with $using: and map the resource with it: New-PSDrive -Name S -PSProvider FileSystem -Root \\dc1\SYSVOL -Credential $using:cred. The password also travels to the server, but only in the process memory and only for one resource.

Resource-based Kerberos delegation. From part 6: the owner of the resource dc1 allows srv1 to access it on behalf of users:

PS C:\> Set-ADComputer dc1 -PrincipalsAllowedToDelegateToAccount (Get-ADComputer srv1)
PS C:\> Invoke-Command srv1 { klist purge -li 0x3e7 }     # clear the ticket cache of the srv1 computer account
PS C:\> Invoke-Command srv1 { Get-ChildItem \\dc1\SYSVOL | Select-Object -First 1 Name }

Name             PSComputerName RunspaceId
----             -------------- ----------
corp.example.com srv1           5d0c2a6e-8f41-4b7e-9a3d-1c6f0e2b7a95

klist purge is needed because the KDC remembers denials srv1 received before the change for 15 minutes. No password goes anywhere, srv1 gets a ticket for cifs/dc1 on behalf of alice.admin through S4U2Proxy, and the right is limited to one resource. The right solution for domain servers — with one caveat: this way the second hop can reach files, LDAP or SQL Server, but not WinRM on the next server.

An endpoint with RunAs or a gMSA. A session configuration on srv1 registered with -RunAsCredential or under a group managed service account from part 13: everything inside the session runs as that account, which has its own TGT and its own rights on dc1. That's already half of JEA.


JEA

Just Enough Administration solves a different problem: giving a help desk employee the right to restart a service and clear the DNS cache on a controller without making them an administrator of the controller. It has three parts.

The role capability file (.psrc) — what is visible in the session:

PS C:\> New-Item -Path 'C:\Program Files\WindowsPowerShell\Modules\JEA\RoleCapabilities' -ItemType Directory -Force | Out-Null
PS C:\> New-PSRoleCapabilityFile -Path 'C:\Program Files\WindowsPowerShell\Modules\JEA\RoleCapabilities\DnsOperator.psrc' `
    -ModulesToImport DnsServer `
    -VisibleCmdlets 'Get-DnsServerResourceRecord', 'Clear-DnsServerCache', @{ Name = 'Restart-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'DNS' } } `
    -VisibleFunctions 'Get-DnsHealth' `
    -FunctionDefinitions @{ Name = 'Get-DnsHealth'; ScriptBlock = { Get-DnsServerStatistics | Select-Object -ExpandProperty QueryStatistics } }

Three cmdlets, with Restart-Service allowed only with the value DNS, and one function. The file must sit in the RoleCapabilities folder of some module on PSModulePath for JEA to find it by name; since PowerShell 6 the file can also be given by path — with the RoleCapabilityFiles key in the role definition instead of RoleCapabilities.

The session configuration file (.pssc) — who gets which role and what it runs as:

PS C:\> New-PSSessionConfigurationFile -Path C:\JEA\DnsOps.pssc `
    -SessionType RestrictedRemoteServer `
    -RunAsVirtualAccount `
    -TranscriptDirectory C:\JEA\Transcripts `
    -RoleDefinitions @{ 'CORP\Helpdesk' = @{ RoleCapabilities = 'DnsOperator' } }
PS C:\> Test-PSSessionConfigurationFile C:\JEA\DnsOps.pssc
True

RestrictedRemoteServer is a session without a language: only calls to permitted commands, no variables, loops or expressions. RunAsVirtualAccount means commands run under a virtual account: a temporary local administrator without a password that exists while the session is open (a gMSA can be given instead with -GroupManagedServiceAccount when the commands need network access); a user from Helpdesk gets no rights on the server, and the rights inside the session are limited by the command list. TranscriptDirectory is a full record of every session: who did what and when.

Registration and connecting. Register-PSSessionConfiguration restarts the WinRM service, and every session open on the server, including the one you run it from, is dropped:

PS C:\> Register-PSSessionConfiguration -Name DnsOps -Path C:\JEA\DnsOps.pssc -Force
PS C:\> Get-PSSessionConfiguration DnsOps | Select-Object Name, RunAsVirtualAccount, Permission

Name   RunAsVirtualAccount Permission
----   ------------------- ----------
DnsOps true                CORP\Helpdesk AccessAllowed

On the help desk employee's workstation:

PS C:\> Enter-PSSession dc1 -ConfigurationName DnsOps
[dc1]: PS> Get-Command | Select-Object -ExpandProperty Name
Clear-DnsServerCache
Clear-Host
Exit-PSSession
Get-Command
Get-DnsHealth
Get-DnsServerResourceRecord
Get-FormatData
Get-Help
Measure-Object
Out-Default
Restart-Service
Select-Object
[dc1]: PS> Restart-Service NTDS
Restart-Service: Cannot validate argument on parameter 'Name'. The argument "NTDS" does not belong to the set "DNS" specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
[dc1]: PS> Restart-Service DNS
[dc1]: PS> whoami
whoami: The term 'whoami' is not recognized …

Inside the session the user is WinRM Virtual Users\WinRM_VA_1_corp_bob with administrator rights, but the only way to use them is the permitted commands. The transcript in C:\JEA\Transcripts records both the real user's name and every command. This is the tool that complements the delegation from part 13 at the server level: there — rights on directory objects, here — rights on actions on the machine.


Looking at it by hand

The process on the server where the session lives:

PS C:\> Invoke-Command dc1 { Get-Process wsmprovhost -IncludeUserName } | Select-Object Id, UserName, StartTime

  Id UserName         StartTime
  -- --------         ---------
3412 CORP\alice.admin 10/15/2026 2:02:17 PM

One wsmprovhost.exe per session, under the connecting user's account; every Invoke-Command without -Session starts a new process, and the list will include the one running this very command. With JEA and a virtual account the process runs as WinRM_VA_….

The exchange in packets: pktmon filter add -p 5985, pktmon start --capture, then Invoke-Command dc1 { hostname }, pktmon stop and pktmon etl2pcap PktMon.etl for Wireshark:

No.  Source         Destination    Protocol  Info
1    192.168.1.100  192.168.1.10   HTTP      POST /wsman HTTP/1.1
2    192.168.1.10   192.168.1.100  HTTP      HTTP/1.1 401  (WWW-Authenticate: Negotiate, Kerberos)
3    192.168.1.100  192.168.1.10   HTTP      POST /wsman HTTP/1.1  (Authorization: Negotiate, Kerberos AP-REQ)
4    192.168.1.10   192.168.1.100  HTTP      HTTP/1.1 200  (multipart/encrypted;protocol="application/HTTP-SPNEGO-session-encrypted")
5    192.168.1.100  192.168.1.10   HTTP      POST /wsman HTTP/1.1  (multipart/encrypted …)

The first request without authentication gets a 401 offering Negotiate and Kerberos, the second carries a Kerberos AP-REQ for HTTP/dc1.corp.example.com in the Authorization header (from a Windows Server 2025 client — for HOST/dc1.corp.example.com), and from then on every body is encrypted with the session key — Wireshark sees only the HTTP headers and the length. If the third packet carried NTLMSSP_NEGOTIATE, that would mean a fallback to NTLM: access by IP or a server outside the domain.

Logs. Get-WinEvent -ComputerName doesn't go through WinRM but over the event log service's RPC, and on a server with the default firewall rules it answers "The RPC server is unavailable." (the Remote Event Log Management rule group is disabled) — it's more convenient to read logs through Remoting itself. On the server, Microsoft-Windows-WinRM/Operational has event 91 — shell creation, with the user name and the client address; on the client, the log with the same name gets 142 ("WSMan operation CreateShell failed, error code …") and 161 with the same error text PowerShell showed when something fails. In Microsoft-Windows-PowerShell/Operational on the server, event 4104 with the script block logging policy enabled (Turn on PowerShell Script Block Logging) contains the text of every executed block; a session in the microsoft.powershell endpoint is Windows PowerShell, so it writes exactly here, while PowerShell 7 keeps its own PowerShellCore/Operational log with its own policy:

PS C:\> Invoke-Command dc1 { Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-WinRM/Operational'; Id=91} -MaxEvents 1 | Select-Object -ExpandProperty Message }
Creating WSMan shell on server with ResourceUri: http://schemas.microsoft.com/powershell/Microsoft.PowerShell (CORP\alice.admin clientIP: 192.168.1.100)
PS C:\> Invoke-Command dc1 { Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} -MaxEvents 1 | Select-Object -ExpandProperty Message }
Creating Scriptblock text (1 of 1):
hostname

In the Security log the same session is event 4624 with logon type 3 (network) and the Kerberos authentication package; with CredSSP — type 8 (NetworkCleartext): that's how CredSSP is recognized in auditing, and this type means the user's credentials stayed on the server.

And a connectivity check without a session:

PS C:\> Test-WSMan dc1 | Select-Object ProductVersion

ProductVersion
--------------
OS: 0.0.0 SP: 0.0 Stack: 3.0

PS C:\> Test-WSMan dc1 -Authentication Default | Select-Object ProductVersion

ProductVersion
--------------
OS: 10.0.26100 SP: 0.0 Stack: 3.0

Without -Authentication the request goes anonymously and checks only that the listener answers, so the OS version comes back as zeros; with it — that authentication succeeds. Stack 3.0 is the WinRM version, the same as in Windows 8 and Windows Server 2012.


Sources


Now we have a transport over which any command runs on any server of the domain. The next step is the administrator's workstation, where these commands are convenient to write: modules for roles it doesn't have, snap-ins that are still needed, and tools that need nothing but a browser.

Next — part 15: managing from a workstation. RSAT as features on demand, the DnsServer, DhcpServer and GroupPolicy modules and their -ComputerName and -CimSession, implicit remoting for modules that aren't available locally, MMC snap-ins and their transport, Windows Admin Center, and how to put together an administrator's workstation that doesn't require signing in to servers.

Only registered users can leave a comment.

Sign in or register to leave a comment.