Files
SAMY/test.ps1
2026-02-05 02:33:36 -05:00

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 $_ }
}
}
}
}