327 lines
13 KiB
PowerShell
327 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 *
|
|
$badpath = $false
|
|
$examine = $outObj.ImagePath
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Activity "Analyzing ImagePath" -Status "Checking $($outObj.ComputerName)\$($outObj.Key)"
|
|
}
|
|
|
|
if ($outObj.Key -eq "Unavailable" -or $examine -eq "Unavailable" -or [string]::IsNullOrWhiteSpace($examine)) {
|
|
$outObj | Add-Member NoteProperty BadKey "Unknown" -Force
|
|
$outObj | Add-Member NoteProperty FixedKey "Can't Fix" -Force
|
|
return $outObj
|
|
}
|
|
|
|
# Ignore already-quoted or special \?? prefixes
|
|
if (-not $examine.StartsWith('"') -and -not $examine.StartsWith("\??")) {
|
|
|
|
if ($examine.Contains(" ")) {
|
|
|
|
# If we see flagged args, try to isolate a path portion
|
|
if ($examine.Contains("-") -or $examine.Contains("/")) {
|
|
|
|
$split = $examine -split " -", 0, "simplematch"
|
|
$split = $split[0] -split " /", 0, "simplematch"
|
|
$newpath = $split[0].Trim()
|
|
|
|
if ($newpath.Contains(" ")) {
|
|
$eval = $newpath -Replace '".*"', ''
|
|
$detunflagged = $eval -split "\\", 0, "simplematch"
|
|
|
|
if ($detunflagged[-1].Contains(" ")) {
|
|
$fixarg = $detunflagged[-1] -split " ", 0, "simplematch"
|
|
$quoteexe = $fixarg[0] + '"'
|
|
$examine = $examine.Replace($fixarg[0], $quoteexe)
|
|
$examine = '"' + $examine.Trim('"') + '"'
|
|
$badpath = $true
|
|
}
|
|
|
|
$examine = $examine.Replace($newpath, '"' + $newpath + '"')
|
|
$badpath = $true
|
|
}
|
|
|
|
} else {
|
|
# No flagged args, either just a bad path or an unflagged argument scenario
|
|
$eval = $examine -Replace '".*"', ''
|
|
$detunflagged = $eval -split "\\", 0, "simplematch"
|
|
|
|
if ($detunflagged[-1].Contains(" ")) {
|
|
$fixarg = $detunflagged[-1] -split " ", 0, "simplematch"
|
|
$quoteexe = $fixarg[0] + '"'
|
|
$examine = $examine.Replace($fixarg[0], $quoteexe)
|
|
$examine = '"' + $examine.Trim('"') + '"'
|
|
$badpath = $true
|
|
} else {
|
|
$examine = '"' + $examine.Trim('"') + '"'
|
|
$badpath = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (-not $badpath) {
|
|
$outObj | Add-Member NoteProperty BadKey "No" -Force
|
|
$outObj | Add-Member NoteProperty FixedKey "N/A" -Force
|
|
return $outObj
|
|
}
|
|
|
|
while ($examine.EndsWith('""')) { $examine = $examine.Substring(0, $examine.Length - 1) }
|
|
|
|
$outObj | Add-Member NoteProperty BadKey "Yes" -Force
|
|
$outObj | Add-Member NoteProperty FixedKey $examine -Force
|
|
return $outObj
|
|
}
|
|
|
|
function _RepairOne {
|
|
param($Obj)
|
|
|
|
$outObj = $Obj | Select-Object *
|
|
|
|
if ($outObj.BadKey -ne 'Yes') {
|
|
return $outObj
|
|
}
|
|
|
|
$target = "\\$($outObj.ComputerName)\$($outObj.Key)"
|
|
$data = $outObj.FixedKey
|
|
|
|
if ($ShowProgress) {
|
|
Write-Progress -Activity "Repairing ImagePath" -Status "Fixing $($outObj.ComputerName)\$($outObj.Key)"
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($data) -or $data -eq 'N/A' -or $data -eq "Can't Fix") {
|
|
$outObj.Status = "Skipped (no FixedKey)"
|
|
return $outObj
|
|
}
|
|
|
|
if ($PSCmdlet.ShouldProcess($target, "Set ImagePath to: $data")) {
|
|
try {
|
|
$args = @(
|
|
'ADD', $target,
|
|
'/v', 'ImagePath',
|
|
'/t', 'REG_EXPAND_SZ',
|
|
'/d', $data,
|
|
'/f'
|
|
)
|
|
|
|
$output = & reg.exe @args 2>&1
|
|
if ($LASTEXITCODE -eq 0) {
|
|
$outObj.Status = "Fixed"
|
|
} else {
|
|
$msg = ($output | Out-String).Trim()
|
|
if ([string]::IsNullOrWhiteSpace($msg)) { $msg = "reg.exe exit code $LASTEXITCODE" }
|
|
$outObj.Status = "Failed: $msg"
|
|
}
|
|
}
|
|
catch {
|
|
$outObj.Status = "Failed: $($_.Exception.Message)"
|
|
}
|
|
} 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 $_ }
|
|
}
|
|
}
|
|
}
|
|
}
|