Unquoted Search Path or Element
Description
Unquoted Search Path or Element occurs when software uses a search path that contains an unquoted element, and that element contains whitespace or other separators. On Windows systems, this commonly manifests when service executables or scheduled tasks are configured with paths containing spaces but without proper quoting. Attackers can exploit this by placing malicious executables in locations that match the truncated path, causing them to execute with elevated privileges.
Risk
Unquoted service paths are a common Windows privilege escalation vector. When a path like "C:\Program Files\My App\service.exe" is unquoted, Windows tries to execute "C:\Program.exe", then "C:\Program Files\My.exe" before finding the correct executable. If attackers can write to C:\ or C:\Program Files, they can plant a malicious executable that runs with the service's privileges (often SYSTEM). This vulnerability affects thousands of third-party applications and is frequently found in penetration tests.
Solution
Always quote paths containing spaces in service configurations, scheduled tasks, and registry entries. Use the full quoted path: "C:\Program Files\My App\service.exe". Audit existing services for unquoted paths using tools like PowerUp or manual registry inspection. Remove write permissions from directories in the Windows path hierarchy. Install applications to paths without spaces when possible. Implement proper access controls on system directories.
Common Consequences
| Impact | Details |
|---|---|
| Access Control | Scope: Privilege Escalation Attackers can execute code with elevated privileges (often SYSTEM). |
| Integrity | Scope: System Compromise Malicious code runs in place of legitimate service executables. |
| Availability | Scope: Service Disruption Legitimate services fail to start when hijacked. |
Example Code + Solution Code
Vulnerable Configuration
# VULNERABLE: Unquoted service path in Windows
# Service configured with:
# ImagePath = C:\Program Files\My Application\service.exe
# Windows tries these paths in order:
# 1. C:\Program.exe
# 2. C:\Program Files\My.exe
# 3. C:\Program Files\My Application\service.exe
# Check for vulnerable services
Get-WmiObject win32_service | Where-Object {
$_.PathName -like '* *' -and
$_.PathName -notlike '"*"' -and
$_.PathName -notlike '* -*'
} | Select-Object Name, PathName, StartMode, State
# VULNERABLE: Scheduled task with unquoted path
schtasks /create /tn "MyTask" /tr C:\Program Files\App\task.exe /sc daily /st 09:00
# VULNERABLE: Registry entry
# HKLM\SYSTEM\CurrentControlSet\Services\MyService
# ImagePath: C:\Program Files\Vulnerable App\svc.exe
// VULNERABLE: C/C++ CreateProcess without quotes
#include <windows.h>
void StartServiceVulnerable() {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Unquoted path - vulnerable!
CreateProcess(
NULL,
"C:\\Program Files\\My App\\service.exe", // Unquoted!
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi
);
}
// VULNERABLE: ShellExecute with unquoted path
void LaunchAppVulnerable() {
ShellExecute(
NULL,
"open",
"C:\\Program Files\\My App\\launcher.exe", // Unquoted!
NULL, NULL, SW_SHOW
);
}
// VULNERABLE: C# service installer without quotes
using System.ServiceProcess;
public class VulnerableServiceInstaller : Installer
{
public VulnerableServiceInstaller()
{
var processInstaller = new ServiceProcessInstaller();
var serviceInstaller = new ServiceInstaller();
// Unquoted path!
serviceInstaller.ServiceName = "MyService";
// Path set in registry will be unquoted
}
}
// VULNERABLE: Process.Start with unquoted path
public void LaunchVulnerable()
{
// If path has spaces and isn't quoted...
Process.Start("C:\\Program Files\\My App\\app.exe");
}
Exploitation Example
# Attacker exploitation steps:
# 1. Find vulnerable services
$services = Get-WmiObject win32_service | Where-Object {
$_.PathName -match '^[^"].*\s.*[^"]$'
}
# 2. Check if we can write to interceptable paths
# For path: C:\Program Files\Vulnerable App\service.exe
# Check: C:\Program.exe (need write to C:\)
# Check: C:\Program Files\Vulnerable.exe (need write to C:\Program Files\)
icacls "C:\Program Files"
# 3. If writable, place malicious executable
# Copy malicious.exe to C:\Program Files\Vulnerable.exe
# 4. Restart service or wait for system reboot
# Malicious code runs as SYSTEM!
Fixed Configuration
# SAFE: Properly quoted service path
# ImagePath = "C:\Program Files\My Application\service.exe"
# Fix existing services via registry
$servicePath = 'HKLM:\SYSTEM\CurrentControlSet\Services\MyService'
$currentPath = (Get-ItemProperty $servicePath).ImagePath
if ($currentPath -notlike '"*"') {
$quotedPath = '"' + $currentPath + '"'
Set-ItemProperty -Path $servicePath -Name ImagePath -Value $quotedPath
}
# SAFE: Create scheduled task with quoted path
schtasks /create /tn "MyTask" /tr '"C:\Program Files\App\task.exe"' /sc daily /st 09:00
# PowerShell script to fix all unquoted paths
$services = Get-WmiObject win32_service | Where-Object {
$_.PathName -like '* *' -and
$_.PathName -notlike '"*"'
}
foreach ($service in $services) {
$name = $service.Name
$path = $service.PathName
# Extract executable path (handle arguments)
if ($path -match '^(.+\.exe)') {
$exePath = $matches[1]
$args = $path.Substring($exePath.Length)
$fixedPath = '"' + $exePath + '"' + $args
Write-Host "Fixing: $name"
Write-Host " Old: $path"
Write-Host " New: $fixedPath"
# Update registry
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$name"
Set-ItemProperty -Path $regPath -Name ImagePath -Value $fixedPath
}
}
// SAFE: C/C++ CreateProcess with quoted path
#include <windows.h>
void StartServiceSafe() {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Properly quoted path
CreateProcess(
NULL,
"\"C:\\Program Files\\My App\\service.exe\"", // Quoted!
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi
);
}
// SAFE: Using lpApplicationName parameter
void StartServiceSafer() {
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
// Using lpApplicationName avoids path parsing issues
CreateProcess(
"C:\\Program Files\\My App\\service.exe", // Full path here
NULL, // Or command line with arguments
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi
);
}
// SAFE: Helper function for quoting paths
char* QuotePath(const char* path) {
if (path == NULL) return NULL;
// Check if already quoted
if (path[0] == '"') return strdup(path);
// Check if quoting needed (contains spaces)
if (strchr(path, ' ') == NULL) return strdup(path);
// Add quotes
size_t len = strlen(path) + 3; // 2 quotes + null
char* quoted = malloc(len);
snprintf(quoted, len, "\"%s\"", path);
return quoted;
}
// SAFE: C# service installer with quoted path
using System.ServiceProcess;
using System.Configuration.Install;
using Microsoft.Win32;
[RunInstaller(true)]
public class SafeServiceInstaller : Installer
{
public SafeServiceInstaller()
{
var processInstaller = new ServiceProcessInstaller
{
Account = ServiceAccount.LocalSystem
};
var serviceInstaller = new ServiceInstaller
{
ServiceName = "MyService",
DisplayName = "My Service",
StartType = ServiceStartMode.Automatic
};
Installers.Add(processInstaller);
Installers.Add(serviceInstaller);
}
public override void Install(IDictionary stateSaver)
{
base.Install(stateSaver);
// Ensure path is quoted in registry
using (var key = Registry.LocalMachine.OpenSubKey(
@"SYSTEM\CurrentControlSet\Services\MyService", true))
{
if (key != null)
{
var imagePath = key.GetValue("ImagePath") as string;
if (!string.IsNullOrEmpty(imagePath) && !imagePath.StartsWith("\""))
{
// Quote the path
key.SetValue("ImagePath", $"\"{imagePath}\"");
}
}
}
}
}
// SAFE: Process.Start with proper quoting
public void LaunchSafe(string path)
{
var startInfo = new ProcessStartInfo
{
FileName = path, // .NET handles quoting internally
UseShellExecute = false
};
Process.Start(startInfo);
}
// SAFE: Helper to ensure quoted paths
public static string EnsureQuotedPath(string path)
{
if (string.IsNullOrEmpty(path)) return path;
// Already quoted
if (path.StartsWith("\"") && path.EndsWith("\"")) return path;
// Needs quoting (contains spaces)
if (path.Contains(" "))
{
return $"\"{path}\"";
}
return path;
}
# Detection and remediation script
import winreg
import re
def find_unquoted_service_paths():
"""Find services with unquoted paths containing spaces."""
vulnerable = []
services_key = r"SYSTEM\CurrentControlSet\Services"
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, services_key) as key:
i = 0
while True:
try:
service_name = winreg.EnumKey(key, i)
service_path = f"{services_key}\\{service_name}"
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, service_path) as svc_key:
try:
image_path, _ = winreg.QueryValueEx(svc_key, "ImagePath")
# Check if unquoted and contains spaces
if ' ' in image_path and not image_path.startswith('"'):
# Extract executable path
exe_match = re.match(r'^(.+\.exe)', image_path, re.IGNORECASE)
if exe_match:
exe_path = exe_match.group(1)
if ' ' in exe_path:
vulnerable.append({
'name': service_name,
'path': image_path
})
except FileNotFoundError:
pass
i += 1
except OSError:
break
except Exception as e:
print(f"Error: {e}")
return vulnerable
def fix_unquoted_path(service_name):
"""Fix unquoted service path."""
service_path = f"SYSTEM\\CurrentControlSet\\Services\\{service_name}"
try:
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, service_path,
0, winreg.KEY_READ | winreg.KEY_WRITE) as key:
image_path, _ = winreg.QueryValueEx(key, "ImagePath")
if not image_path.startswith('"'):
# Find executable and arguments
exe_match = re.match(r'^(.+\.exe)(.*)', image_path, re.IGNORECASE)
if exe_match:
exe_path = exe_match.group(1)
args = exe_match.group(2)
fixed_path = f'"{exe_path}"{args}'
winreg.SetValueEx(key, "ImagePath", 0, winreg.REG_EXPAND_SZ, fixed_path)
print(f"Fixed: {service_name}")
print(f" Old: {image_path}")
print(f" New: {fixed_path}")
return True
except Exception as e:
print(f"Error fixing {service_name}: {e}")
return False
# Run detection
if __name__ == "__main__":
print("Scanning for unquoted service paths...\n")
vulnerable = find_unquoted_service_paths()
for svc in vulnerable:
print(f"[VULNERABLE] {svc['name']}")
print(f" Path: {svc['path']}\n")
print(f"\nFound {len(vulnerable)} vulnerable services")
Exploited in the Wild
Common Penetration Test Finding
Unquoted service paths are consistently among the most common privilege escalation vectors found in Windows enterprise environments.
Third-Party Software Vulnerabilities
Many commercial applications install services with unquoted paths, including security software, backup solutions, and enterprise management tools.
Malware Persistence
Malware families have used unquoted service path exploitation for persistence and privilege escalation.
Tools to test/exploit
-
PowerUp — PowerShell privilege escalation tool.
-
WinPEAS — Windows privilege escalation scanner.
-
Metasploit — exploit/windows/local/unquoted_service_path.
-
BeRoot — privilege escalation tool.
CVE Examples
-
CVE-2019-6453 — Mattermost unquoted service path.
-
CVE-2018-16157 — Ivanti unquoted service path.
-
CVE-2020-5316 — Dell unquoted service path.
References
-
MITRE. "CWE-428: Unquoted Search Path or Element." https://cwe.mitre.org/data/definitions/428.html
-
Microsoft. "CreateProcess function." https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessa