WinRM 加固的目标很明确:把 5985 明文 HTTP 监听彻底关掉,只保留 5986 HTTPS,并将认证方式从 Basic/NTLM 收敛到证书或 Kerberos,再用 JEA 端点把管理员权限切成最小集合。本文给出一套可在 Windows Server 2016 以上直接执行的配置脚本,覆盖监听器重建、注册表加固、JEA 端点、日志审计与验证命令。
适用场景
- 运维团队使用 PowerShell Remoting、Ansible WinRM 或 SCVMM 批量管理 Windows 主机;
- 服务器暴露在公网或跨网段管理,曾被 5985 端口扫描与弱口令爆破命中;
- 等保或内审要求「远程管理使用加密通道 + 最小权限」,需要落地证据;
- 需要为第三方运维人员开放受限的远程执行能力,但不想发放管理员口令。
不适用场景:仅通过 RDP 管理、从未启用 WinRM 的主机。此类主机应保持 WinRM 服务禁用(Stop-Service WinRM; Set-Service WinRM -StartupType Disabled),而不是按本文配置后再暴露端口。
前置条件
- Windows Server 2016 及以上,或 Windows 10/11 企业版;
- 具备本地管理员权限,操作在带外控制台或已建立的 RDP 会话中执行,避免改配置时把自己锁在门外;
- 已准备好服务器证书:域环境推荐由企业内部 CA 签发,工作组环境可用自签证书并导入客户端信任存储;
- PowerShell 5.1 以上,
Get-Command New-PSSessionConfigurationFile可执行; - 如使用 Ansible,控制端需安装
pywinrm[kerberos]。
原理说明
WinRM 是 WS-Management 协议的 Windows 实现,默认监听两个端口:5985 走 HTTP,报文在网络上以可解析形式传输(NTLM 协商后凭据不直接明文,但会话内容与部分认证数据可被抓取重放);5986 走 HTTPS,由 TLS 提供机密性与完整性。风险点主要有四个:
- 明文监听未关闭:攻击者对内网 5985 做口令喷洒,成功即获得远程命令执行;
- Basic 认证开启:凭据以 Base64 形式传输,等同于明文;
- TrustedHosts 设为 *:客户端失去服务端身份校验,易被中间人劫持;
- 端点权限过宽:默认
Microsoft.PowerShell端点以完整管理员令牌运行,任何能连上的人即拥有整机控制权。
加固思路对应四层:关闭 5985、强制 HTTPS、收敛认证方式、用 JEA 限制可执行命令。JEA(Just Enough Administration)通过会话配置文件定义「谁能以什么身份运行哪些命令」,是这套方案里唯一能限制「拿到连接权限后能做什么」的机制。
操作步骤
步骤一:关闭 5985,重建 5986 HTTPS 监听器
# 查看当前监听器
winrm enumerate winrm/config/listener
# 删除所有 HTTP 监听器
Get-ChildItem WSMan:\localhost\Listener |
Where-Object { $_.Keys -contains "Transport=HTTP" } |
ForEach-Object { Remove-Item -Path ("WSMan:\localhost\Listener\" + $_.Name) -Recurse -Force }
# 申请/导入证书(示例:自签,生产建议用企业 CA)
$cert = New-SelfSignedCertificate -DnsName "win-srv-01.corp.local" `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyAlgorithm RSA -KeyLength 2048 -KeyExportPolicy NonExportable `
-NotAfter (Get-Date).AddYears(2)
$thumb = $cert.Thumbprint
# 创建 HTTPS 监听器
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS `
-Address * -CertificateThumbPrint $thumb -Force
# 防火墙仅放行 5986,且限制来源网段
New-NetFirewallRule -DisplayName "WinRM-HTTPS-AdminOnly" -Direction Inbound `
-Protocol TCP -LocalPort 5986 -Action Allow `
-RemoteAddress 10.10.5.0/24 -Profile Domain,Private
# 显式阻断 5985(防止组策略回滚后重新开放)
New-NetFirewallRule -DisplayName "WinRM-HTTP-Block" -Direction Inbound `
-Protocol TCP -LocalPort 5985 -Action Block
要点:-RemoteAddress 必须写具体管理网段,不要用 Any;-KeyExportPolicy NonExportable 防止私钥被导出复制;删除监听器后原 HTTP 端点立即失效,因此务必先确认 5986 可用再继续。
步骤二:注册表层收敛服务端认证策略
$base = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Service"
Set-ItemProperty $base -Name "AllowUnencrypted" -Value 0 -Type DWord
Set-ItemProperty $base -Name "Auth\Basic" -Value 0 -Type DWord
Set-ItemProperty $base -Name "Auth\CredSSP" -Value 0 -Type DWord
Set-ItemProperty $base -Name "MaxEnvelopeSizekb" -Value 1024 -Type DWord
Set-ItemProperty $base -Name "MaxTimeoutms" -Value 60000 -Type DWord
Set-ItemProperty $base -Name "MaxMemoryPerShellMB" -Value 1024 -Type DWord
# 会话与空闲限制,降低凭据被长期持有的风险
Set-Item WSMan:\localhost\Shell\MaxShellRunTime -Value 1800000
Set-Item WSMan:\localhost\Shell\IdleTimeout -Value 900000
Set-Item WSMan:\localhost\Service\RootSDDL `
-Value "O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;ER)S:P(AU;FA;GA;;;WD)(AU;SA;GXGW;;;WD)"
AllowUnencrypted=0 与 Auth\Basic=0 是硬性要求,缺一不可:只关 Basic 而保留 AllowUnencrypted,某些客户端仍会回退到明文 NTLM 通道;只设 AllowUnencrypted 而留 Basic,凭据仍可能明文传输。
步骤三:客户端侧清空 TrustedHosts,强制证书校验
管理端(运维机)执行:
# 清空历史遗留的通配设置
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "" -Force
Get-Item WSMan:\localhost\Client\TrustedHosts
# 工作组环境:把服务器证书公钥导入受信任根
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("C:\certs\win-srv-01.cer")
Import-Certificate -FilePath "C:\certs\win-srv-01.cer" `
-CertStoreLocation "Cert:\LocalMachine\Root"
# 测试连接(必须带 -UseSSL)
Test-WSMan -ComputerName win-srv-01.corp.local -UseSSL -Port 5986
若 TrustedHosts 保持为 *,客户端会跳过服务端证书链校验,攻击者用任意自签证书即可冒充服务器。清空后必须走 -UseSSL,否则连接直接失败——这正是期望行为。
步骤四:用 JEA 端点限制可执行命令
# 1) 创建受限角色能力文件
New-Item -Path "C:\ProgramData\JEA\OpsRole" -ItemType Directory -Force
@'
@{
GUID = "c3a1f6a2-2f4c-4f2e-9b0a-7d1e5c8b4a10"
Author = "SecurityTeam"
CompanyName = "Corp"
RoleCapabilities = @(
"Get-Service","Restart-Service","Get-Process",
"Get-EventLog","Get-WinEvent","Test-NetConnection"
)
}
'@ | Set-Content "C:\ProgramData\JEA\OpsRole\OpsRole.psrc" -Encoding UTF8
# 2) 编写会话配置文件
@'
@{
SchemaVersion = "1.0.0.0"
GUID = "d7b2e9c1-8a3f-4d6b-b5e2-1c9f7a4d3e21"
SessionType = "RestrictedRemoteServer"
RunAsVirtualAccount = $true
TranscriptDirectory = "C:\ProgramData\JEA\Transcripts"
RoleDefinitions = @{ "CORP\OpsTeam" = @{ RoleCapabilities = "OpsRole" } }
}
'@ | Set-Content "C:\ProgramData\JEA\OpsRole\OpsSession.pssc" -Encoding UTF8
# 3) 注册端点
Register-PSSessionConfiguration -Name "OpsEndpoint" `
-Path "C:\ProgramData\JEA\OpsRole\OpsSession.pssc" -Force
关键参数:RunAsVirtualAccount=$true 让远端以虚拟账户执行,调用方无需知道管理员口令;SessionType=RestrictedRemoteServer 只暴露 RoleCapabilities 中列出的命令,其余 cmdlet 与脚本执行全部不可用;TranscriptDirectory 记录完整会话文本,作为审计证据。
步骤五:开启 PowerShell 日志与脚本块审计
# 脚本块日志(事件 ID 4104),记录实际执行的代码片段
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Force
Set-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" `
-Name "EnableScriptBlockLogging" -Value 1 -Type DWord
# 模块日志(事件 ID 4103)
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -Force
Set-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" `
-Name "EnableModuleLogging" -Value 1 -Type DWord
# 转发 WinRM 操作日志到集中日志服务器(可选)
wevtutil sl "Microsoft-Windows-WinRM/Operational" /e:true
4104 事件是排查「谁在什么时候执行了什么」的唯一可靠来源,因为命令行参数会随事件重建。日志量较大,建议配合 wevtutil sl 调整日志上限或直接转发到 SIEM。
配置验证
# 1. 监听器是否只剩 HTTPS
winrm enumerate winrm/config/listener
# 2. 服务端认证策略
winrm get winrm/config/service
winrm get winrm/config/service/auth
# 3. 5985 是否已不可达(应从管理网段返回 filtered)
Test-NetConnection -ComputerName win-srv-01.corp.local -Port 5985
# 4. 明文连接应失败
winrm identify -r:http://win-srv-01.corp.local:5985 -auth:basic -u:test -p:test
# 5. JEA 端点权限裁剪是否生效
$s = New-PSSession -ComputerName win-srv-01.corp.local -ConfigurationName OpsEndpoint -UseSSL
Invoke-Command -Session $s { Get-Command | Measure-Object } # 命令数应显著少于默认端点
Invoke-Command -Session $s { Start-Process notepad } # 应报 CommandNotFoundException
Remove-PSSession $s
# 6. 审计日志是否有记录
Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" -MaxEvents 5 |
Where-Object Id -eq 4104 | Select-Object TimeCreated, Message
判定标准:监听器仅一条 Transport=HTTPS;AllowUnencrypted 与 Basic 均为 false;5985 连接超时或被拒绝;JEA 会话内 Start-Process 不可用;4104 事件可查到近期脚本内容。
常见问题
FAQ 1:关闭 5985 后 Ansible 批量管理全部失败,如何处理?
Ansible 的 winrm 连接插件默认走 5985。需在 inventory 中显式声明加密与端口:ansible_port=5986、ansible_winrm_transport=kerberos(或 certificate)、ansible_winrm_scheme=https,并确保控制端信任服务器证书。使用 certificate 传输时,需在服务器上导入客户端公钥并将该证书映射到本地账户,再在 inventory 中配置 ansible_winrm_cert_pem 与 ansible_winrm_cert_key_pem。Kerberos 方式在域环境更省事,但要求控制端已加入域或配置好 krb5.conf。
FAQ 2:工作组环境没有企业 CA,自签证书每次连接都报证书错误怎么办?
两种可行做法。一是把服务器证书的公钥(.cer)分发到所有管理端的「受信任的根证书颁发机构」,并在签发时把 -DnsName 写成客户端实际访问的主机名或 IP(IP 需放在 -TextExtension 的 SAN 中,否则校验仍失败)。二是搭建内部 AD CS 或使用小型 PKI 工具签发,长期看更易维护。切勿用「把 TrustedHosts 设为 *」来绕过报错,那等于放弃服务端身份校验。
FAQ 3:JEA 端点里能否运行自定义脚本?
可以,但不建议直接暴露 Invoke-Command 或 powershell.exe。正确做法是把脚本封装成函数模块,在 RoleCapabilities 中用 FunctionDefinitions 引用该模块,并在 VisibleFunctions 中只列出封装后的函数名。这样调用方传参进入函数,内部逻辑受你控制,无法借参数拼接执行任意命令。
总结
WinRM 加固可按四层推进:网络层只留 5986 并限制来源,协议层关掉 Basic 与明文,认证层用证书或 Kerberos 替代 NTLM,授权层用 JEA 把管理员权限切成可列举的命令集合。四层缺任何一层,前面做的收敛都可能被绕过。配置完成后务必在带外通道验证一次,确认自己仍能登录,再关闭测试账号。日常巡检重点看两处:监听器是否被组策略回滚出 5985,以及 4104 事件中是否出现未授权的脚本内容。