Microsoft Windows Unquoted Service Path Enumeration – SCCM baseline

This is a vulnerability that Nessus highlighted on some of our laptop estate, it highlighted the vulnerability was related to an unquoted Intel Audio Service. After quite a lot of searching and testing I’ve managed to resolve this using an SCCM baseline. The script is dynamic so it will fix any Windows Service that is vulnerable to this exploit. Screenshots are below, and code at the bottom.

Discovery script

Remediation script

Compliance rule

Deployment

Discovery script code

*you might need to overtype the quotation marks

$count = cmd /c 'wmic service get name,displayname,pathname,startmode |findstr /i "auto" |findstr /i /v "c:\windows\" |findstr /i /v """'

if ($count)
{
return $True
}
else
{
return $False
}

Remediation script code

The below code is basically a wrapper around this script https://github.com/VectorBCO/windows-path-enumerate/blob/development/Windows_Path_Enumerate.ps1.

The wrapper looks as follows:

function Fix-WindowsPathEnumerate {

<#
.SYNOPSIS
    Fix for Microsoft Windows Unquoted Service Path Enumeration

.DESCRIPTION
    Script for fixing vulnerability "Unquoted Service Path Enumeration" in Services and Uninstall strings. Script modifying registry values. 
    Require Administrator rights and should be run on x64 powershell version in case if OS also have x64 architecture

.PARAMETER FixServices
    This bool parameter allow proceed Services with vulnerability. By default this parameter enabled.
    For disabling this parameter use -FixServices:$False

.PARAMETER FixUninstall
    Parameter allow find and fix vulnerability in UninstallPaths.
    Will be covered paths for x86 and x64 applications on x64 systems.

.PARAMETER FixEnv
    Find services with Environment variables in the ImagePath parameter, and replace Env. variable to the it value
    EX. %ProgramFiles%\service.exe will be replace to "C:\Program Files\service.exe"

.PARAMETER WhatIf
    Parameter should be used for checking possible system impact.
    With this parameter script would not change anything on your system,
    and only will show information about possible (needed) changes.

.PARAMETER CreateBackup
    When switch parameter enabled script will export registry tree`s
    specified for services or uninstall strings based on operator selection.
    Tree would be exported before any changes.

    [Note] For restoring backup could be used RestoreBackup parameter
    [Note] For providing full backup path could be used BackupName parameter

.PARAMETER RestoreBackup
    This parameter will allow restore previously created backup.
    If BackupName parameter would not be provided will be used last created backup,
    in other case script will try to find selected backup name

    [Note] For creation backup could be used CreateBackup parameter
    [Note] For providing full backup path could be used BackupName parameter

.PARAMETER BackupFolderPath
    Parameter would be proceeded only with CreateBackup or RestoreBackup
    If CreateBackup or RestoreBackup parameter will be provided, then path from this parameter will be used.

    During backup will be created reg file with original values per each service and application that will be modified
    During restoration all reg files in the specified format will be iterable imported to the registry

    Input example: C:\Backup\

    Backup file format:
      for -FixServices switch => Service_<ServiceName>_YYYY-MM-DD_HHmmss.reg
      for -FixUninstall switch => Software_<ApplicationName>_YYYY-MM-DD_HHmmss.reg

.PARAMETER Passthru
    With this parameter will be returned object array without any messages in a console
    Each element will continue Service\Program Name, Path, Type <Service\Software>, ParamName <ImagePath\UninstallString>, OriginalValue, ExpectedValue

.PARAMETER Silent
    [i] Silent parameter will work only together with Passthru parameter
    If at least 1 Service Path or Uninstall String should be fixed script will return $true
    Otherwise script will return $false

    Example:
        .\windows_path_enumerate.ps1 -FixUninstall -WhatIf -Passthru -Silent
    Output:
        $true
    Description:
        $true mean at least 1 service need to be fixed.
        WhatIf switch mean that nothing was fixed, registry was only diagnosed for the vulnerability

.PARAMETER Help
    Will display this help message

.PARAMETER LogName
    Parameter allow to change output file location, or disable logging setting this parameter to empty string or $null.

.EXAMPLE
    # Run powershell as administrator and type path to this script. In case if it will not run type dot (.) before path.
    . C:\Scripts\Windows_Path_Enumerate.ps1


VERBOSE:
--------
    2017-02-19 15:43:50Z  :  INFO  :  ComputerName: W8-NB
    2017-02-19 15:43:50Z  :  Old Value :  Service: 'BadDriver' - %ProgramFiles%\bad driver\driver.exe -k -l 'oper'
    2017-02-19 15:43:50Z  :  Expected  :  Service: 'BadDriver' - "%ProgramFiles%\bad driver\driver.exe" -k -l 'oper'
    2017-02-19 15:43:50Z  :  SUCCESS  : New Value of ImagePath was changed for service 'BadDriver'
    2017-02-19 15:43:50Z  :  Old Value :  Service: 'NotAVirus' - C:\Program Files\Strange Software\virus.exe -silent
    2017-02-19 15:43:51Z  :  Expected  :  Service: 'NotAVirus' - "C:\Program Files\Strange Software\virus.exe" -silent'
    2017-02-19 15:43:51Z  :  SUCCESS  : New Value of ImagePath was changed for service 'NotAVirus'

Description
-----------
    Fix 2 services 'BadDriver', 'NotAVirus'.
    Env variable %ProgramFiles% did not changed to full path in service 'BadDriver'


.EXAMPLE
    # This command, or similar could be used for running script from SCCM
    Powershell -ExecutionPolicy bypass -command ". C:\Scripts\Windows_Path_Enumerate.ps1 -FixEnv"


VERBOSE:
--------
    2017-02-19 15:43:50Z  :  INFO  :  ComputerName: W8-NB
    2017-02-19 15:43:50Z  :  Old Value :  Service: 'BadDriver' - %ProgramFiles%\bad driver\driver.exe -k -l 'oper'
    2017-02-19 15:43:50Z  :  Expected  :  Service: 'BadDriver' - "C:\Program Files\bad driver\driver.exe" -k -l 'oper'
    2017-02-19 15:43:50Z  :  SUCCESS  : New Value of ImagePath was changed for service 'BadDriver'
    2017-02-19 15:43:50Z  :  Old Value :  Service: 'NotAVirus' - %SystemDrive%\Strange Software\virus.exe -silent
    2017-02-19 15:43:51Z  :  Expected  :  Service: 'NotAVirus' - "C:\Strange Software\virus.exe" -silent'
    2017-02-19 15:43:51Z  :  SUCCESS  : New Value of ImagePath was changed for service 'NotAVirus'

Description
-----------
    Fix 2 services 'BadDriver', 'NotAVirus'.
    Env variable %ProgramFiles% replaced to full path 'C:\Program Files' in service 'BadDriver'

.EXAMPLE
    # This command, or similar could be used for running script from SCCM
    Powershell -ExecutionPolicy bypass -command ". C:\Scripts\Windows_Path_Enumerate.ps1 -FixUninstall -FixServices:$False -WhatIf"


VERBOSE:
--------
    2018-07-02 22:23:02Z  :  INFO  :  ComputerName: test
    2018-07-02 22:23:04Z  :  Old Value : Software : 'FakeSoft32' - c:\Program files (x86)\Fake inc\Pseudo Software\uninstall.exe -silent
    2018-07-02 22:23:04Z  :  Expected  : Software : 'FakeSoft32' - "c:\Program files (x86)\Fake inc\Pseudo Software\uninstall.exe" -silent


Description
-----------
    Script will find and displayed


.EXAMPLE
    # This command will return $true if at least 1 path should be fixed or $false if there nothing to fix
    # Log will not be available
    .\windows_path_enumerate.ps1 -FixUninstall -WhatIf -Passthru -Silent -LogName ''


VERBOSE:
--------
    true



.NOTES
    Name:  Windows_Path_Enumerate.PS1
    Version: 3.5.1
    Author: Vector BCO
    Updated: 8 April 2021

.LINK
    https://github.com/VectorBCO/windows-path-enumerate/
    https://gallery.technet.microsoft.com/scriptcenter/Windows-Unquoted-Service-190f0341
    https://www.tenable.com/sc-report-templates/microsoft-windows-unquoted-service-path-enumeration
    http://www.commonexploits.com/unquoted-service-paths/
#>

[CmdletBinding(DefaultParameterSetName = "Fixing")]

Param (
    [parameter(Mandatory=$false,
        ParameterSetName = "Fixing")]
    [parameter(Mandatory = $False,
        ParameterSetName = "Restoring")]
    [Alias("s")]
        [Bool]$FixServices=$true,

    [parameter(Mandatory = $false,
        ParameterSetName = "Fixing")]
    [parameter(Mandatory=$False,
        ParameterSetName = "Restoring")]
    [Alias("u")]
        [Switch]$FixUninstall,

    [parameter(Mandatory = $false,
        ParameterSetName = "Fixing")]
    [Alias("e")]
        [Switch]$FixEnv,

    [parameter(Mandatory = $False,
        ParameterSetName = "Fixing")]
    [Alias("cb","backup")]
        [switch]$CreateBackup,

    [parameter(Mandatory=$False,
        ParameterSetName = "Restoring")]
    [Alias("rb","restore")]
        [switch]$RestoreBackup,

    [parameter(Mandatory=$False,
        ParameterSetName = "Fixing")]
    [parameter(Mandatory = $False,
        ParameterSetName = "Restoring")]
        [string]$BackupFolderPath = "C:\Temp\PathEnumerationBackup",

    [parameter(Mandatory = $False,
        ParameterSetName = "Fixing")]
    [parameter(Mandatory = $False,
        ParameterSetName = "Restoring")]
        [string]$LogName = "C:\Temp\ServicesFix-3.5.1.Log",

    [parameter(Mandatory = $False,
        ParameterSetName = "Fixing")]
    [parameter(Mandatory = $False,
        ParameterSetName = "Restoring")]
    [Alias("ShowOnly")]
        [Switch]$WhatIf,

    [parameter(Mandatory = $False,
        ParameterSetName = "Fixing")]
        [Switch]$Passthru,

    [parameter(Mandatory = $False,
        ParameterSetName = "Fixing")]
        [Switch]$Silent,

    [parameter(Mandatory = $true,
        ParameterSetName = "Help")]
    [Alias("h")]
        [switch]$Help
)

Function Fix-ServicePath {
    <#
    .SYNOPSIS
        Microsoft Windows Unquoted Service Path Enumeration

    .DESCRIPTION
        Use Fix-ServicePath to fix vulnerability "Unquoted Service Path Enumeration".

    .PARAMETER FixServices
        This switch parameter allow proceed Services with vulnerability. By default this parameter enabled.
        For disable this parameter use -FixServices:$False

    .PARAMETER FixUninstall
        Parameter allow find and fix vulnerability in UninstallPath.
        Will be covered paths for x86 and x64 applications on x64 systems.

    .PARAMETER FixEnv
        Find services with Environment variables in the ImagePath parameter, and replace Env. variable to the it value
        EX. %ProgramFiles%\service.exe will be replace to "C:\Program Files\service.exe"

    .PARAMETER WhatIf
        Parameter should be used for checking possible system impact.
        With this parameter script would not be changing anything on your system,
        and only will show information about possible changes

    .EXAMPLE
        Fix-ServicePath


    VERBOSE:
    --------
        2017-02-19 15:43:50Z  :  Old Value :  Service: 'BadDriver' - %ProgramFiles%\bad driver\driver.exe -k -l 'oper'
        2017-02-19 15:43:50Z  :  Expected  :  Service: 'BadDriver' - "%ProgramFiles%\bad driver\driver.exe" -k -l 'oper'
        2017-02-19 15:43:50Z  :  SUCCESS  : New Value of ImagePath was changed for service 'BadDriver'
        2017-02-19 15:43:50Z  :  Old Value :  Service: 'NotAVirus' - C:\Program Files\Strange Software\virus.exe -silent
        2017-02-19 15:43:51Z  :  Expected  :  Service: 'NotAVirus' - "C:\Program Files\Strange Software\virus.exe" -silent'
        2017-02-19 15:43:51Z  :  SUCCESS  : New Value of ImagePath was changed for service 'NotAVirus'

    Description
    -----------
        Fix 2 services 'BadDriver', 'NotAVirus'.
        Env variable %ProgramFiles% did not changed to full path in service 'BadDriver'


    .EXAMPLE
        Fix-ServicePath -FixEnv


    VERBOSE:
    --------
        2017-02-19 15:43:50Z  :  Old Value :  Service: 'BadDriver' - %ProgramFiles%\bad driver\driver.exe -k -l 'oper'
        2017-02-19 15:43:50Z  :  Expected  :  Service: 'BadDriver' - "C:\Program Files\bad driver\driver.exe" -k -l 'oper'
        2017-02-19 15:43:50Z  :  SUCCESS  : New Value of ImagePath was changed for service 'BadDriver'
        2017-02-19 15:43:50Z  :  Old Value :  Service: 'NotAVirus' - %SystemDrive%\Strange Software\virus.exe -silent
        2017-02-19 15:43:51Z  :  Expected  :  Service: 'NotAVirus' - "C:\Strange Software\virus.exe" -silent'
        2017-02-19 15:43:51Z  :  SUCCESS  : New Value of ImagePath was changed for service 'NotAVirus'

    Description
    -----------
        Fix 2 services 'BadDriver', 'NotAVirus'.
        Env variable %ProgramFiles% replaced to full path 'C:\Program Files' in service 'BadDriver'

    .EXAMPLE
        Fix-ServicePath -FixUninstall -FixServices:$False -WhatIf


    VERBOSE:
    --------
        2018-07-02 22:23:04Z  :  Old Value : Software : 'FakeSoft32' - c:\Program files (x86)\Fake inc\Pseudo Software\uninstall.exe -silent
        2018-07-02 22:23:04Z  :  Expected  : Software : 'FakeSoft32' - "c:\Program files (x86)\Fake inc\Pseudo Software\uninstall.exe" -silent


    Description
    -----------
        Script will find problems and only display result but will not change anything


    .NOTES
        Name:  Fix-ServicePath
        Version: 3.5
        Author: Vector BCO
        Last Modified: 3 May 2020

    .LINK
        https://gallery.technet.microsoft.com/scriptcenter/Windows-Unquoted-Service-190f0341
        https://www.tenable.com/sc-report-templates/microsoft-windows-unquoted-service-path-enumeration
        http://www.commonexploits.com/unquoted-service-paths/
    #>

    Param (
        [bool]$FixServices = $true,
        [Switch]$FixUninstall,
        [Switch]$FixEnv,
        [Switch]$Backup,
        [string]$BackupFolder = "C:\Temp\PathEnumeration",
        [Switch]$WhatIf,
        [Switch]$Passthru
    )

    # Get all services
    $FixParameters = @()
    If ($FixServices) {
        $FixParameters += @{"Path" = "HKLM:\SYSTEM\CurrentControlSet\Services\" ; "ParamName" = "ImagePath"}
    }
    If ($FixUninstall) {
        $FixParameters += @{"Path" = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\" ; "ParamName" = "UninstallString"}
        # If OS x64 - adding paths for x86 programs
        If (Test-Path "$($env:SystemDrive)\Program Files (x86)\") {
            $FixParameters += @{"Path" = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\" ; "ParamName" = "UninstallString"}
        }
    }
    If ($Backup){
        If (! (Test-Path $BackupFolder)){
            New-Item $BackupFolder -Force -ItemType Directory | Out-Null
        }
    }
    $PTElements = @()
    ForEach ($FixParameter in $FixParameters) {
        Get-ChildItem $FixParameter.Path -ErrorAction SilentlyContinue | ForEach-Object {
            $SpCharREGEX = '([\[\]])'
            $RegistryPath = $_.name -Replace 'HKEY_LOCAL_MACHINE', 'HKLM:' -replace $SpCharREGEX, '`$1'
            $OriginalPath = (Get-ItemProperty "$RegistryPath")
            $ImagePath = $OriginalPath.$($FixParameter.ParamName)
            If ($FixEnv) {
                If ($($OriginalPath.$($FixParameter.ParamName)) -match '%(?''envVar''[^%]+)%') {
                    $EnvVar = $Matches['envVar']
                    $FullVar = (Get-ChildItem env: | Where-Object {$_.Name -eq $EnvVar}).value
                    $ImagePath = $OriginalPath.$($FixParameter.ParamName) -replace "%$EnvVar%", $FullVar
                    Clear-Variable Matches
                } # End If
            } # End If $fixEnv
            # Get all services with vulnerability
            If (($ImagePath -like "* *") -and ($ImagePath -notLike '"*"*') -and ($ImagePath -like '*.exe*')) {
                # Skip MsiExec.exe in uninstall strings
                If ((($FixParameter.ParamName -eq 'UninstallString') -and ($ImagePath -NotMatch 'MsiExec(\.exe)?') -and ($ImagePath -Match '^((\w\:)|(%[-\w_()]+%))\\')) -or ($FixParameter.ParamName -eq 'ImagePath')) {
                    $NewPath = ($ImagePath -split ".exe ")[0]
                    $key = ($ImagePath -split ".exe ")[1]
                    $trigger = ($ImagePath -split ".exe ")[2]
                    $NewValue = ''
                    # Get service with vulnerability with key in ImagePath
                    If (-not ($trigger | Measure-Object).count -ge 1) {
                        If (($NewPath -like "* *") -and ($NewPath -notLike "*.exe")) {
                            $NewValue = "`"$NewPath.exe`" $key"
                        } # End If
                        # Get service with vulnerability with out key in ImagePath
                        ElseIf (($NewPath -like "* *") -and ($NewPath -like "*.exe")) {
                            $NewValue = "`"$NewPath`""
                        } # End ElseIf
                        If ((-not ([string]::IsNullOrEmpty($NewValue))) -and ($NewPath -like "* *")) {
                            try {
                                $soft_service = $(if ($FixParameter.ParamName -Eq 'ImagePath') {'Service'}Else {'Software'})
                                $OriginalPSPathOptimized = $OriginalPath.PSPath -replace $SpCharREGEX, '`$1'
                                Write-Output "$(get-date -format u)  :  Old Value : $soft_service : '$($OriginalPath.PSChildName)' - $($OriginalPath.$($FixParameter.ParamName))"
                                Write-Output "$(get-date -format u)  :  Expected  : $soft_service : '$($OriginalPath.PSChildName)' - $NewValue"
                                if ($Passthru){
                                    $PTElements += '' | Select-Object `
                                        @{n = 'Name'; e = {$OriginalPath.PSChildName}}, `
                                        @{n = 'Type'; e = {$soft_service}}, `
                                        @{n = 'ParamName'; e = {$FixParameter.ParamName}}, `
                                        @{n = 'Path'; e = {$OriginalPSPathOptimized}}, `
                                        @{n = 'OriginalValue'; e = {$OriginalPath.$($FixParameter.ParamName)}}, `
                                        @{n = 'ExpectedValue'; e = {$NewValue}}
                                }
                                If ($Backup){
                                    $BcpFileName = "$BackupFolder\$soft_service`_$($OriginalPath.PSChildName)`_$(get-date -uFormat "%Y-%m-%d_%H%M%S").reg"
                                    $BcpTmpFileName = "$BackupFolder\$soft_service`_$($OriginalPath.PSChildName)`_$(get-date -uFormat "%Y-%m-%d_%H%M%S").tmp"
                                    $BcpRegistryPath = $RegistryPath -replace '\:'
                                    Write-Output "$(get-date -format u)  :  Creating registry backup : $BcpFileName"
                                    $ExportResult = REG EXPORT $BcpRegistryPath $BcpTmpFileName | Out-String
                                    Get-Content $BcpTmpFileName | Out-File $BcpFileName -Append
                                    Remove-Item $BcpTmpFileName -Force -ErrorAction "SilentlyContinue"
                                    Write-Output "$(get-date -format u)  :  Backup Result : $($ExportResult -split '\r\n' | Where-Object {$_ -NotMatch '^$'})"
                                }
                                If (! $WhatIf) {
                                    Set-ItemProperty -Path $OriginalPSPathOptimized -Name $($FixParameter.ParamName) -Value $NewValue -ErrorAction Stop
                                    $DisplayName = ''
                                    $keyTmp = (Get-ItemProperty -Path $OriginalPSPathOptimized)
                                    If ($soft_service -match 'Software') {
                                        $DisplayName = $keyTmp.DisplayName
                                    }
                                    If ($keyTmp.$($FixParameter.ParamName) -eq $NewValue) {
                                        Write-Output "$(get-date -format u)  :  SUCCESS  : Path value was changed for $soft_service '$($OriginalPath.PSChildName)' $(if($DisplayName){"($DisplayName)"})"
                                    } # End If
                                    Else {
                                        Write-Output "$(get-date -format u)  :  ERROR  : Something is going wrong. Path was not changed for $soft_service '$(if($DisplayName){$DisplayName}else{$OriginalPath.PSChildName})'."
                                    } # End Else
                                } # End If
                            } # End try
                            Catch {
                                Write-Output "$(get-date -format u)  :  ERROR  : Something is going wrong. Value changing failed in service '$($OriginalPath.PSChildName)'."
                                Write-Output "$(get-date -format u)  :  ERROR  : $_"
                            } # End Catch
                            Clear-Variable NewValue
                        } # End If
                    } # End Main If
                } # End if (Skip not needed strings)
            } # End If
            If (($trigger | Measure-Object).count -ge 1) {
                Write-Output "$(get-date -format u)  :  ERROR  : Can't parse  $($OriginalPath.$($FixParameter.ParamName)) in registry  $($OriginalPath.PSPath -replace 'Microsoft\.PowerShell\.Core\\Registry\:\:') "
            } # End If
        } # End Foreach
    } # End Foreach
    if ($Passthru){
        return $PTElements
    }
}

