Unquoted Service Paths

Manual and Automated Process to resolve Unquote Service Path issues

The Risk

The remote Windows host contains services installed that use unquoted service paths, which contains at least one whitespace. A local attacker can gain elevated privileges by inserting an executable file in the path of the affected service.

The Fix

  1. Open the registry editor in Administrator Mode
  2. Goto HKLM\System\CurrentControlSet\Services
  3. Locate the service which has been highlighted as the issue

    e.g.

    • OpenVPNConnectorService
      Value name: ImagePath
      Value data: C:\Program Files\OpenVPN Connect\ovpnconnector.exe run
  4. Enclose the path in quote marks

    e.g.

    • OpenVPNConnectorService
      Value name: ImagePath
      Value data: "C:\Program Files\OpenVPN Connect\ovpnconnector.exe" run

Also

You can search for any "Unquoted Path" issues using the following PowerShell command.

$pat='^\s*(?:"(?<bin>[^"]+)"|(?<bin>\S+))'; Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq 'Auto' -and $_.PathName -and $_.PathName -notmatch '(?i)\\Windows\\|%SystemRoot%\\' -and ($_.PathName -match $pat) -and ($Matches['bin'] -match '\s') -and ($_.PathName.TrimStart() -notlike '"*') } | Select-Object Name,DisplayName,PathName,StartMode | Format-Table -AutoSize

You can also run a script to modify any identified paths, either as a one-time task or as a recurring task within an RMM tool or similar. Note the below with a -WhatIf parameter will advise on what it found and "would have completed".

<# 
    Fix-UnquotedServicePaths.ps1
    - Quotes unquoted service ImagePath binaries that contain spaces.
    - Preserves REG_EXPAND_SZ vs REG_SZ.
    - Run as Administrator (self-elevates). Use -WhatIf first to preview.
#>

param([switch]$WhatIf)

# --- Self-elevate if not admin ---
function Ensure-Administrator {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    $p  = New-Object Security.Principal.WindowsPrincipal($id)
    if (-not $p.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) {
        Write-Host "Re-launching with administrative privileges..."
        $argsList = @('-NoProfile','-ExecutionPolicy','Bypass','-File',"`"$PSCommandPath`"")
        if ($WhatIf) { $argsList += '-WhatIf' }
        Start-Process -FilePath 'powershell.exe' -ArgumentList $argsList -Verb RunAs
        exit
    }
}
Ensure-Administrator

Write-Host "Running in Admin Mode" -ForegroundColor Yellow
Write-Host "Checking for unquoted service ImagePath values..." -ForegroundColor Cyan

$svcKeys = Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' -ErrorAction SilentlyContinue
if (-not $svcKeys) { Write-Host "No services found."; exit }

# Regex: capture a quoted or unquoted binary, then any remaining arguments
#   bin = executable path (quoted or first token)
#   args = remainder (may be empty)
$pattern = '^\s*(?:"(?<bin>[^"]+)"|(?<bin>\S+))\s*(?<args>.*)$'

$changed = 0
foreach ($k in $svcKeys) {
    # Read ImagePath (REG_SZ or REG_EXPAND_SZ)
    $raw = $null
    try { $raw = [string](Get-ItemPropertyValue -Path $k.PSPath -Name ImagePath -ErrorAction Stop) } catch { }
    if ([string]::IsNullOrWhiteSpace($raw)) { continue }

    # Parse into binary + args (case-insensitive)
    if ($raw -notmatch $pattern) { continue }
    $bin  = $Matches['bin']
    $args = $Matches['args']

    # Only touch if the executable path contains spaces AND is not already correctly quoted
    $needsQuote = $bin -match '\s'
    if (-not $needsQuote) { continue }

    $new = '"' + $bin + '"'
    if ($args) { $new += ' ' + $args.Trim() }

    # If effectively identical, skip
    if ($new.Trim() -eq $raw.Trim()) { continue }

    # Preserve value kind (REG_EXPAND_SZ vs REG_SZ) when writing
    try {
        $regKey = (Get-Item -Path $k.PSPath).OpenSubKey('', $true)
        $kind   = $regKey.GetValueKind('ImagePath')
    } catch {
        Write-Warning "[$($k.PSChildName)] Unable to read registry value kind: $($_.Exception.Message)"
        continue
    }

    Write-Host "[$($k.PSChildName)]" -ForegroundColor Gray
    Write-Host "  Old: $raw"
    Write-Host "  New: $new"

    try {
        $setParams = @{
            Path        = $k.PSPath
            Name        = 'ImagePath'
            Value       = $new
            ErrorAction = 'Stop'
        }
        if ($kind -eq [Microsoft.Win32.RegistryValueKind]::ExpandString) {
            $setParams['Type'] = 'ExpandString'
        }
        Set-ItemProperty @setParams -WhatIf:$WhatIf
        $changed++
    }
    catch {
        Write-Warning "  Failed to update: $($_.Exception.Message)"
    }
}

if ($WhatIf) {
    $msg = if ($changed -eq 1) {
        "`nDry run complete. Would update 1 entry. Use without -WhatIf to apply."
    } else {
        "`nDry run complete. Would update $changed entries. Use without -WhatIf to apply."
    }
    Write-Host $msg -ForegroundColor Yellow
} else {
    $msg = if ($changed -eq 1) {
        "`nCompleted. Updated 1 entry."
    } else {
        "`nCompleted. Updated $changed entries."
    }
    Write-Host $msg -ForegroundColor Green
}