PowerShell: Fixing Unquoted Service Paths (Complete)

This post is a culmination for the previous 3 script posts. All three of the scripts are here, updated with progress output so you’re not staring at the blank screen for hours during runs against dozens or hundreds of computers, or over low-quality connections.

In addition, I’ve included a custom Parallel wrapper that will take the longest part (Getting paths from machines) and allow it to run in configurable parallel. All outputs are PSCustom Objects and can be sorted/manipulated/filtered by standard PowerShell cmdlets or exported to CSV/HTML as desired.

Remember to check your results! I cannot be liable for any damage caused by running these scripts. It’s your environment and you ran them. That said I am very confident this will correctly fix all but the most extreme cases.

I recommend:
.\Get-SVCPath.ps1 | .\Find-BADSVCPath.ps1 | Export-CSV report.csv

Open in excel and sort on the column that says “BadKey”
Make sure all “FixedKey” values look correct. You can change any that do not look correct in Excel as long as you do not move the header. Retain the CSV file type so Excel doesn’t feed additional garbage into your file.

When satisfied your results are good, actually run the fix:
Import-CSV report.csv | .\Fix-BADSVCPath.ps1

.\Get-SVCPath.ps1
This script will contact a remote machine via the network protocols built into REG.exe. The remote host does not need to be running PowerShell, only the host this script is executed on does. You must be respected as an administrator on the target machine. It will create a custom PowerShell object for each key it locates. Offline machines will have their fields marked “Unavailable”

Computername: Name
Status: Retrieved
Key: \Path\Name
ImagePath: Ltr:\PathValue\Executable Argument(s)

Usage
You can specify a collection of computers on the command line to interrogate, you can get-content from a text file, or you can pipe objects in that have name/computername parameters. There is an optional parameter to turn progress bars off (-progress “No”), which is used by my parallel wrapper. If you do not include any arguments, it assumes the computername environment variable on the host running the script and only runs locally.

.\Get-SVCPath.ps1 computer1,computer2,computer3
.\Get-SVCPath.ps1 (Get-Content textfile)
Get-ADComputer -filter * | .\Get-SVCPath.ps1

#GET-SVCpath.ps1
[cmdletbinding()]
	Param ( #Define a Mandatory name input
	[Parameter(
	ValueFromPipeline=$true,
	ValueFromPipelinebyPropertyName=$true, 
	Position=0)]
	[Alias('Computer', 'ComputerName', 'Server', '__ServerName')]
		[string[]]$name = $ENV:Computername,
	[Parameter(Position=1)]
		[string]$progress = "Yes"
	) #End Param
 
Process
{ #Process Each object on Pipeline
	ForEach ($computer in $name)
	{ #ForEach for singular or arrayed input on the shell
	  #Try to get SVC Paths from $computer
	Write-Progress "Done" "Done" -Completed #clear progress bars inherited from the pipeline
	if ($progress -eq "Yes"){ Write-Progress -Id 1 -Activity "Getting keys for $computer" -Status "Connecting..."}
	$result = REG QUERY "\\$computer\HKLM\SYSTEM\CurrentControlSet\Services" /v ImagePath /s 2>&1
	#Error output from this command doesn't catch, so we need to test for it...
	if ($result[0] -like "*ERROR*" -or $result[0] -like "*Denied*")
		{ #Only evals true when return from reg is exception
		if ($progress -eq "Yes"){ Write-Progress -Id 1 -Activity "Getting keys for $computer" -Status "Connection Failed"}
		$obj = New-Object -TypeName PSObject
		$obj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
		$obj | Add-Member -MemberType NoteProperty -Name Status -Value "REG Failed"
		$obj | Add-Member -MemberType NoteProperty -Name Key -Value "Unavailable"
		$obj | Add-Member -MemberType NoteProperty -Name ImagePath -Value "Unavailable"
		[array]$collection += $obj
		}	
	else
		{
		#Clean up the format of the results array
		if ($progress -eq "Yes"){ Write-Progress -Id 1 -Activity "Getting keys for $computer" -Status "Connected"}
		$result = $result[0..($result.length -2)] #remove last (blank line and REG Summary)
		$result = $result | ? {$_ -ne ""} #Removes Blank Lines
		$count = 0
		While ($count -lt $result.length)
			{
 			if ($progress -eq "Yes"){ Write-Progress -Id 2 -Activity "Processing keys..." -Status "Formatting $computer\$($result[$count])"}
			$obj = New-Object -Typename PSObject
			$obj | Add-Member -Membertype NoteProperty -Name ComputerName -Value $computer
			$obj | Add-Member -MemberType NoteProperty -Name Status -Value "Retrieved"
			$obj | Add-Member -MemberType NoteProperty -Name Key -Value $result[$count]
			$pathvalue = $($result[$count+1]).Split("", 11) #split ImagePath return
			$pathvalue = $pathvalue[10].Trim(" ") #Trim out white space, left with just value data
			$obj | Add-Member -MemberType NoteProperty -Name ImagePath -Value $pathvalue
 
			[array]$collection += $obj
 
			$count = $count + 2
			} #End While
		} #End Else
	if ($progress -eq "Yes"){Write-Progress -Id 2 "Done" "Done" -Completed}
	Write-Output $collection
	$collection = $null #reset collection
	} #End ForEach
	if ($progress -eq "Yes"){Write-Progress -Id 1 "Done" "Done" -Completed}
 
} #End Process

.\Find-BADSVCPath.ps1
This script inspects the objects that result from .\Get-SVCPath for unquoted/improperly quoted service. It will amend the object and mark it “Badkey = Yes” or “BadKey = No”. If a bad key is detected, it will update the “FixedKey” field with the properly quoted path value. This script accepts its input from the pipeline. Values marked unavailable (by offline) are passed through the pipeline.

Useage

From the pipeline only:
.\Get-SVCPath.ps1 | .\Find-BADSVCPath.ps1
-or
$Var = .\Get-SVCPath.ps1
$Var | .\Find-BADSVCPath.ps1

#Find-BADSVCPath.ps1
[cmdletbinding()]
	Param ( #Define a Mandatory input
	[Parameter(
	 ValueFromPipeline=$true,
	 ValueFromPipelinebyPropertyName=$true,
	 Position=0)] $obj
	) #End Param
 
Process
{ #Process Each object on Pipeline
	Write-Progress -Activity "Checking for bad keys: " -status "Checking $($obj.computername)\$($obj.key)"
	if ($obj.key -eq "Unavailable")
	{ #The keys were unavailable, I just append object and continue
	$obj | Add-Member –MemberType NoteProperty –Name BadKey -Value "Unknown"
	$obj | Add-Member –MemberType NoteProperty –Name FixedKey -Value "Can't Fix"
	Write-Output $obj
	$obj = $nul #clear $obj
 
	} #end if
	else
	{
	#If we get here, I have a key to examine and fix
	#We're looking for keys with spaces in the path and unquoted
	#the Path is always the first thing on the line, even with embedded arguments
	$examine = $obj.ImagePath
	if (!($examine.StartsWith('"'))) { #Doesn't start with a quote
		if (!($examine.StartsWith("\??"))) { #Some MS Services start with this but don't appear vulnerable
			if ($examine.contains(" ")) { #If contains space
				#when I get here, I can either have a good path with arguments, or a bad path
				if ($examine.contains("-") -or $examine.contains("/")) { #found arguments, might still be bad
					#split out arguments
					$split = $examine -split " -", 0, "simplematch"
					$split = $split[0] -split " /", 0, "simplematch"
					$newpath = $split[0].Trim(" ") #Path minus flagged args
					if ($newpath.contains(" ")){
						#check for unflagged argument
						$eval = $newpath -Replace '".*"', '' #drop all quoted arguments
						$detunflagged = $eval -split "\", 0, "simplematch" #split on foler delim
							if ($detunflagged[-1].contains(" ")){ #last elem is executable and any unquoted args
								$fixarg = $detunflagged[-1] -split " ", 0, "simplematch" #split out args
								$quoteexe = $fixarg[0] + '"' #quote that EXE and insert it back
								$examine = $examine.Replace($fixarg[0], $quoteexe)
								$examine = $examine.Replace($examine, '"' + $examine)
								$badpath = $true
							} #end detect unflagged
						$examine = $examine.Replace($newpath, '"' + $newpath + '"')
						$badpath = $true
					} #end if newpath
					else { #if newpath doesn't have spaces, it was just the argument tripping the check
						$badpath = $false
					} #end else
				} #end if parameter
				else
					{#check for unflagged argument
					$eval = $examine -Replace '".*"', '' #drop all quoted arguments
					$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.Replace($examine, '"' + $examine)
						$badpath = $true
					} #end detect unflagged
					else
					{#just a bad path
						#surround path in quotes
						$examine = $examine.replace($examine, '"' + $examine + '"')
						$badpath = $true
					}#end else
				}#end else
			}#end if contains space
			else { $badpath = $false }
		} #end if starts with \??
		else { $badpath = $false }
	} #end if startswith quote
	else { $badpath = $false }
	#Update Objects
	if ($badpath -eq $false){
		$obj | Add-Member -MemberType NoteProperty -Name BadKey -Value "No"
		$obj | Add-Member -MemberType NoteProperty -Name FixedKey -Value "N/A"
		Write-Output $obj
		$obj = $nul #clear $obj
		}
	if ($badpath -eq $true){
		$obj | Add-Member -MemberType NoteProperty -Name BadKey -Value "Yes"
		#sometimes we catch doublequotes
		if ($examine.endswith('""')){ $examine = $examine.replace('""','"') }
		$obj | Add-Member -MemberType NoteProperty -Name FixedKey -Value $examine
		Write-Output $obj
		$obj = $nul #clear $obj
		}	
	} #end top else
} #End Process

.\Fix-BADSVCPath.ps1
This script accepts input on the pipeline from .\Find-BADSVCPath. The “FixedKey” value from the object is read, properly escaped, and fed to a forced REG ADD. It then updates the object status to “Fixed.” Values not marked bad are simply passed through the pipeline.

Useage

From the Pipeline only:
.\Get-SVCPath.ps1 | .\Find-BADSVCPath.ps1 | .\Fix-BADSVCPath.ps1
-or-
.\Get-SVCPath.ps1 | .\Find-BADSVCPath.ps1 | Export-CSV result.csv
Import-CSV result.csv | .\Fix-BADSVCPath.ps1

#Fix-BADSVCPath.ps1
[cmdletbinding()]
	Param ( #Define a Mandatory input
	[Parameter(
	 ValueFromPipeline=$true,
	 ValueFromPipelinebyPropertyName=$true,
	 Position=0)] $obj
	) #End Param
 
Process
{ #Process Each object on Pipeline
	if ($obj.badkey -eq "Yes"){
		Write-Progress -Activity "Fixing $($obj.computername)\$($obj.key)" -Status "Working..."
		$regpath = $obj.Fixedkey
		$regpath = '"' + $regpath.replace('"', '\"') + '"' + ' /f'
		$obj.status = "Fixed"
		REG ADD "\\$($obj.computername)\$($obj.key)" /v ImagePath /t REG_EXPAND_SZ /d $regpath
		}
	Write-Output $obj
 
} #End Process

.\WrapParallel.ps1
This script is a parallel wrapped for the .\Get-SVCTag script, which is the longest running element of the workflow due to network constraints. This wrapper is configurable, and can accept input either from the pipeline or from the shell directly. The script is throttled by checking for configured number of running jobs, and sleeping for the configured interval. It’s not a “true” throttle, but its close enough. The Progress bar reflects which machine was last sent, the number of running jobs, and the number of jobs detected complete in the background. This script could be used to wrap other scripts in parallel if you remove the “-progress No” argument from its start-job call. It waits until all objects are jobs are completed before feeding the pipeline.