Function Get-OSandPoShArchitecture {
    # Check OS architecture
    if ((Get-CimInstance Win32_OperatingSystem | Select-Object OSArchitecture).OSArchitecture -match "64.?bits?") {
        if ([intptr]::Size -eq 8){
            Return $true, $true
        } else {
            Return $true, $false
        }
    } else { Return $false, $false }
}

Function Tee-Log {
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
            $Input,
        [Parameter(Mandatory = $true)]
            $FilePath,
        [switch]$Silent
    )
    if($Silent){
        $Input | Out-File -FilePath $LogName -Append
    } else {
        $Input | Tee-Object -FilePath $LogName -Append
    }
}

if ((! $FixServices) -and (! $FixUninstall)){
    Throw "Should be selected at least one of two parameters: FixServices or FixUninstall. `r`n For more details use 'get-help Windows_Path_Enumerate.ps1 -full'"
}

if ($Help){
    Write-Output "For help use this command in powershell: Get-Help $($MyInvocation.MyCommand.Path) -full"
    powershell -command "& Get-Help $($MyInvocation.MyCommand.Path) -full"
    exit
}

$OS, $PoSh = Get-OSandPoShArchitecture
If (($OS -eq $true) -and ($PoSh -eq $true)){
    $validation = "$(get-date -format u)  :  INFO  : Executed x64 Powershell on x64 OS"
} elseIf (($OS -eq $true) -and ($PoSh -eq $false)) {
    $validation =  "$(get-date -format u)  :  WARNING  : !ATTENTION! : Executed x32 Powershell on x64 OS. Not all vulnerabilities could be fixed.`r`n"
    $validation += "$(get-date -format u)  :  WARNING  : For fixing all vulnerabilities should be used x64 Powershell."
} else {
    $validation = "$(get-date -format u)  :  INFO  : Executed x32 Powershell on x32 OS"
}

