Gorstaks Patcher
unknown
powershell
2 months ago
22 kB
8
No Index
<#
.SYNOPSIS
Fetches CISA Known Exploited Vulnerabilities (KEV), detects new CVEs, and applies
scriptable mitigations where configured—without waiting for Windows Update.
.DESCRIPTION
- Pulls CISA KEV catalog (JSON).
- Tracks which CVEs were already seen (state file) so only NEW entries trigger actions.
- For each new CVE, if a scriptable mitigation exists in CVE-MitigationActions.json,
runs it (registry, services, firewall, etc.). Otherwise reports with advisory links.
- Default is -WhatIf (no changes). Use -Apply to actually apply mitigations.
- Use -RegisterSchedule to install to C:\ProgramData\CVE-MitigationPatcher and run hourly as SYSTEM (fire-and-forget). Log: same folder, CVE-MitigationPatcher.log.
.NOTES
Requires PowerShell 5.1+. Run as Administrator to apply service/registry mitigations.
Mitigations are workarounds; install Windows Updates when available.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[switch] $Apply, # Actually apply mitigations (default: WhatIf)
[switch] $Reapply, # Process all relevant CVEs (incl. previously seen) - use after adding config mappings
[switch] $ReportOnly, # Only list new CVEs and links, do not run any mitigation
[switch] $RegisterSchedule, # Create a scheduled task to run every hour
[switch] $UnregisterSchedule, # Remove the scheduled task
[switch] $ScheduleApply, # With -RegisterSchedule: task runs with -Apply (else -ReportOnly)
[string] $FilterVendor, # e.g. "Microsoft" - only process these CVEs
[string] $FilterProduct, # e.g. "Windows" - product name contains this
[string] $StatePath, # Where to store "last seen" CVE IDs (default: script dir)
[string] $ConfigPath, # Path to CVE-MitigationActions.json
[string] $LogPath # Optional log file
)
$ErrorActionPreference = "Stop"
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$InstallDir = "C:\ProgramData\CVE-MitigationPatcher"
$TaskName = "CVE-MitigationPatcher"
$StatePath = if ($StatePath) { $StatePath } else { Join-Path $ScriptDir "CVE-PatcherState.json" }
$ConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $ScriptDir "CVE-MitigationActions.json" }
$LogPath = if ($LogPath) { $LogPath } else { Join-Path $ScriptDir "CVE-MitigationPatcher.log" }
# --------------- Embedded config (used when no external JSON found - single-file distribution) ---------------
$EmbeddedConfigJson = @'
{"templates":{"Set-Registry-DWord":{"type":"registry","path":"{{path}}","name":"{{name}}","value":"{{value}}","valueType":"DWord"},"Delete-RegistryKey":{"type":"deleteRegistryKey","path":"{{path}}"},"Disable-Service":{"type":"service","name":"{{name}}","startupType":"Disabled"},"Block-Firewall-Port":{"type":"firewall","displayName":"{{displayName}}","port":"{{port}}","protocol":"TCP","direction":"Inbound","action":"Block"}},"defaultActionsForVendorProduct":[{"vendor":"Microsoft","productPattern":"Windows","description":"Generic Windows hardening","run":["Disable-SMBv1","Block-MSDTProtocol","Disable-WebClient"]}],"actions":{"CVE-2017-0143":{"description":"SMBv1 RCE","run":["Disable-SMBv1"]},"CVE-2017-0144":{"description":"EternalBlue","run":["Disable-SMBv1"]},"CVE-2017-0145":{"description":"EternalRomance","run":["Disable-SMBv1"]},"CVE-2017-0146":{"description":"SMBv1 RCE","run":["Disable-SMBv1"]},"CVE-2017-0147":{"description":"SMBv1 info disclosure","run":["Disable-SMBv1"]},"CVE-2017-0148":{"description":"SMBv1 server RCE","run":["Disable-SMBv1"]},"CVE-2020-0796":{"description":"SMBGhost","run":["Disable-SMBv3Compression"]},"CVE-2019-0708":{"description":"BlueKeep","run":["Enable-RDPNLA"]},"CVE-2020-1350":{"description":"SigRed","run":["Mitigate-SigRed"]},"CVE-2022-30190":{"description":"Follina","run":["Block-MSDTProtocol"]},"CVE-2022-34713":{"description":"MSDT RCE","run":["Block-MSDTProtocol"]},"CVE-2021-34527":{"description":"PrintNightmare","run":["Disable-PrintSpooler"]},"CVE-2021-1675":{"description":"PrintNightmare LPE","run":["Disable-PrintSpooler"]},"CVE-2024-38063":{"description":"IPv6 RCE","run":["Disable-IPv6"]},"CVE-2022-26923":{"description":"PetitPotam","run":["Disable-WebClient"]},"CVE-2022-30136":{"description":"NFSv4 RCE","run":["Disable-NFS"]},"CVE-2022-26937":{"description":"NFS RCE","run":["Disable-NFS"]}}}
'@
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [$Level] $Message"
Add-Content -Path $LogPath -Value $line -ErrorAction SilentlyContinue
if ($Level -eq "ERROR") { Write-Error $Message } else { Write-Host $Message }
}
# --------------- Install self to ProgramData (for fire-and-forget) ---------------
function Install-Self {
if (-not (Test-Path $InstallDir)) { New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null }
$scriptName = Split-Path -Leaf $MyInvocation.MyCommand.Path
$destScript = Join-Path $InstallDir $scriptName
$srcScript = $MyInvocation.MyCommand.Path
Copy-Item -Path $srcScript -Destination $destScript -Force
$configName = "CVE-MitigationActions.json"
$srcConfig = Join-Path (Split-Path -Parent $srcScript) $configName
if (Test-Path $srcConfig) { Copy-Item -Path $srcConfig -Destination (Join-Path $InstallDir $configName) -Force }
$mitDir = Join-Path (Split-Path -Parent $srcScript) "Mitigations"
if (Test-Path $mitDir) {
$destMit = Join-Path $InstallDir "Mitigations"
if (-not (Test-Path $destMit)) { New-Item -ItemType Directory -Path $destMit -Force | Out-Null }
Copy-Item -Path "$mitDir\*" -Destination $destMit -Recurse -Force -ErrorAction SilentlyContinue
}
return $destScript
}
# --------------- Task scheduling ---------------
if ($UnregisterSchedule) {
$t = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($t) {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Host "Scheduled task '$TaskName' removed. To remove files: delete $InstallDir"
} else {
Write-Host "Scheduled task '$TaskName' not found."
}
exit 0
}
if ($RegisterSchedule) {
$installedScript = Install-Self
$workDir = $InstallDir
$argList = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$installedScript`""
if ($ScheduleApply) { $argList += " -Apply" } else { $argList += " -ReportOnly" }
if ($FilterVendor) { $argList += " -FilterVendor `"$FilterVendor`"" }
if ($FilterProduct) { $argList += " -FilterProduct `"$FilterProduct`"" }
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $argList -WorkingDirectory $workDir
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Hours 1) -RepetitionDuration ([TimeSpan]::MaxValue)
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
$principal = if ($ScheduleApply) {
New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
} else {
New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount
}
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
$params = @{ TaskName = $TaskName; Action = $action; Trigger = $trigger; Settings = $settings; Principal = $principal }
if ($task) { Set-ScheduledTask @params } else { Register-ScheduledTask @params }
Write-Host "Fire-and-forget: task '$TaskName' runs every hour as SYSTEM. Log: $InstallDir\CVE-MitigationPatcher.log"
exit 0
}
$KevUrl = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
# --------------- Built-in mitigation actions (scriptable) ---------------
$MitigationActions = @{
"Disable-SMBv1" = {
# SMBv1 - EternalBlue, EternalChampion, etc. (CVE-2017-0143, 2017-0144, 2017-0145, 2017-0146)
$path = "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters"
if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
Set-ItemProperty -Path $path -Name "SMB1" -Value 0 -Type DWord -Force
$fs = Get-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -ErrorAction SilentlyContinue
if ($fs -and $fs.State -eq "Enabled") {
Disable-WindowsOptionalFeature -Online -FeatureName SMB1Protocol -NoRestart -ErrorAction SilentlyContinue
}
}
"Disable-SMBv3Compression" = {
# CVE-2020-0796 SMBGhost - disables SMBv3 compression on server
$path = "HKLM:\SYSTEM\CurrentControlSet\Services\LanmanServer\Parameters"
if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
Set-ItemProperty -Path $path -Name "DisableCompression" -Value 1 -Type DWord -Force
}
"Disable-PrintSpooler" = {
# PrintNightmare and similar (CVE-2021-34527, CVE-2021-1675)
Stop-Service -Name Spooler -Force -ErrorAction SilentlyContinue
Set-Service -Name Spooler -StartupType Disabled
}
"Block-MSDTProtocol" = {
# CVE-2022-30190 Follina, CVE-2022-34713 - removes ms-msdt URL protocol handler
$path = "HKCR:\ms-msdt"
if (Test-Path $path) {
Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue
}
}
"Mitigate-SigRed" = {
# CVE-2020-1350 SigRed - DNS server only; restricts TCP response size
$svc = Get-Service -Name DNS -ErrorAction SilentlyContinue
if (-not $svc) { return }
$path = "HKLM:\SYSTEM\CurrentControlSet\Services\DNS\Parameters"
if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
Set-ItemProperty -Path $path -Name "TcpReceivePacketSize" -Value 0xFF00 -Type DWord -Force
Restart-Service -Name DNS -Force -ErrorAction SilentlyContinue
}
"Enable-RDPNLA" = {
# CVE-2019-0708 BlueKeep - requires NLA before RDP session
$path = "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp"
if (Test-Path $path) {
Set-ItemProperty -Path $path -Name "UserAuthentication" -Value 1 -Type DWord -Force
}
}
"Disable-WebClient" = {
# NTLM relay, WebDAV abuse vectors
Stop-Service -Name WebClient -Force -ErrorAction SilentlyContinue
Set-Service -Name WebClient -StartupType Disabled
}
"Disable-IPv6" = {
# CVE-2024-38063 workaround: disable IPv6 via registry
$path = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters"
if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
Set-ItemProperty -Path $path -Name "DisabledComponents" -Value 0xFF -Type DWord -Force
}
"Disable-LLMNR" = {
# NTLM relay / LLMNR poisoning: disable multicast name resolution
$path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient"
if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
Set-ItemProperty -Path $path -Name "EnableMulticast" -Value 0 -Type DWord -Force
}
"Disable-NBT-NS" = {
# NTLM relay / NetBIOS poisoning: disable NetBIOS over TCP/IP on all interfaces
$basePath = "HKLM:\SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces"
if (Test-Path $basePath) {
Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue | ForEach-Object {
Set-ItemProperty -Path $_.PSPath -Name "NetbiosOptions" -Value 2 -Type DWord -Force -ErrorAction SilentlyContinue
}
}
}
"Disable-RemoteRegistry" = {
# Reduces lateral movement; attackers often use Remote Registry for recon
Stop-Service -Name RemoteRegistry -Force -ErrorAction SilentlyContinue
Set-Service -Name RemoteRegistry -StartupType Disabled -ErrorAction SilentlyContinue
}
"Disable-NFS" = {
# CVE-2022-30136: Windows NFSv4 RCE - disable NFS services (Server/Client/Redirector)
Get-Service -Name *Nfs* -ErrorAction SilentlyContinue | ForEach-Object {
Stop-Service -Name $_.Name -Force -ErrorAction SilentlyContinue
Set-Service -Name $_.Name -StartupType Disabled -ErrorAction SilentlyContinue
}
}
}
# --------------- Fetch CISA KEV and parse ---------------
function Get-CisaKevCatalog {
Write-Log "Fetching CISA KEV catalog..."
try {
$r = Invoke-RestMethod -Uri $KevUrl -Method Get -UseBasicParsing
return $r
} catch {
Write-Log "Failed to fetch KEV: $_" "ERROR"
throw
}
}
function Get-RelevantCves {
param($Catalog, [string]$Vendor, [string]$Product)
$list = @($Catalog.vulnerabilities)
if ($Vendor) {
$list = $list | Where-Object { $_.vendorProject -eq $Vendor }
}
if ($Product) {
$list = $list | Where-Object { $_.product -like "*$Product*" }
}
return $list
}
# --------------- State: last seen CVE IDs ---------------
function Get-SeenCveIds {
if (-not (Test-Path $StatePath)) { return @{} }
try {
$o = Get-Content $StatePath -Raw | ConvertFrom-Json
$h = @{}
$o.seenCveIds.PSObject.Properties | ForEach-Object { $h[$_.Name] = $true }
return $h
} catch {
return @{}
}
}
function Save-SeenCveIds {
param([string[]]$CveIds)
$seen = Get-SeenCveIds
foreach ($id in $CveIds) { $seen[$id] = $true }
@{ seenCveIds = $seen; lastUpdate = (Get-Date -Format "o") } | ConvertTo-Json -Depth 3 | Set-Content $StatePath -Encoding UTF8
}
# --------------- Load config: CVE -> actions, templates (file or embedded) ---------------
$script:ConfigDir = $ScriptDir # fallback for embedded config / relative script paths
$script:Templates = @{}
function Get-MitigationConfig {
$cfg = $null
if (Test-Path $ConfigPath) {
try {
$cfg = Get-Content $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
$script:ConfigDir = Split-Path -Parent $ConfigPath
} catch {
Write-Log "Error loading config file: $_" "ERROR"
}
}
if (-not $cfg) {
try {
$cfg = $EmbeddedConfigJson | ConvertFrom-Json
Write-Log "Using embedded config (no file at $ConfigPath)"
$script:ConfigDir = $ScriptDir
} catch {
Write-Log "Error loading embedded config: $_" "ERROR"
return @{ actions = @{}; defaultActionsForVendorProduct = @() }
}
}
$out = @{}
if ($cfg.actions) {
$cfg.actions.PSObject.Properties | ForEach-Object {
$out[$_.Name] = $_.Value
}
}
if ($cfg.templates) {
$script:Templates = @{}
$cfg.templates.PSObject.Properties | ForEach-Object {
$script:Templates[$_.Name] = $_.Value
}
}
$defaults = @()
if ($cfg.defaultActionsForVendorProduct) {
$defaults = @($cfg.defaultActionsForVendorProduct)
}
return @{ actions = $out; defaultActionsForVendorProduct = $defaults }
}
# --------------- Expand template with params ({{key}} -> params.key) ---------------
function Expand-Template {
param($Template, [hashtable]$Params)
if (-not $Template) { return $null }
$json = ($Template | ConvertTo-Json -Depth 10 -Compress)
foreach ($k in $Params.Keys) {
$val = [string]$Params[$k]
$val = $val -replace '\\','\\\\' -replace '"','\"'
$json = $json -replace [regex]::Escape("{{$k}}"), $val
}
try { return $json | ConvertFrom-Json } catch { return $null }
}
# --------------- Execute generic action (registry, service, firewall, deleteRegistryKey, script) ---------------
function Invoke-GenericAction {
param($Action, [string]$CveId)
$type = if ($Action.PSObject.Properties['type']) { $Action.type } else { $null }
if (-not $type) {
Write-Log "Generic action missing 'type' (CVE: $CveId)" "ERROR"
return $false
}
$desc = "$type for $CveId"
if ($ReportOnly) { return $false }
if (-not $Apply) {
Write-Log "[WhatIf] Would apply: $desc"
return $true
}
try {
switch ($type) {
"registry" {
$path = $Action.path
$name = $Action.name
$val = if ($null -ne $Action.value) { $Action.value } else { $Action.Value }
$vtype = if ($Action.valueType) { $Action.valueType } else { "DWord" }
if ($vtype -eq "DWord" -or $vtype -eq "QWord") { $val = [int]$val }
if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null }
Set-ItemProperty -Path $path -Name $name -Value $val -Type $vtype -Force
}
"deleteRegistryKey" {
$path = $Action.path
if (Test-Path $path) { Remove-Item -Path $path -Recurse -Force -ErrorAction SilentlyContinue }
}
"service" {
$svcName = $Action.name
$startup = if ($Action.startupType) { $Action.startupType } else { "Disabled" }
Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue
Set-Service -Name $svcName -StartupType $startup
}
"firewall" {
$dn = $Action.displayName
$port = [int]$Action.port
$proto = if ($Action.protocol) { $Action.protocol } else { "TCP" }
$dir = if ($Action.direction) { $Action.direction } else { "Inbound" }
$act = if ($Action.action) { $Action.action } else { "Block" }
$rule = Get-NetFirewallRule -DisplayName $dn -ErrorAction SilentlyContinue
if (-not $rule) {
New-NetFirewallRule -DisplayName $dn -Direction $dir -Protocol $proto -LocalPort $port -Action $act -ErrorAction Stop
}
}
"script" {
$relPath = $Action.path
$fullPath = if ([System.IO.Path]::IsPathRooted($relPath)) { $relPath } else { Join-Path $script:ConfigDir $relPath }
$args = if ($Action.arguments) { @($Action.arguments) } else { @() }
if (-not (Test-Path $fullPath)) { Write-Log "Script not found: $fullPath" "ERROR"; return $false }
& $fullPath @args
}
default { Write-Log "Unknown generic action type: $type" "ERROR"; return $false }
}
Write-Log "Applied mitigation: $desc"
return $true
} catch {
Write-Log "Failed to apply $desc : $_" "ERROR"
return $false
}
}
# --------------- Resolve run entry: string (named action) or object (generic/template/script) ---------------
function Invoke-RunEntry {
param($Entry, [string]$CveId)
if ($Entry -is [string]) {
if (-not $MitigationActions[$Entry]) {
Write-Log "Unknown named action: $Entry (CVE: $CveId)" "ERROR"
return $false
}
if ($ReportOnly) { return $false }
if ($Apply) {
try {
& $MitigationActions[$Entry]
Write-Log "Applied mitigation: $Entry for $CveId"
return $true
} catch {
Write-Log "Failed to apply $Entry for $CveId : $_" "ERROR"
return $false
}
} else {
Write-Log "[WhatIf] Would apply: $Entry for $CveId"
return $true
}
}
if ($Entry.PSObject.Properties['template']) {
$tplName = $Entry.template
$params = @{}
if ($Entry.params) {
$Entry.params.PSObject.Properties | ForEach-Object { $params[$_.Name] = $_.Value }
}
$tpl = $script:Templates[$tplName]
if (-not $tpl) { Write-Log "Template not found: $tplName (CVE: $CveId)" "ERROR"; return $false }
$expanded = Expand-Template -Template $tpl -Params $params
if ($expanded) { return Invoke-GenericAction -Action $expanded -CveId $CveId }
return $false
}
if ($Entry.PSObject.Properties['type']) {
return Invoke-GenericAction -Action $Entry -CveId $CveId
}
Write-Log "Invalid run entry (CVE: $CveId)" "ERROR"
return $false
}
# --------------- Apply one mitigation by name (legacy) ---------------
function Invoke-MitigationAction {
param([string]$ActionName, [string]$CveId)
return Invoke-RunEntry -Entry $ActionName -CveId $CveId
}
# --------------- Main ---------------
try {
$mode = if ($ReportOnly) { "ReportOnly" } elseif ($Apply) { "Apply" } else { "WhatIf" }
$filter = if ($FilterVendor) { "Vendor=$FilterVendor" } elseif ($FilterProduct) { "Product=$FilterProduct" } else { "All" }
Write-Log "Run started. Mode=$mode Filter=$filter"
$catalog = Get-CisaKevCatalog
$cves = Get-RelevantCves -Catalog $catalog -Vendor $FilterVendor -Product $FilterProduct
if (-not $FilterVendor -and -not $FilterProduct) {
$cves = @($catalog.vulnerabilities)
}
Write-Log "Relevant CVEs in KEV: $($cves.Count)"
$seen = Get-SeenCveIds
$config = Get-MitigationConfig
$newCves = if ($Reapply) { @($cves) } else { @($cves | Where-Object { -not $seen[$_.cveID] }) }
$newIds = @($newCves | ForEach-Object { $_.cveID })
if ($newIds.Count -eq 0) {
Write-Log "No CVEs to process. (Use -Reapply to process previously seen CVEs with updated config.)"
exit 0
}
if ($Reapply) {
Write-Log "Reapply mode: processing all $($newIds.Count) relevant CVEs (including previously seen)"
} else {
Write-Log "New CVEs (since last run): $($newIds.Count)"
}
foreach ($cve in $newCves) {
$id = $cve.cveID
$vendor = $cve.vendorProject
$product = $cve.product
$name = $cve.vulnerabilityName
$notes = $cve.notes
$link = ($notes -split ' ' | Where-Object { $_ -match '^https?://' } | Select-Object -First 1) -replace '[,;]$',''
Write-Log " $id | $vendor | $product | $name"
Write-Log " Link: $link"
$actionConfig = $config.actions[$id]
$runList = $null
if ($actionConfig -and $actionConfig.run) {
$runList = @($actionConfig.run)
} elseif ($config.defaultActionsForVendorProduct -and $config.defaultActionsForVendorProduct.Count -gt 0) {
# Vendor/product-based defaults: apply when no explicit CVE config
$matched = $config.defaultActionsForVendorProduct | Where-Object {
$v = $_.vendor; $p = $_.productPattern
(-not $v -or $vendor -eq $v) -and (-not $p -or $product -like "*$p*")
} | Select-Object -First 1
if ($matched -and $matched.run) {
$runList = @($matched.run)
Write-Log " Using vendor/product defaults for $vendor | $product"
}
}
if ($runList) {
foreach ($entry in $runList) {
$null = Invoke-RunEntry -Entry $entry -CveId $id
}
} else {
Write-Log " No scriptable mitigation in config. Apply workarounds from the link above."
}
}
if (-not $Reapply) { Save-SeenCveIds -CveIds $newIds }
Write-Log "State updated. Done."
} catch {
Write-Log "Script failed: $_" "ERROR"
exit 1
}
Editor is loading...
Leave a Comment