适用于 Windows Server 2016+ 及 PowerShell 5.1+
很多安全事件的根源并不是零日漏洞,而是基础配置疏忽——弱密码策略、高危端口暴露、补丁长期未更新。定期执行安全基线检查是运维最基本也是最有效的防线。本文用 PowerShell 编写一套轻量审计脚本,覆盖密码策略、防火墙、RDP、补丁和服务五大维度,最终汇总为一份 HTML 审计报告。
账户与密码策略
密码策略是安全基线的第一道门。如果系统允许空密码或密码永不过期,攻击者可以轻松暴力破解。同时管理员组成员过多、Guest 账户处于启用状态,也会显著增加横向移动的风险。下面这段代码通过 net accounts 获取当前密码策略,并检查本地管理员组和 Guest 账户的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| function Get-SecurityBaselineAccount { $result = @() $pwdPolicy = net accounts | Out-String
$result += [PSCustomObject]@{ Category = "密码策略"; Check = "密码最短期限" Value = if ($pwdPolicy -match '密码最短期限:\s*(\d+)') { $Matches[1] + " 天" } else { "未知" } Expected = "≥ 1 天" } $result += [PSCustomObject]@{ Category = "密码策略"; Check = "密码最长期限" Value = if ($pwdPolicy -match '密码最长期限:\s*(\d+)') { $Matches[1] + " 天" } else { "未知" } Expected = "≤ 90 天" } $result += [PSCustomObject]@{ Category = "密码策略"; Check = "最小密码长度" Value = if ($pwdPolicy -match '最小密码长度:\s*(\d+)') { $Matches[1] + " 字符" } else { "未知" } Expected = "≥ 14 字符" } $adminMembers = Get-LocalGroupMember -SID "S-1-5-32-544" -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "账户安全"; Check = "本地管理员组成员数" Value = "$($adminMembers.Count) 个"; Expected = "尽量精简" } $guest = Get-LocalUser -Name "Guest" -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "账户安全"; Check = "Guest 账户状态" Value = if ($guest.Enabled) { "已启用 (风险)" } else { "已禁用" }; Expected = "已禁用" } return $result }
|
执行后输出类似如下结果,可以看到最小密码长度仅为 7 字符,不满足基线要求,需要重点关注:
1 2 3 4 5 6 7
| -------- ----- ----- --------
|
防火墙与高危端口检测
防火墙是系统级的网络屏障,任何一个配置文件被关闭都会让对应网络场景下的所有端口暴露。FTP(21)、Telnet(23)、SMB(445)、RDP(3389)、VNC(5800/5900)等高危端口如果监听在 0.0.0.0 上,攻击面会大幅增加。下面这段代码检查三个防火墙配置文件的启用状态,并扫描当前对外开放的高危端口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function Get-SecurityBaselineNetwork { $result = @()
foreach ($profile in @("Domain", "Private", "Public")) { $fw = Get-NetFirewallProfile -Name $profile -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "防火墙"; Check = "$profile 配置文件" Value = if ($fw.Enabled) { "已启用" } else { "已禁用 (风险)" } Expected = "已启用" } } $listeningPorts = Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Where-Object { $_.LocalAddress -eq "0.0.0.0" -or $_.LocalAddress -eq "::" } | Select-Object LocalPort, OwningProcess -Unique
$highRiskPorts = @(21, 23, 445, 3389, 5800, 5900) $riskyPorts = $listeningPorts | Where-Object { $_.LocalPort -in $highRiskPorts }
$result += [PSCustomObject]@{ Category = "网络"; Check = "高危端口开放" Value = if ($riskyPorts) { "发现 $($riskyPorts.Count) 个: $($riskyPorts.LocalPort -join ', ')" } else { "无" } Expected = "无" } return $result }
|
下面的执行结果显示 Public 配置文件的防火墙已关闭,同时 445(SMB)和 3389(RDP)对外开放,存在较高的被利用风险:
1 2 3 4 5 6
| -------- ----- ----- --------
,
|
RDP 安全配置
RDP 是远程管理 Windows 服务器的常用方式,但如果不加限制就会成为暴力破解的入口。需要关注两个关键点:RDP 是否开启,以及是否启用了网络级别身份验证(NLA)。NLA 在建立连接之前就要求身份验证,能有效防御未认证的拒绝服务攻击和凭据窃取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function Get-SecurityBaselineRDP { $result = @()
$rdp = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server" ` -Name "fDenyTSConnections" -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "RDP 安全"; Check = "RDP 状态" Value = if ($rdp.fDenyTSConnections -eq 0) { "已启用" } else { "已禁用" } Expected = "按需启用" }
$nla = Get-ItemProperty -Path "HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" ` -Name "UserAuthentication" -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "RDP 安全"; Check = "NLA(网络级别身份验证)" Value = if ($nla.UserAuthentication -eq 1) { "已启用" } else { "未启用 (风险)" } Expected = "已启用" } return $result }
|
执行结果示例中 RDP 已启用且 NLA 也已启用,处于合规状态。如果 NLA 未启用,攻击者可在身份验证之前消耗服务器资源,建议立刻启用:
1 2 3 4
| -------- ----- ----- --------
|
补丁状态检查
未及时安装补丁是导致系统被入侵的常见原因。这段代码检查最近一次补丁安装的日期、Windows Update 服务状态,以及当前有多少重要更新待安装。补丁超过 30 天未更新或存在待安装的重要更新,都说明更新策略可能存在问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function Get-SecurityBaselineUpdates { $result = @() $recentUpdates = Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 5 $latestUpdate = $recentUpdates | Select-Object -First 1 $result += [PSCustomObject]@{ Category = "系统更新"; Check = "最新补丁日期" Value = if ($latestUpdate.InstalledOn) { $latestUpdate.InstalledOn.ToString("yyyy-MM-dd") } else { "未知" } Expected = "30 天内" } $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "系统更新"; Check = "Windows Update 服务" Value = "$($wuService.Status)"; Expected = "Running" } $updateSession = New-Object -ComObject Microsoft.Update.Session $searchResult = $updateSession.CreateUpdateSearcher().Search( "IsInstalled=0 and Type='Software' and AutoSelectOnWebSites=1" ) $result += [PSCustomObject]@{ Category = "系统更新"; Check = "待安装重要更新" Value = "$($searchResult.Updates.Count) 个"; Expected = "0 个" } return $result }
|
以下执行结果显示还有 3 个重要更新待安装,建议尽快安排补丁窗口完成更新:
1 2 3 4 5
| Category Check Value Expected
系统更新 最新补丁日期 2025-03-10 30 天内 系统更新 Windows Update 服务 Running Running 系统更新 待安装重要更新 3 个 0 个
|
危险服务检查
某些服务在大多数生产环境中并不需要,反而会增大攻击面。Telnet 明文传输凭据,远程注册表允许远程修改注册表,SSDP 和 UPnP 可能被用于网络发现和反射攻击。下面逐一检查这些服务的运行状态,并统计系统级自启动项数量——自启动项过多可能意味着恶意软件的持久化驻留。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function Get-SecurityBaselineServices { $result = @() $dangerousServices = @{ "Telnet" = "tlntsvr" "远程注册表" = "RemoteRegistry" "SSDP 发现" = "ssdpsrv" "UPnP 设备主机" = "upnphost" } foreach ($svc in $dangerousServices.GetEnumerator()) { $service = Get-Service -Name $svc.Value -ErrorAction SilentlyContinue $result += [PSCustomObject]@{ Category = "服务检查"; Check = "$($svc.Key) ($($svc.Value))" Value = if ($service) { "$($service.Status), 启动类型: $($service.StartType)" } else { "未安装" } Expected = "未安装或已禁用" } } $startupItems = Get-CimInstance -ClassName Win32_StartupCommand | Where-Object { $_.Location -eq "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" } $result += [PSCustomObject]@{ Category = "启动项"; Check = "系统级自启动项" Value = "$($startupItems.Count) 个"; Expected = "审查后保留必要项" } return $result }
|
执行结果显示远程注册表服务处于自动启动且正在运行,在生产环境中建议禁用:
1 2 3 4 5 6 7
| -------- ----- ----- --------
, , ,
|
生成 HTML 审计报告
将前面所有检查函数汇总为一份带风险标记的 HTML 报告。不合规的行以红色背景高亮显示,便于快速定位问题。报告顶部汇总检查总数和风险项数量,帮助管理层一眼掌握整体安全态势。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| function New-SecurityBaselineReport { param( [string]$OutputPath = ".\SecurityBaseline_$(Get-Date -Format 'yyyyMMdd').html" )
$allChecks = @(Get-SecurityBaselineAccount) + @(Get-SecurityBaselineNetwork) + @(Get-SecurityBaselineRDP) + @(Get-SecurityBaselineUpdates) + @(Get-SecurityBaselineServices)
$totalChecks = $allChecks.Count $riskItems = ($allChecks | Where-Object { $_.Value -match "风险|FAIL|未启用" }).Count
$html = @" <!DOCTYPE html><html><head><title>安全基线审计报告</title> <style> body{font-family:Microsoft YaHei;margin:20px} table{border-collapse:collapse;width:100%} th,td{border:1px solid #ddd;padding:8px;text-align:left} th{background-color:#4CAF50;color:white} .risk{background-color:#ffcccc} </style></head><body> <h1>安全基线审计报告 - $(Get-Date -Format 'yyyy-MM-dd')</h1> <p>检查项: $totalChecks | 风险项: <span style="color:red">$riskItems</span></p> <table><tr><th>类别</th><th>检查项</th><th>当前值</th><th>期望值</th></tr> "@ foreach ($item in $allChecks) { $class = if ($item.Value -match "风险|FAIL|未启用") { ' class="risk"' } else { "" } $html += "<tr$class><td>$($item.Category)</td><td>$($item.Check)</td>" + "<td>$($item.Value)</td><td>$($item.Expected)</td></tr>" } $html += "</table></body></html>" $html | Out-File -FilePath $OutputPath -Encoding UTF8 Write-Host "报告已生成: $OutputPath" }
|
执行后在当前目录生成 HTML 文件,浏览器打开即可看到完整的审计表格。不合规的行以红色背景标出:
1
| 报告已生成: .\SecurityBaseline_20250331.html
|
1 2 3 4 5 6 7 8
| 检查项: 19 | 风险项: 4
| 类别 | 检查项 | 当前值 | 期望值 | |----------|-----------------------------|-------------------------------|-----------------| | 密码策略 | 最小密码长度 | 7 字符 | ≥ 14 字符 | | 防火墙 | Public 配置文件 | 已禁用 (风险) | 已启用 | | 网络 | 高危端口开放 | 发现 2 个: 445, 3389 | 无 | | 服务检查 | 远程注册表 (RemoteRegistry) | Running, 启动类型: Automatic | 未安装或已禁用 |
|
建议在运维流程中定期执行 New-SecurityBaselineReport,并将历次报告归档对比,跟踪风险项的变化趋势。安全基线不是一次性的工作,而是持续改进的过程。