$DeleteLogFile = $false

if ([string]::IsNullOrEmpty($LogName)){
    # Log will be written to the temp file if file not specified
    $DeleteLogFile = $true
    $LogName = New-TemporaryFile 
} 
If (! (Test-Path $LogName)){
    # If path does not exists it should be created
    try{
        $tmpLogPath = $LogName
        if ($tmpLogPath -NotMatch '[\\\/]$') { 
            $tmpLogName = ($tmpLogPath -split '[\\\/]')[-1]
            $tmpLogPath = $tmpLogPath -replace "$tmpLogName`$"
        } else {
            $tmpLogName = 'ServicesFix-3.X.Log'
        }
        if (! (Test-Path $tmpLogPath)) {
            New-Item -Path $tmpLogPath -Force -ItemType Directory | Out-Null
        }
        New-Item -Path "$tmpLogPath\$tmpLogName" -Force -ItemType File | Out-Null
        $LogName = "$tmpLogPath\$tmpLogName"
    } catch {
        Throw "Log file '$LogName' does not exists and cannot be created. Error: $_"
    }
}


'*********************************************************************' | Tee-Log -FilePath $LogName -Silent:$Passthru
"$(get-date -format u)  :  INFO  : ComputerName: $($Env:ComputerName)" | Tee-Log -FilePath $LogName -Silent:$Passthru
$validation | Tee-Log -FilePath $LogName -Silent:$Passthru

if ($RestoreBackup){
    if ($FixServices -and (! $FixUninstall)){
        $RegexPart = "Service"
    } elseif ($FixUninstall -and (! $FixServices)) {
        $RegexPart = "Software"
    } elseif ($FixUninstall -and $FixServices) {
        $RegexPart = "(Service|Software)"
    }

    if (Test-Path $BackupFolderPath){
        $FilesToImport = Get-ChildItem "$BackupFolderPath\" | Where-Object {$_.Name -match "$RegexPart`_.+_\d{4}-\d{1,2}-\d{1,2}_\d{3,6}\.reg$"} 
        if ([string]::IsNullOrEmpty($FilesToImport)){
            Write-Output "$(get-date -format u)  :  No backup files find in $BackupFolderPath" | Tee-Log -FilePath $LogName -Silent:$Passthru
        } else {
            Foreach ($FileToImport in $FilesToImport) {
                Write-Output "$(get-date -format u)  :  Importing '$($FileToImport.Name)' file to the registry" | Tee-Log -FilePath $LogName -Silent:$Passthru
                if ($WhatIf){
                    Write-Output "$(get-date -format u)  :  Whatif switch selected so nothing changed..." | Tee-Log -FilePath $LogName -Silent:$Passthru
                } else {
                    REGEDIT /s $($FileToImport.FullName)
                }
                #Write-Output "$(get-date -format u)  :  Result : $($ImportResult -split '\r\n' | Where-Object {$_ -NotMatch '^$'})" | Tee-Log -FilePath $LogName -Silent:$Passthru 
            }
        }
    } else {
        Write-Output "$(get-date -format u)  :  Backup folder does not exists. Nothing to restore..." | Tee-Log -FilePath $LogName -Silent:$Passthru
    }
} else {
    $ScriptExecutionResult = Fix-ServicePath `
        -FixUninstall:$FixUninstall `
        -FixServices:$FixServices `
        -WhatIf:$WhatIf `
        -FixEnv:$FixEnv `
        -Passthru:$Passthru `
        -Backup:$CreateBackup `
        -BackupFolder $BackupFolderPath 

    if ($Passthru -and (! [string]::IsNullOrEmpty($ScriptExecutionResult))){
        $Objects = $ScriptExecutionResult | Where-Object {$_.GetType().Name -eq 'PSCustomObject' }
        $ScriptExecutionResult = $ScriptExecutionResult | Where-Object {$_.GetType().Name -ne 'PSCustomObject' }
    }

    $ScriptExecutionResult | Tee-Log -FilePath $LogName -Silent:$Passthru
    If ($Passthru){
        If ($Silent -and $(( $Objects | Measure-Object ).Count -ge 1)){
            $True
        } ElseIf ($Silent){
            $False
        } Else {
            $Objects
        }
    }
}

if ($DeleteLogFile){
    Remove-Item $LogName -Force -ErrorAction "SilentlyContinue"
}

} # end function