Parameters
-scriptpath (Mandatory) The path to the script you want wrap parallel. Can be local to the path of execution and dot sourced.

-name (Mandatory) Can be fed either directly, via (Get-Content textfile) or from the pipeline

-throttle (optional) Defaults to 10. When the script encounters more than 10 jobs who are state running, it sleeps for the specified timer before sending another job. It checks before each job is sent.

-sleep (optional) Defaults to 30 seconds. When the throttle limit is hit, this parameter defines the sleep-interval before sending another job.

Useage

.\WrapParallel.ps1 -scriptpath .\Get-SVCPath.ps1 -name (Get-Content textfile) -throttle 20 | .\Find.....
-or-
Get-ADComputer -filter * | .\WrapParallel.ps1 -scriptpath .\Get-SVCPath.ps1 -sleep 60 | .\Find.....

###This is a parallel wrapper for script processes
 
[cmdletbinding()]
	Param ( #Define a Mandatory name input
	[Parameter(ValueFromPipeline=$true,ValueFromPipelinebyPropertyName=$true,Position=0,Mandatory=$true)]
		[Alias('Computer', 'ComputerName', 'Server', '__ServerName')]
		[string[]]$name = $ENV:Computername,
	[Parameter(Mandatory=$true)]
		[string]$scriptpath, 
	[int]$throttle = 10, 
	[int]$sleep = 30
	) #End Param
 
 
## Start Parallel Processing:
$jobcount = 0
$completecount = 0
$totaljobs = $name.count
ForEach ($pc in $name) {
	If ($jobcount -gt $throttle){Start-Sleep -Seconds $sleep}
 
		Start-Job -filepath $scriptpath -ArgumentList "$pc", "-progress no" > $nul
 
			$jobs = Get-Job | Where {$_.state -eq "Running"}
			$jobcount = $jobs.count
			$jobs = Get-Job | Where {$_.state -eq "Completed"}
			$completecount = $jobs.count
 
			$sentjobs = (Get-Job).count
 
			Write-Progress -id 1 -Activity "Sending Job to $pc" -Status "Sent $sentjobs of $totaljobs ($jobcount jobs 'Running') ($completecount jobs 'Completed')" -PercentComplete ((($sentjobs) / ($totaljobs))*100)
			} #End ForEach
 
#Wait for Jobs to finish
$jobcount = $totaljobs
Write-Progress -id 1 "Done" "Done" -completed
While ($jobcount -ne $null){
$countwait = $totaljobs - $jobcount
Write-Progress -id 1 -Activity "Jobs are still running..." -Status "Waiting for $jobcount jobs" -PercentComplete (($countwait / $totaljobs)*100)
$jobs = Get-Job | Where {$_.state -eq "Running"}
$jobcount = $jobs.Count
Start-Sleep -Seconds 3
}
 
Start-Sleep -Seconds 3
 
#Get-Data
$Global:TotalReport = @()
$jobs = Get-Job | Where {$_.state -eq "Completed"}
ForEach ($job in $jobs) {
	$result = Receive-Job $job.id -keep
	$Global:TotalReport += $result
	}
 
Write-Progress -id 1 "Done" "Done" -completed
 
$Global:TotalReport
 
#Clean up jobs
 
Get-Job | Remove-Job -force

Jeff is a system administrator for an enterprise of over 4000 devices. He is comfortable on Windows and *nix and participates in everything from desktop to network support. In addition, he develops in PowerShell to aid in the automation of administrative tasks.

When not at work, Jeff is adjunct faculty at the University of Alaska Anchorage, where he teaches PC Architecture to first year students in the Computer and Networking Technology Program. He is also working on new curriculum in PowerShell and Virtualization as well as leading department technology deployments.


View Jeff Liford's profile on LinkedIn



Posted in Computer Security, Desktop, General, PowerShell