
Scripted Microsoft Patch Removal
September 19, 2011Many patch management systems have the ability to uninstall previously deployed patches. This functionality is typically used when a conflict with the patch is discovered after deployment.
Unfortunately, many of the automated removal mechanisms depend on the patch developer supporting and including a patch removal mechanism for each patch. When a patch doesn’t support this functionality, the management platform falls short in supporting this function.
As a workaround, here are a couple of scripts that will provide a semi-automated approach that can be used to remove patches remotely.
First, this script will list the patches installed on a system in the past X days where X is a command line parameter:
#Get list of patched installed in the past X days
param($Argument1,$Argument2)
$ComputerName = $Argument1
$intDays = $Argument2
$bolDaysValid = $true
#Validate second paramter – must be a number between 1 and 1000
If ([Microsoft.VisualBasic.Information]::isnumeric($intDays)) {
If ($intDays -lt 1 -or $intDays -gt 1000) {$bolDaysvalid = $false }
else {$bolDaysValid = $true}
}
else {
$bolDaysValid = $false
}
If ($bolDaysValid -eq $false) {
write-host "Invalid days parameter. Please use a number between 1 and 1000."
write-host "Example: .\PatchList.ps1 mycomputer 30"
write-host "Exiting"
Exit
}
#Validate first paramter – WMI call to get OS
$OS = Get-WmiObject -Class win32_OperatingSystem -namespace "root\CIMV2" -ComputerName $computerName -ErrorAction silentlycontinue
if ($OS -eq $NULL) {
write-host "Can’t access computer $ComputerName. Exiting."
Exit
}
#Get list of updates
Get-WmiObject -Computername $ComputerName Win32_QuickFixEngineering | ? {$_.InstalledOn} | where { (Get-date($_.Installedon)) -gt (get-date).adddays(-$intDays) }
Then, once the research is complete and the offending patch is found, the following script can be used to remotely remove the patch.
param($Argument1,$Argument2)
Add-Type -AssemblyName Microsoft.VisualBasic
$computername=$Argument1
$hotfixid=[string]$Argument2
#Second paramter validation – make sure it starts with ‘KB’ followed by a number
If (-not (($hotfixid.substring(0,2) -eq "KB") -and ([Microsoft.VisualBasic.Information]::isnumeric($hotfixid.substring(2))))) {
write-host "Invalid hotfix parameter. Please use ‘KB’ and the article number."
write-host "Example: .\PatchRemove.ps1 mycomputer KB976432"
write-host "Exiting"
Exit
}
#First paramter validation – get OS to be used later. If call fails, bad parameter
$OS = Get-WmiObject -Class win32_OperatingSystem -namespace "root\CIMV2" -ComputerName $computerName -ErrorAction silentlycontinue
if ($OS -eq $NULL) {
write-host "Can’t access computer $ComputerName. Exiting."
Exit
}
#Get hotfix list from target computer
$hotfixes = Get-WmiObject -ComputerName $computername -Class Win32_QuickFixEngineering |select hotfixid
#Search for requested hotfix
if($hotfixes -match $hotfixID) {
$hotfixNum = $HotfixID.Replace("KB","")
Write-host "Found the hotfix KB " $HotfixNum
Write-Host "Uninstalling the hotfix"
#Windows 2008/R2 use WUSA to uninstall patch
if ($OS.Version -like "6*") {
$UninstallString = "cmd.exe /c wusa.exe /uninstall /KB:$hotfixNum /quiet /norestart"
$strProcess = "wusa"
}
#Windows 2003 use spuninst in $NTuninstall folder to uninstall patch
elseif ($OS.Version -like "5*") {
$colFiles = Get-WMIObject -ComputerName $computername -Class CIM_DataFile -Filter "Name=`"C:\\Windows\\`$NtUninstall$HotFixID`$\\spuninst\\spuninst.exe`""
if ($colfiles.FileName -eq $NULL) {
Write-Host "Could not find removal script, please remove the hotfix manually."
}
else {
$UninstallString = "C:\Windows\`$NtUninstallKB$hotfixNum`$\spuninst\spuninst.exe /quiet /z"
$strProcess = "spuninst"
}
}
#Send removal command
([WMICLASS]"\\$computername\ROOT\CIMV2:win32_process").Create($UninstallString) | out-null
#Wait for removal to finish
while (@(Get-Process $strProcess -computername $computername -ErrorAction SilentlyContinue).Count -ne 0) {
Start-Sleep 3
Write-Host "Waiting for update removal to finish …"
}
#Test removal by getting hotfix list again
$afterhotfixes = Get-WmiObject -ComputerName $computername -Class Win32_QuickFixEngineering |select hotfixid
if($afterhotfixes -match $hotfixID) {
write-host "Uninstallation of $hotfixID succeeded"
}
else {
write-host "Uninstallation of $hotfixID failed"
}
}
else {
write-host "Hotfix $hotfixID not found"
return
}
Note that these scripts are tested on servers running Windows 2003 or later.