$Status = Fix-WindowsPathEnumerate -Passthru -Silent
if ($Status) { 
    exit 0
    }
else {
    exit 1
}


Advertisement

ConfigMgr_OfflineImageServicing error – cannot delete

I came across an issue where the Offline Servicing for an OS image failed, and I couldnt get it to re-run successfully.

After a lot of research I found these commands helped to fully resolve the issue.

Run PowerShell as admin, then

dism.exe /cleanup-wim


dism.exe /cleanup-mountpoints


dism /get-mountedwiminfo

change the path below to your appropriate path. The below is 1 line of code.

dism /unMount-Wim /MountDir:F:\ConfigMgr_OfflineImageServicing\ABC00288\ImageMountDir /discard

posieandfig.co.uk

Resume BitLocker remotely using PowerShell

We came across a few machines during an in-place upgrade to Windows 10 1809 that failed to resume BitLocker once the upgrade had occurred.

We needed to identify which machines were affected, and we needed to remotely Resume bitlocker on those machines.

Identifying the machines in SQL

I came up with this SQL query that identifies the machines were the C:\ bitlocker status wasnt enabled:

select distinct HWS.DriveLetter0, HWS.ProtectionStatus0, SYS.Netbios_Name0
from v_GS_ENCRYPTABLE_VOLUME HWS INNER JOIN v_R_System SYS on HWS.ResourceID = SYS.ResourceID
where HWS.DriveLetter0 = 'C:' and HWS.ProtectionStatus0 = '0'

Resuming Bitlocker Remotely using PowerShell

verify its the machine in question

dir \\LAPTOP123\c$\users

check that the BitLocker status is actually suspended

Manage-bde -status -cn LAPTOP123 C:

Resume BitLocker

manage-bde -on C: -cn LAPTOP123

Outlook 2016/2019 – Disable Shared Mailbox Caching

We have been experiencing a lot of issues recently with Shared Mailboxes in Outlook 2016 not staying up to date.  This can be resolved by configuring 2 Group Policy settings.

These settings still allow the primary user mailbox to be cached to improve performance, but the Shared Mailboxes work Online so do not cache and remain up to date all the time.

The 2 settings you need to configure are:

