350 lines
13 KiB
PowerShell
350 lines
13 KiB
PowerShell
function Invoke-ServiceImagePathAudit {
|
|
<#
|
|
.SYNOPSIS
|
|
Scans, analyzes, and optionally repairs Windows service ImagePath values for unquoted paths with spaces.
|
|
|
|
.DESCRIPTION
|
|
Single entry-point for the classic workflow:
|
|
- Scan: Retrieve service ImagePath values from HKLM:\SYSTEM\CurrentControlSet\Services
|
|
- Analyze: Identify unquoted service paths with spaces and generate a FixedKey suggestion
|
|
- Repair: Apply FixedKey back to the registry for items marked BadKey="Yes"
|
|
- ScanAnalyze: Scan then Analyze (default)
|
|
- ScanFix: Scan then Analyze then Repair
|
|
|
|
Supports -WhatIf and -Confirm for repairs.
|
|
|
|
.PARAMETER Operation
|
|
What to do:
|
|
- Scan Output ComputerName/Key/ImagePath records
|
|
- Analyze Add BadKey/FixedKey to incoming records
|
|
- Repair Write FixedKey back for incoming records where BadKey="Yes"
|
|
- ScanAnalyze Scan then Analyze (default)
|
|
- ScanFix Scan then Analyze then Repair
|
|
|
|
.PARAMETER ComputerName
|
|
One or more computer names to scan (used by Scan/ScanAnalyze/ScanFix).
|
|
Defaults to the local computer.
|
|
|
|
.PARAMETER InputObject
|
|
Pipeline input (used by Analyze/Repair). Expected properties:
|
|
- Analyze: ComputerName, Key, ImagePath
|
|
- Repair: ComputerName, Key, BadKey, FixedKey
|
|
|
|
.PARAMETER ShowProgress
|
|
Show progress bars during scanning/analyzing/repair. Default is enabled if you do not specify it.
|
|
|
|
.EXAMPLE
|
|
Invoke-ServiceImagePathAudit
|
|
Scans and analyzes the local computer (default Operation is ScanAnalyze).
|
|
|
|
.EXAMPLE
|
|
Invoke-ServiceImagePathAudit -Operation Scan -ComputerName Server1,Server2
|
|
Scans Server1 and Server2 and returns raw ImagePath records.
|
|
|
|
.EXAMPLE
|
|
Invoke-ServiceImagePathAudit -ComputerName Server1 |
|
|
Where-Object BadKey -eq 'Yes'
|
|
Scans and analyzes Server1 and filters to vulnerable entries.
|
|
|
|
.EXAMPLE
|
|
Invoke-ServiceImagePathAudit -Operation ScanFix -ComputerName Server1 -WhatIf
|
|
Shows what would be repaired on Server1 without making changes.
|
|
|
|
.EXAMPLE
|
|
Import-Csv .\scan.csv | Invoke-ServiceImagePathAudit -Operation Analyze
|
|
Analyzes previously exported scan results.
|
|
|
|
.EXAMPLE
|
|
Import-Csv .\scan.csv | Invoke-ServiceImagePathAudit -Operation Repair -WhatIf
|
|
Dry-run repairs from previously analyzed records.
|
|
|
|
.NOTES
|
|
Save as UTF-8 (no BOM).
|
|
Remote scan/repair requires Remote Registry access and permissions to read/write HKLM on targets.
|
|
#>
|
|
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'ScanSet')]
|
|
param(
|
|
[Parameter(Position = 0)]
|
|
[ValidateSet('Scan','Analyze','Repair','ScanAnalyze','ScanFix')]
|
|
[string]$Operation = 'ScanAnalyze',
|
|
|
|
[Parameter(ParameterSetName = 'ScanSet')]
|
|
[Alias('Name','Computer','Server','__ServerName')]
|
|
[string[]]$ComputerName = $env:COMPUTERNAME,
|
|
|
|
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'PipelineSet')]
|
|
$InputObject,
|
|
|
|
[switch]$ShowProgress
|
|
)
|
|
|
|
begin {
|
|
# Default ShowProgress to ON if user didn't specify it
|
|
if (-not $PSBoundParameters.ContainsKey('ShowProgress')) {
|
|
$ShowProgress = $true
|
|
}
|
|
|
|
function _Scan {
|
|
param([string[]]$Targets)
|
|
|
|
foreach ($computer in $Targets) {
|
|
$collection = @()
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Id 1 -Activity "Scanning services on $computer" -Status "Connecting..."
|
|
Write-Progress -Id 2 -Activity "Parsing results" -Status "Waiting..." -PercentComplete 0
|
|
}
|
|
|
|
$result = & reg.exe QUERY "\\$computer\HKLM\SYSTEM\CurrentControlSet\Services" /v ImagePath /s 2>&1
|
|
|
|
if (-not $result -or $result[0] -match 'ERROR|Denied') {
|
|
if ($ShowProgress) {
|
|
Write-Progress -Id 1 -Activity "Scanning services on $computer" -Status "Connection Failed"
|
|
Write-Progress -Id 2 -Activity "Parsing results" -Completed
|
|
Write-Progress -Id 1 -Activity "Scanning services on $computer" -Completed
|
|
}
|
|
|
|
$collection += [pscustomobject]@{
|
|
ComputerName = $computer
|
|
Status = "REG Failed"
|
|
Key = "Unavailable"
|
|
ImagePath = "Unavailable"
|
|
}
|
|
|
|
$collection
|
|
continue
|
|
}
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Id 1 -Activity "Scanning services on $computer" -Status "Connected"
|
|
}
|
|
|
|
$lines = $result | Where-Object { $_ -and $_.Trim() -ne '' }
|
|
|
|
$currentKey = $null
|
|
$i = 0
|
|
foreach ($line in $lines) {
|
|
$i++
|
|
if ($ShowProgress) {
|
|
$pct = [math]::Min([math]::Round(($i / [math]::Max($lines.Count, 1)) * 100), 100)
|
|
Write-Progress -Id 2 -Activity "Parsing results" -Status "Reading $computer" -PercentComplete $pct
|
|
}
|
|
|
|
if ($line -match '^HKEY_') {
|
|
$currentKey = $line.Trim()
|
|
continue
|
|
}
|
|
|
|
if ($line -match '^\s*ImagePath\s+REG_\w+\s+(?<val>.+)$') {
|
|
$collection += [pscustomobject]@{
|
|
ComputerName = $computer
|
|
Status = "Retrieved"
|
|
Key = $currentKey
|
|
ImagePath = $Matches['val'].Trim()
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Id 2 -Activity "Parsing results" -Completed
|
|
Write-Progress -Id 1 -Activity "Scanning services on $computer" -Completed
|
|
}
|
|
|
|
$collection
|
|
}
|
|
}
|
|
|
|
function _AnalyzeOne {
|
|
param($Obj)
|
|
|
|
$outObj = $Obj | Select-Object *
|
|
$img = [string]$outObj.ImagePath
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Activity "Analyzing ImagePath" -Status "Checking $($outObj.ComputerName)\$($outObj.Key)"
|
|
}
|
|
|
|
# Default outputs
|
|
$badKey = "No"
|
|
$fixed = "N/A"
|
|
|
|
# Can't analyze
|
|
if ($outObj.Key -eq "Unavailable" -or $img -eq "Unavailable" -or [string]::IsNullOrWhiteSpace($img)) {
|
|
$outObj | Add-Member NoteProperty BadKey "Unknown" -Force
|
|
$outObj | Add-Member NoteProperty FixedKey "Can't Fix" -Force
|
|
return $outObj
|
|
}
|
|
|
|
$trim = $img.Trim()
|
|
|
|
# Already quoted or special prefix we don't touch
|
|
if ($trim.StartsWith('"') -or $trim.StartsWith('\??')) {
|
|
$outObj | Add-Member NoteProperty BadKey $badKey -Force
|
|
$outObj | Add-Member NoteProperty FixedKey $fixed -Force
|
|
return $outObj
|
|
}
|
|
|
|
# If no spaces, not vulnerable in the classic sense
|
|
if ($trim -notmatch '\s') {
|
|
$outObj | Add-Member NoteProperty BadKey $badKey -Force
|
|
$outObj | Add-Member NoteProperty FixedKey $fixed -Force
|
|
return $outObj
|
|
}
|
|
|
|
# Quote only the executable portion (best practice for service ImagePath)
|
|
# Matches:
|
|
# C:\Path With Spaces\app.exe <args>
|
|
# \\server\share\Path With Spaces\app.exe <args>
|
|
# Also tolerates env-var rooted paths like:
|
|
# %ProgramFiles%\App\app.exe <args>
|
|
$exeRegex = '^(?<exe>(?:(?:[A-Za-z]:\\)|(?:\\\\[^\\]+\\[^\\]+\\)|(?:%[^%]+%\\))[^"]*?\.(?:exe|com|bat|cmd))(?<args>\s+.*)?$'
|
|
|
|
if ($trim -match $exeRegex) {
|
|
$exe = $Matches['exe']
|
|
$args = $Matches['args']
|
|
|
|
# Only "bad" if the exe path contains whitespace and is unquoted (it is)
|
|
if ($exe -match '\s') {
|
|
$badKey = "Yes"
|
|
$fixed = '"' + $exe + '"' + ($(if ($null -ne $args) { $args } else { '' }))
|
|
#PoSH 7 $fixed = '"' + $exe + '"' + ($args ?? '')
|
|
} else {
|
|
$badKey = "No"
|
|
$fixed = "N/A"
|
|
}
|
|
}
|
|
else {
|
|
# Fallback: if we can't confidently isolate an exe, quote the whole string
|
|
# (better than producing broken quotes)
|
|
$badKey = "Yes"
|
|
$fixed = '"' + $exe + '"' + ($(if ($null -ne $args) { $args } else { '' }))
|
|
#PoSH 7 -> $fixed = '"' + $trim.Trim('"') + '"'
|
|
}
|
|
|
|
$outObj | Add-Member NoteProperty BadKey $badKey -Force
|
|
$outObj | Add-Member NoteProperty FixedKey $fixed -Force
|
|
return $outObj
|
|
}
|
|
|
|
|
|
function _RepairOne {
|
|
param($Obj)
|
|
|
|
$outObj = $Obj | Select-Object *
|
|
|
|
if ($outObj.BadKey -ne 'Yes') {
|
|
return $outObj
|
|
}
|
|
|
|
$computer = [string]$outObj.ComputerName
|
|
$fullKey = [string]$outObj.Key
|
|
$data = [string]$outObj.FixedKey
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Activity "Repairing ImagePath" -Status "Fixing $computer"
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($data) -or $data -eq 'N/A' -or $data -eq "Can't Fix") {
|
|
$outObj.Status = "Skipped (no FixedKey)"
|
|
return $outObj
|
|
}
|
|
|
|
# Determine hive + subkey from the Key string we scanned
|
|
$hive = $null
|
|
$subKeyPath = $null
|
|
|
|
if ($fullKey -match '^(HKEY_LOCAL_MACHINE|HKLM)\\(?<sub>.+)$') {
|
|
$hive = [Microsoft.Win32.RegistryHive]::LocalMachine
|
|
$subKeyPath = $Matches['sub']
|
|
}
|
|
elseif ($fullKey -match '^(HKEY_USERS|HKU)\\(?<sub>.+)$') {
|
|
$hive = [Microsoft.Win32.RegistryHive]::Users
|
|
$subKeyPath = $Matches['sub']
|
|
}
|
|
else {
|
|
$outObj.Status = "Failed: Unsupported registry root in Key: $fullKey"
|
|
return $outObj
|
|
}
|
|
|
|
if ($PSCmdlet.ShouldProcess("$computer\$fullKey", "Set ImagePath to: $data")) {
|
|
try {
|
|
# Open remote HKLM/HKU
|
|
$base = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hive, $computer)
|
|
|
|
# Open the service key writable
|
|
$svcKey = $base.OpenSubKey($subKeyPath, $true)
|
|
if ($null -eq $svcKey) {
|
|
$outObj.Status = "Failed: Registry key not found: $fullKey"
|
|
return $outObj
|
|
}
|
|
|
|
try {
|
|
# Write as REG_EXPAND_SZ (ExpandString)
|
|
$svcKey.SetValue('ImagePath', $data, [Microsoft.Win32.RegistryValueKind]::ExpandString)
|
|
}
|
|
finally {
|
|
$svcKey.Close()
|
|
$base.Close()
|
|
}
|
|
|
|
$outObj.Status = "Fixed"
|
|
}
|
|
catch {
|
|
$msg = $_.Exception.Message
|
|
if ([string]::IsNullOrWhiteSpace($msg)) { $msg = "$_" }
|
|
$outObj.Status = "Failed: $msg"
|
|
}
|
|
}
|
|
else {
|
|
$outObj.Status = "WhatIf"
|
|
}
|
|
|
|
return $outObj
|
|
}
|
|
|
|
|
|
}
|
|
|
|
process {
|
|
switch ($Operation) {
|
|
|
|
'Scan' {
|
|
if ($PSCmdlet.ParameterSetName -ne 'ScanSet') {
|
|
throw "Operation 'Scan' requires -ComputerName (not pipeline input)."
|
|
}
|
|
_Scan -Targets $ComputerName
|
|
}
|
|
|
|
'Analyze' {
|
|
if ($PSCmdlet.ParameterSetName -ne 'PipelineSet') {
|
|
throw "Operation 'Analyze' requires pipeline input (-InputObject)."
|
|
}
|
|
if ($null -ne $InputObject) { _AnalyzeOne -Obj $InputObject }
|
|
}
|
|
|
|
'Repair' {
|
|
if ($PSCmdlet.ParameterSetName -ne 'PipelineSet') {
|
|
throw "Operation 'Repair' requires pipeline input (-InputObject)."
|
|
}
|
|
if ($null -ne $InputObject) { _RepairOne -Obj $InputObject }
|
|
}
|
|
|
|
'ScanAnalyze' {
|
|
if ($PSCmdlet.ParameterSetName -ne 'ScanSet') {
|
|
throw "Operation 'ScanAnalyze' requires -ComputerName (not pipeline input)."
|
|
}
|
|
_Scan -Targets $ComputerName | ForEach-Object { _AnalyzeOne -Obj $_ }
|
|
}
|
|
|
|
'ScanFix' {
|
|
if ($PSCmdlet.ParameterSetName -ne 'ScanSet') {
|
|
throw "Operation 'ScanFix' requires -ComputerName (not pipeline input)."
|
|
}
|
|
_Scan -Targets $ComputerName |
|
|
ForEach-Object { _AnalyzeOne -Obj $_ } |
|
|
ForEach-Object { _RepairOne -Obj $_ }
|
|
}
|
|
}
|
|
}
|
|
}
|