User Configuration/Policies/Administrative Templates/Microsoft Outlook 2016/Account Settings/Exchange/Cached Exchange Mode
Download shared non-mail folders – Disabled
User Configuration/Policies/Administrative Templates/Microsoft Outlook 2016/Outlook Options/Delegates
Disable shared mail folder caching – Enabled

SharedMailbox

Include Lenovo BIOS update in a task sequence

We are deploying Windows 10 Creators Upgrade package to our 1400 Windows 1511 laptops, but we noticed a bug related to usb functionality that a BIOS update resolves.  Having never included a BIOS update in a task sequence before, I started the challenge and spent literally days getting it to work.  Now I have the process working I thought I would share to save someone else some long days.

 

  1. I first downloaded the bios update from the Lenovo site and then extracted it locally onto my machine.

 

  1. Then I created a new package in SCCM containing those files, and chose not to create a new program when asked in the wizard; we’ll run everything from command line.

 

  1. I created a new step in the task sequence to create a directory on the laptop running the task sequence. My logic was that I can copy the bios files there from the temporary package content, and run the bios upgrade from the permanent files rather than risking SCCM removing the content before the laptop has applied the update.  This seemed to add some stability to the bios installation process, so I’d recommend you do the same.

Add a Run Command Line step called Make Bios Directory and use the command

cmd.exe /c md c:\bios

(choose another directory if you wish)

1

 

4.   The next step is a Group with a wmi filter specified. This determines what model of laptop can run which bios upgrade, in the case of our Yoga 260s the wmi filter is

SELECT * FROM Win32_ComputerSystem WHERE MODEL LIKE ‘%20FEA00FUK%’

2

 

5.   The next step is where the bios files are copied from the package cache to c:\bios. Ensure you select the package that the bios files are located in

This is completed using the Run Command Line, and the command used is

xcopy.exe “.\*.*” “C:\bios” /D /E /C /I /Q /H /R /Y /S

3

 

6.  The final step is to apply the bios upgrade, again I use another Run Command Line action. It’s very important to populate the Start in: field with the directory the bios files are located in.  This caused me hours and hours of frustration as none of the previous guides showed this as being necessary, but in my case it certainly was needed. The command needed is

C:\bios\WINUPTP.exe -s

and the Start In field is

C:\bios

4

Make sure your laptop has AC Power or else the bios upgrade won’t occur! (Another gotcha I came across.)

Ensure the Continue on error tickbox is ticked on this step as it usually returns an error even though its successful.  (Another gotcha I came across.)

Now test the task sequence and ensure it works.

 

Within the logs in sccm it shows this Upgrading Bios step as failed as it returns an error code of 1 instead of 0.  Once I was happy that it was working reliably on multiple machines I altered the Success codes to include a code of 1.  Nice white logs rather than red and orange!

5

I squeezed in a little bit of time at the end because Lenovo give an option to edit the logo on the bios screen.  I went ahead and added the version number so I can easily tell which machines have the correct bios.  The file needs to be created in the root of the extracted bios files named logo.bmp, it has to be a certain size etc but just read the documentation for details.

Hope this saves someone some time.

6

 

 

 

 

Writing Current User registry keys in SCCM as System

It is possible to write CurrentUser registry keys by deploying an application/package that runs as the System.  This could be useful when installing an application and wanting to set the personalisation registry keys for the logged in user at the same time.  The script I’ve used below also allows you to install it for all users on that machine, and also for the Default User so all future users get those settings.

 

You’ll need 3 things:

  1. A registry file that contains the settings you want to add.

1

  1. This script from TechNet https://gallery.technet.microsoft.com/scriptcenter/Write-to-HKCU-from-the-3eac1692

**this file looks to have been removed for some reason. I have included the script at the bottom of the page, just save it as WriteToHkcuFromsystem.ps1**

  1. A batch file similar to this.

It basically enables the powershell script to run, runs the script to add the registry key(s) for the Current User that is logged on, and then returns the powershell execution policy back to what it was.

2

PowerShell.exe Set-ExecutionPolicy -ExecutionPolicy Unrestricted

PowerShell.exe -File “%~dp0WriteToHkcuFromsystem.ps1” -RegFile “%~dp0DisableTaskBarThumbnails.reg” -CurrentUser

PowerShell.exe Set-ExecutionPolicy -ExecutionPolicy Restricted

 

I created a new package in SCCM containing the following files

3

 

Ensure that you choose “Only when a user is logged on”. This means it will be able to pick up the Current User and apply the registry settings to that user.

4

 

For the Command being run just choose the install.bat.   I made sure it runs hidden as well.

5

 

Deploy out to some test machines and you should find it populates the Current User hive of the registry.  Take a look at the script on TechNet as it shows how to add the registry key(s) to   -CurrentUser -AllUsers -DefaultProfile

WriteToHkcuFromsystem.ps1 contents

PARAM(

    [Parameter(Mandatory=$true)]
    [ValidatePattern('\.reg$')]
    [string]$RegFile,

    [switch]$CurrentUser,
    [switch]$AllUsers,
    [switch]$DefaultProfile
)


function Write-Registry {
    PARAM($RegFileContents)
    $tempFile = '{0}{1:yyyyMMddHHmmssff}.reg' -f [IO.Path]::GetTempPath(), (Get-Date)
    $RegFileContents | Out-File -FilePath $tempFile
    Write-Host ('Writing registry from file {0}' -f $tempFile)
    try { $p = Start-Process -FilePath C:\Windows\regedit.exe -ArgumentList "/s $tempFile" -PassThru -Wait } catch { }
    if($p -ne $null) { $exitCode = $p.ExitCode } else { $exitCode = 0 }
    if($exitCode -ne 0) {
        Write-Warning 'There was an error merging the reg file'
    } else {
        Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
    }
}

if(-not (Test-Path -Path $RegFile)) {
    Write-Warning "RegFile $RegFile doesn't exist. Operation aborted"
} else {

    if($CurrentUser -or $AllUsers -or $DefaultProfile) {

        Write-Host ('Reading the registry file {0}' -f $RegFile)
        $registryData = Get-Content -Path $RegFile -ReadCount 0

        if($CurrentUser) {
            Write-Host "Writing to the currenlty loggoed on user's registry"
            $explorers = Get-WmiObject -Namespace root\cimv2 -Class Win32_Process -Filter "Name='explorer.exe'"
            $explorers | ForEach-Object {
                $owner = $_.GetOwner()
                if($owner.ReturnValue -eq 0) {
                    $user = '{0}\{1}' -f $owner.Domain, $owner.User
                    $ntAccount = New-Object -TypeName System.Security.Principal.NTAccount($user)
                    $sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
                    $RegFileContents = $registryData -replace 'HKEY_CURRENT_USER', "HKEY_USERS\$sid"
                    Write-Registry -RegFileContents $RegFileContents
                }
            }
        }

        if($AllUsers) {
            Write-Host "Writing to every user's registry"
            $res = C:\Windows\system32\reg.exe query HKEY_USERS
            $res -notmatch 'S-1-5-18|S-1-5-19|S-1-5-20|DEFAULT|Classes' | ForEach-Object {
                if($_ -ne '') {
                    $sid = $_ -replace 'HKEY_USERS\\'
                    $RegFileContents = $registryData -replace 'HKEY_CURRENT_USER', "HKEY_USERS\$sid"
                    Write-Registry -RegFileContents $RegFileContents

                }
            }
        }

        if($DefaultProfile) {
            Write-Host "Writing to the default profile's registry (for future users)"
            C:\Windows\System32\reg.exe load 'HKU\DefaultUser' C:\Users\Default\NTUSER.DAT | Out-Null
            $RegFileContents = $registryData -replace 'HKEY_CURRENT_USER', 'HKEY_USERS\DefaultUser'
            Write-Registry -RegFileContents $RegFileContents
            C:\Windows\System32\reg.exe unload 'HKU\DefaultUser' | Out-Null
        }

    } else {
        Write-Warning 'No mode was selected. Operation aborted'
    }
}

Silent install of InfoPath 2013 standalone

We have updated our task sequence to deploy Office 2016 but InfoPath is not included in Office 2016. Microsoft provide a standalone version of Infopath for installation and the below are instructions on how to make that installation occur silently. note: I strongly advise you install Infopath 2013 first and then Office 2016 second. Doing it the other way around means InfoPath 2013 overwrites some Office 2016 settings.

 

  1. Download one of the InfoPath standalone packages from here
  1. Extract the files using one of the below command from an elevated command prompt

infopath_4753-1001_x64_en-us.exe /extract

or

infopath_4753-1001_x86_en-us.exe /extract

 

  1. In the newly extracted files, open Config.xml located within the “infopathr.ww” directory

11

  1. Modify the Config.xml file like this:

<Configuration Product=”Infopathr”>

<Display Level=”none” CompletionNotice=”no” SuppressModal=”yes” AcceptEula=”Yes” />

</Configuration>

22

  1. I now use a batch file to run the installation. The batch file should be located in the root of the newly extracted files (same level as setup.exe)

“%~dp0setup.exe” /config “%~dp0infopathr.ww\config.xml”

33

  1. The first time InfoPath is opened it may prompt for activated online. If this occurs add in the following line at the end of the batch file

 

cscript “C:\Program Files (x86)\Microsoft Office\Office15\ospp.vbs” /act

 

Update 2

I also came across an issue where old Office 2003 format documents with the .xml file format would not open, excelDoc.xml WordDoc.xml etc.  To fix this add these reg keys and this restores the functionality and also makes sure you dont have blank icons for those xml files. For anyone using the reg keys below know it took 25+ hours to identify them!

The top section allows .xml files to be determined by their content.

The second section stops Edge from trying to take over .xml file formats.

The third section stops a security prompt in Outlook when the files are opened from attachments.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared\XML\ProgIds]
“InfoPath.Document”=””
“Excel.Sheet”=””
“PowerPoint.Show”=””
“Word.Document”=””

[HKEY_CLASSES_ROOT\AppXcc58vyzkbjbs4ky0mxrmxf8278rk9b3t]
“NoOpenWith”=””
“NoStaticDefaultVerb”=””

[HKEY_CLASSES_ROOT\xmlfile]
“EditFlags”=hex:00,00,01,00

 

 

unattended install InfoPath 2013 standalone Office 2013 Office 2016

Credits to Eddie Jackson http://eddiejackson.net/wp/?p=10954

Credits to Adam Fowler  https://www.adamfowlerit.com/2017/09/disabling-outlook-opening-mail-attachment-prompt/

Credits to Ramesh Srinivasan http://www.winhelponline.com/blog/edge-hijack-pdf-htm-associations/

 

 

Silent install of Adobe Flash v22 + forcing uninstallation of v10 + v11

We recently had a requirement to ensure all laptops were running the latest version v22 of Adobe Flash on our Windows 7 laptops, and a quick query in SCCM showed 95% were on version 21 which was good, but a number were stuck on version 10 and 11!  A lot of troubleshooting later and it turns out that the uninstall of these versions was the cause, as it was leaving behind some registry entries that caused the new version to fail installation.

 

After combining a few vbs scripts I came up with the following vbs script that removes the registry entries in order for the uninstall to complete, and the install to work.  If you need to alter it, locate the GUID  HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Installer\Products\xxx where xxx is the adobe flash installation. the sub keys will tell you whether it is or not.

removeRegTraces.vbs

'this script removes remnants of flash player from SOFTWARE\Classes\Installer\Products\ 
On Error Resume Next
Const HKEY_LOCAL_MACHINE = &H80000002

strComputer = "."

Set objRegistry = GetObject("winmgmts:{impersonationLevel=impersonate}!\\" & strComputer & "\root\default:StdRegProv")

'adobe flash 10.3.183.5
strKeyPath = "SOFTWARE\Classes\Installer\Products\C4DD4D27947025346BE3A7C7296834E0"
if KeyExists(HKEY_LOCAL_MACHINE, strKeyPath) = True then
   DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath 
end if

'adobe flash 11.0.1.152
strKeyPath = "SOFTWARE\Classes\Installer\Products\03797D32A1CEE534388FAABEEF25730B"
if KeyExists(HKEY_LOCAL_MACHINE, strKeyPath) = True then
   DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath 
end if

' adobe flash 16.0.0.305
strKeyPath = "SOFTWARE\Classes\Installer\Products\0418CB86AAF391446BEECC6FB06EAD2B"
if KeyExists(HKEY_LOCAL_MACHINE, strKeyPath) = True then
   DeleteSubkeys HKEY_LOCAL_MACHINE, strKeypath 
end if



Sub DeleteSubkeys(HKEY_LOCAL_MACHINE, strKeyPath) 
 objRegistry.EnumKey HKEY_LOCAL_MACHINE, strKeyPath, arrSubkeys 

 If IsArray(arrSubkeys) Then 
   For Each strSubkey In arrSubkeys 
   DeleteSubkeys HKEY_LOCAL_MACHINE, strKeyPath & "\" & strSubkey 
   Next 
 End If 

 objRegistry.DeleteKey HKEY_LOCAL_MACHINE, strKeyPath 
End Sub


Function KeyExists(Key, KeyPath)
 Dim oReg: Set oReg = GetObject("winmgmts:!root/default:StdRegProv")
   If oReg.EnumKey(Key, KeyPath, arrSubKeys) = 0 Then
     KeyExists = True
   Else
     KeyExists = False
   End If
End Function

Along with uninstall_flash_player.exe http://download.macromedia.com/get/flashplayer/current/support/uninstall_flash_player.exe

And an mms.cfg file to stop auto updates I managed to get it complete successfully, and now all 1000 windows 7 machines are upgrading fine 🙂

 

Here is the mms.cfg file contents

SilentAutoUpdateEnable=0
AutoUpdateDisable=1

 

And the batch file that is run that carries out all the above actions.

@echo off
"%~dp0uninstall_flash_player.exe" -uninstall
@call cscript "%~dp0removeRegTraces.vbs"
msiexec /i "%~dp0install_flash_player_22_active_x.msi" /qn
del /s c:\Windows\System32\Macromed\Flash\mms.cfg
copy /y "%~dp0mms.cfg" c:\Windows\System32\Macromed\Flash

 

upgrade folder contents

adobe upgrade

Stop network adapter going to sleep on Windows 10

We were experiencing issues with the wireless network adapter going to sleep when not in use, and wanted to make sure it stays awake all the time.

There is a tickbox on the network adapter that needed to be unticked on the wireless card and also the network port on the laptop. The setting is Allow the computer to turn off this device to save power. Rather than doing this manually every time a laptop is rebuilt I found a powershell script and modified it to suit our needs.

screenshot

This is the original script that disables power saving on all Intel network cards.

$nics = Get-WmiObject Win32_NetworkAdapter | where {$_.Name.Contains('Intel')}

foreach ($nic in $nics)
{
    $powerMgmt = Get-WmiObject MSPower_DeviceEnable -Namespace root\wmi | where {$_.InstanceName.Contains($nic.PNPDeviceID)}
    $powerMgmt.Enable = $False
    $powerMgmt.psbase.Put()
}

 

I altered it to suit our needs and now the powershell script runs and outputs a log file.  It searches for all 4 of the network adapters we want to change, makes sure it can disable Power Management on that network adapter, then checks if power management is enabled and if it is then it disables it.

$file = "C:\NICpowerChange.log"
"Searching for AX88772A / Thinkpad / Lenovo / Intel" | Add-Content -Path $file

#find relevant network adapters
$nics = Get-WmiObject Win32_NetworkAdapter | where {$_.Name.Contains('AX88772A') -or $_.Name.Contains('Thinkpad') -or $_.Name.Contains('Lenovo') -or $_.Name.Contains('Intel')}

$nicsFound = $nics.Count
"number of network adapters found: ", $nicsFound | Add-Content -Path $file

foreach ($nic in $nics)
{
   $powerMgmt = Get-WmiObject MSPower_DeviceEnable -Namespace root\wmi | where {$_.InstanceName -match [regex]::Escape($nic.PNPDeviceID)}
 
   # check to see if power management can be turned off
   if(Get-Member -inputobject $powerMgmt -name "Enable" -Membertype Properties){

     # check if powermanagement is enabled
     if ($powerMgmt.Enable -eq $True){
       $nic.Name, "----- Enabled method exists. PowerSaving is set to enabled, disabling..." | Add-Content -Path $file
       $powerMgmt.Enable = $False
       $powerMgmt.psbase.Put()
     }
     else
     {
       $nic.Name, "----- Enabled method exists. PowerSaving is already set to disabled. skipping..." | Add-Content -Path $file
     }
   }
   else
   {
     $nic.Name, "----- Enabled method doesnt exist, so PowerSaving cannot be set" | Add-Content -Path $file 
   }
}

Error installing OneDrive for Business Next Generation Sync Client

We wanted to install the latest and greatest OneDrive for Business Next Generation Sync Client in the hope it will fix the numerous support calls we receive about syncing issues from our staff.
We have the Office 2013 365 OneDrive for Business already installed but ran into a lot of trouble trying to the Next Gen client installed. We saw the following errors (amongst others) in the logs

!ERROR! (0x80070005) (fsmanagerimpl.cpp:470) ERROR: “” failed with 0x80070005 in .

LogsUploader: Unable to resolve upload host: 11001

ERROR: “” failed with 0x80040b01 in
After some searching around it seems a group policy was stopping it install. The group policy stops the personal version of OneDrive in our corporate environment but it seems this
was stopping the Next Generation client installing.

The policy is
Computer Configuration – Polices – Administrative Templates – Windows Components – OneDrive
“Prevent usage of onedrive for file storage” = Disabled

This was set to Enabled and was causing our errors.

To quickly prove this we tweaked the corresponding registry key located here: HKLM\Software\Policies\Microsoft\Windows\OneDrive – DisableFileSyncNGSC and set it to the value 0 (zero)

We also came across some registry settings that we used to customise the user experience of the new client.

HKEY_CURRENT_USER\SOFTWARE\Microsoft\OneDrive

Create a DWORD key with value name “DefaultToBusiness” with value data 1
Create a DWORD key with value name “EnableAddAccounts” with value data 1
Create a DWORD key with value name “DisablePersonalSync” with value data 1