Privilege Escalation · intermediate · ~12 min
**What you will learn** - Explain why Windows services are a prime local privilege-escalation target because they usually run as `SYSTEM`. - Identify the three classic service misconfigurations: **unquoted service paths**, **weak service permissions**, and **writable service binaries/DLLs**. - Enumerate services safely in an authorized lab with `sc qc`, `accesschk.exe`, and `icacls`, and read the output to tell a misconfiguration from a safe configuration. - Apply the correct **remediation** for each: quoting paths, tightening service DACLs, and fixing NTFS file ACLs — then **verify** each fix actually blocks the escalation. - Choose the Windows event logs and monitoring signals that let a blue team detect service tampering, and know what must never be logged.
Security objective. The asset you are protecting is the SYSTEM (a.k.a. NT AUTHORITY\SYSTEM) integrity level on a Windows host — the highest local privilege. The threat is a local attacker who already has a low-privileged foothold (a standard user account, e.g. from phishing or a web-app shell) and wants to become SYSTEM without an exploit, just by abusing how a service is configured. By the end you will be able to detect these misconfigurations before an attacker does and prevent the escalation.
A Windows service is a background program managed by the Service Control Manager (SCM). Most services run under a highly privileged account — LocalSystem, LocalService, or NetworkService — because they need to touch hardware, the registry, or other users' data. That is exactly why they are dangerous: if a low-privileged user can influence what code a SYSTEM service runs, that code runs as SYSTEM.
This lesson builds directly on your prereqs. From Windows services you already know what a service is, that it has a binary path (binPath), a start type, and a service account, and that sc.exe and services.msc manage them. From Privilege escalation: the enumeration-first mindset you know the golden rule: enumerate before you act — you map the terrain (what services exist, who can change them, what files you can write) before touching anything. Here we apply that mindset to three specific, extremely common weaknesses and — most importantly — to fixing them.
Everything below is written for an authorized lab: your own Windows VM, a course-provided target, or a CTF box. Never run these techniques against a machine you do not own or have explicit written permission to test.
Service misconfigurations are consistently among the most common local privilege-escalation paths on Windows, and they need no memory-corruption exploit, no shellcode, and no zero-day — just built-in tools and a writable path. That makes them reliable for attackers and, crucially, cheap to fix for defenders.
In authorized professional work these skills matter on both sides of the table:
VulnSvc allows any authenticated user to gain SYSTEM" is concrete, reproducible, and actionable.Understanding why each misconfiguration escalates — not just how to trigger it — is what lets you write a correct fix and prove it works, which is the whole job.
Windows services run as SYSTEM and are controlled by the Service Control Manager (SCM). Three independent misconfigurations each let a low-privileged user control what a SYSTEM service executes. We teach each one, then the enumeration and remediation that ties them together.
Definition. A service whose binary path contains spaces but is not wrapped in quotes, e.g. binPath = C:\Program Files\My App\service.exe.
Plain explanation. When the SCM launches a service, it parses the path left to right. Because a space normally separates a program from its arguments, an unquoted path with spaces is ambiguous. Windows tries each interpretation in order:
C:\Program.exeC:\Program Files\My.exeC:\Program Files\My App\service.exeHow it works. The SCM runs the first file it finds. If a low-privileged user can create C:\Program.exe (the root of C:\ is sometimes writable by non-admins), the SCM runs *their* binary as SYSTEM on next service start.
When it applies / when not. Only when the path has a space and is unquoted and an earlier interpretation points to a directory the attacker can write to. A quoted path, or a path with no spaces, or one where every earlier directory is protected, is not exploitable.
Pitfall. Testers often flag every unquoted path as "critical." It is only exploitable if you can actually write to an earlier segment. Without a writable earlier directory it is a hygiene finding, not an escalation.
Definition. A service object whose DACL (Discretionary Access Control List) grants a low-privileged user rights such as SERVICE_CHANGE_CONFIG (SC_MANAGER / WRITE on the service).
Plain explanation. Every service is a securable object with its own permissions, separate from the file on disk. If you hold SERVICE_CHANGE_CONFIG, you can rewrite the service's binPath to point at any program you like.
How it works. You reconfigure and restart the service:
sc config vulnsvc binPath= "C:\lab\payload.exe"
sc stop vulnsvc
sc start vulnsvc
On start, the SCM launches your program as the service account (SYSTEM). In a lab your payload.exe might simply add your user to the local Administrators group — never real malware.
When it applies / when not. Only when your specific user or a group you belong to (e.g. Authenticated Users, Everyone, INTERACTIVE) is granted config-changing rights. Read-only rights (SERVICE_QUERY_CONFIG) do not escalate.
Pitfall. Confusing service-object permissions with file permissions. They are different ACLs; a service can have a locked-down binary but a wide-open service DACL, and vice versa.
Definition. The service's .exe, or a DLL it loads, sits in a location the low-privileged user can overwrite (an NTFS ACL problem), or the service searches for a DLL in a directory the user can write to.
Plain explanation. If you can replace the file the SYSTEM service executes, the service runs your code next time it starts. DLL hijacking is the same idea one level down: if a service loads helper.dll by name and searches a writable directory first, you drop your own helper.dll there.
How it works. Overwrite the binary (or plant the DLL), then wait for or trigger a restart. The Windows DLL search order means a service that loads a library without an absolute path may pick up an attacker-planted DLL from its working directory or a writable PATH entry.
When it applies / when not. Only when the file or a search-path directory is writable by your user and you can cause a restart (reboot, scheduled restart, or you hold start/stop rights). Correct ACLs make this impossible.
Pitfall. Assuming "the file is in C:\Program Files, so it's safe." Installers sometimes loosen ACLs on their own subfolder. Always check the actual ACL, not the location.
You never guess — you enumerate:
sc qc <service> — shows BINARY_PATH_NAME (spot unquoted paths) and the service account.accesschk.exe -uwcqv <user> * (Sysinternals) — lists services your user can modify (SERVICE_CHANGE_CONFIG, SERVICE_ALL_ACCESS). The -w flag limits output to write access.icacls "C:\Path\to\service.exe" — shows NTFS permissions on the binary (look for (M)/(F)/(W) granted to your user or broad groups).THREAT MODEL — Windows service privilege escalation
ASSET: NT AUTHORITY\SYSTEM integrity (full host control)
+-------------------------------------------------------------+
| Windows host (authorized lab VM) |
| |
| [Low-priv user shell] <-- attacker foothold |
| | |
| =======|=========== TRUST BOUNDARY ====================== |
| | (standard user --> SYSTEM) |
| v |
| Entry points controlled by the SCM: |
| (1) Unquoted binPath -> writable earlier dir |
| (2) Weak service DACL -> sc config binPath |
| (3) Writable exe/DLL -> overwrite / DLL hijack |
| | |
| v |
| [Service Control Manager] --starts--> chosen code |
| | |
| v |
| Runs as SYSTEM ==> asset compromised |
+-------------------------------------------------------------+
Defender goal: keep every entry point closed so crossing the
trust boundary is impossible, and log attempts that try.
Knowledge check.
C:\, C:\Program Files) denies write to standard users. Which insecure assumption is absent here, and why is the finding therefore not an escalation?sc config to repoint a service's binPath (think about the earlier list of enumeration/remediation tools), and why is that event rare enough to alert on?The key commands are all built-in or Sysinternals tools; none of them are exploits by themselves — they are the same commands admins use to audit and fix services. Annotated, lab-safe:
sc qc <service>
# ^ ^ ^
# | | +-- service name (short name, not display name)
# | +----- "query config": prints BINARY_PATH_NAME, START_TYPE,
# | SERVICE_START_NAME (the account it runs as)
# +-------- Service Control tool (built in)
accesschk.exe -uwcqv <username> *
# | | | | | ^ wildcard: check all services
# | | | | +--- v: verbose (list granted rights)
# | | | +----- q: omit banner
# | | +------- c: treat name as a Windows service
# | +--------- w: only objects with WRITE access
# +----------- u: suppress errors
# => lists services <username> can RECONFIGURE (the danger set)
icacls "C:\Program Files\My App\service.exe"
# => prints NTFS ACL. Look for your user or a broad group with
# (M) Modify, (F) Full, or (W) Write. Those are writable-binary hits.
Remediation commands (what you run as an admin to fix each issue):
# Fix 1 - quote an unquoted path:
sc config vulnsvc binPath= "\"C:\Program Files\My App\service.exe\""
# note: a space is REQUIRED after 'binPath=' (sc.exe quirk)
# Fix 2 - reset a weak service DACL to a known-good SDDL:
sc sdset vulnsvc "D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)"
# grants full control to SYSTEM (SY) and Admins (BA); read/query only
# to Interactive (IU) and Service (SU) users -> no CHANGE_CONFIG for them
# Fix 3 - remove a wide grant from a service binary's NTFS ACL:
icacls "C:\Program Files\My App\service.exe" /remove:g "Users"
icacls "C:\Program Files\My App\service.exe" /inheritance:e
Windows services usually run as SYSTEM, so controlling what a service executes means SYSTEM. Three classic service misconfigurations:
A service binary path with spaces and no quotes:
C:\Program Files\My App\service.exe
Windows tries C:\Program.exe, then C:\Program Files\My.exe, etc. If you can write to an earlier location (e.g. C:\), your binary runs as SYSTEM. Fix: quote the path.
If your user has SERVICE_CHANGE_CONFIG on a service, you can repoint its binary:
sc config vulnsvc binPath= "C:\Users\you\evil.exe"
then restart it → SYSTEM. Audited with accesschk. Fix: correct service DACLs.
If the service's .exe or a DLL it loads is writable by your user (an NTFS ACL problem), replace it. DLL hijacking also applies when a service loads a DLL from a writable directory in its search order. Fix: correct file ACLs; have services load DLLs from absolute, protected paths.
sc qc <svc>, accesschk.exe -uwcqv <user> *, and WinPEAS surface these. Each turns into SYSTEM via the service restarting.
The safest way to learn this is the insecure -> secure -> verify loop on your own VM. Everything here is lab-only.
The snippet below creates a deliberately misconfigured service so you can study all three problems at once, then detect them. Run it as admin only inside a throwaway Windows VM with no network access.
# LAB SETUP (run as admin in an isolated VM). Creates a weak service.
# 1) Unquoted path WITH spaces:
sc.exe create VulnSvc binPath= "C:\Program Files\Lab App\svc.exe" start= demand
# 2) Weaken the service DACL so 'Authenticated Users' can reconfigure it
# (this is the misconfiguration an installer might create by accident):
sc.exe sdset VulnSvc "D:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)"
# 3) Loosen NTFS ACL on the folder so standard users can write the binary:
icacls "C:\Program Files\Lab App" /grant "Authenticated Users:(OI)(CI)M"
C:\> sc qc VulnSvc
[SC] QueryServiceConfig SUCCESS
BINARY_PATH_NAME : C:\Program Files\Lab App\svc.exe <-- UNQUOTED + spaces
SERVICE_START_NAME : LocalSystem <-- runs as SYSTEM
C:\> accesschk.exe -uwcqv authusers VulnSvc
RW VulnSvc
SERVICE_CHANGE_CONFIG <-- weak DACL: reconfigurable
C:\> icacls "C:\Program Files\Lab App"
C:\Program Files\Lab App Authenticated Users:(OI)(CI)(M) <-- writable binary path
All three red flags confirmed by enumeration, not by attacking anything.
# Run as admin.
# Fix 1: quote the binary path.
sc.exe config VulnSvc binPath= "\"C:\Program Files\Lab App\svc.exe\""
# Fix 2: reset the service DACL so only SYSTEM + Admins can change config;
# normal users get read/query only.
sc.exe sdset VulnSvc "D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)"
# Fix 3: remove the over-broad write grant and re-enable inheritance from
# the protected 'Program Files' parent.
icacls "C:\Program Files\Lab App" /remove:g "Authenticated Users"
icacls "C:\Program Files\Lab App" /inheritance:e
# Bad input is now REJECTED:
C:\> sc qc VulnSvc | findstr BINARY_PATH_NAME
BINARY_PATH_NAME : "C:\Program Files\Lab App\svc.exe" <-- quoted: no earlier-path hijack
C:\> accesschk.exe -uwcqv authusers VulnSvc
No matching objects found. <-- user can no longer reconfigure
C:\> icacls "C:\Program Files\Lab App"
C:\Program Files\Lab App BUILTIN\Users:(OI)(CI)(RX) <-- read+execute only, no write
# Good input still WORKS: the service starts and runs normally as SYSTEM.
C:\> sc start VulnSvc
[SC] StartService SUCCESS
Expected outcome. After remediation, sc qc shows a quoted path, accesschk reports "No matching objects found" for a standard user, and icacls shows read/execute only — yet the service still starts and functions. That combination is the proof that the fixes closed the escalation without breaking the service.
# Run as admin to fully remove the lab service and folder.
sc.exe stop VulnSvc
sc.exe delete VulnSvc
Remove-Item -Recurse -Force "C:\Program Files\Lab App"
Then revert the VM to a clean snapshot so no weakened ACLs linger.
Walking the insecure -> secure -> verify loop step by step:
sc.exe create VulnSvc binPath= "C:\Program Files\Lab App\svc.exe" — registers a service whose stored path has spaces and no inner quotes. The SCM will later parse this ambiguously.sc.exe sdset ... AU ... — rewrites the service's security descriptor (SDDL) so the AU (Authenticated Users) SID gets config-change rights. This is Concept 2's weak DACL, injected on purpose.icacls ... /grant "Authenticated Users:(OI)(CI)M" — grants Modify (M) on the folder to all authenticated users. (OI)(CI) make it inherit to files and subfolders, so the .exe is now writable — Concept 3.sc qc VulnSvc — enumeration output. The key line is BINARY_PATH_NAME with no surrounding quotes and a space after Program. SERVICE_START_NAME : LocalSystem confirms the payoff would be SYSTEM.accesschk -uwcqv authusers VulnSvc — prints SERVICE_CHANGE_CONFIG, proving a standard user could run sc config to repoint the binary. This is the evidence a tester records.icacls "...Lab App" — shows Authenticated Users:(M) on the path, confirming the writable-binary hit.Now the fixes, and how each value changes:
| Step | Command | Before | After | Why the escalation closes |
|---|---|---|---|---|
| Fix 1 | sc config ... binPath= "\"...\"" |
C:\Program Files\... (unquoted) |
"C:\Program Files\..." (quoted) |
SCM no longer parses C:\Program.exe; only the real binary can run |
| Fix 2 | sc sdset (no AU entry) |
AU has CHANGE_CONFIG |
AU absent; IU/SU read-only |
Standard users can't rewrite binPath |
| Fix 3 | icacls /remove:g + /inheritance:e |
Authenticated Users:(M) |
Users:(RX) inherited |
Binary/DLL can't be overwritten; no hijack |
(RX)), while sc start VulnSvc still succeeds — the service works, the escalation does not. Re-running the attacker's enumeration as the defender's proof is the core discipline: a fix you cannot verify is not a fix.Real mistakes learners and even professionals make:
| Wrong approach | Why it's wrong | Corrected approach | How to recognise / prevent |
|---|---|---|---|
| Flagging every unquoted path as "critical SYSTEM escalation" | Exploitability depends on a writable earlier directory; most unquoted paths under C:\Program Files are not writable by standard users |
Confirm write access with icacls C:\ and each earlier segment before rating it; otherwise it's a low/hygiene finding |
Test the assumption; severity = exploitability + impact, not pattern-match |
Quoting the path but forgetting the special sc.exe syntax |
binPath= needs a space after the = and escaped inner quotes; a malformed command silently sets a broken path and the service won't start |
Use binPath= "\"C:\...\"" exactly, then sc qc to confirm |
Always re-query after any sc config change |
| Fixing the file ACL but ignoring the service DACL (or vice versa) | They are two separate ACLs; a locked binary with a wide-open service DACL is still fully exploitable via sc config |
Remediate and verify all three vectors independently | Keep a checklist: path quoting, service DACL, file/DLL ACL |
| Assuming an antivirus/scanner "pass" means the host is secure | Scanners miss custom services and vendor ACL mistakes; a passing scan does not prove security | Manually enumerate services and ACLs; treat scanners as one input, not proof | Never claim a system is "completely secure" |
| Testing on a machine you don't own "just to check" | Unauthorized access is illegal and unethical regardless of intent | Only test on your VM, a lab, or a scope you have written authorization for | Keep the authorization letter/scope handy before any test |
| Leaving the deliberately weak lab service installed | The vulnerable VulnSvc is a real backdoor if the VM is ever exposed |
Run the cleanup/reset steps and revert to a clean snapshot | Snapshot before, revert after |
When enumeration or a fix doesn't behave as expected:
accesschk.exe prints a EULA prompt or nothing. First run needs accesschk.exe /accepteula. If output is empty, you may be checking the wrong principal — try your actual username, "Authenticated Users", Everyone, and Users. Confirm your identity with whoami /groups.sc config returns Access is denied. You don't hold SERVICE_CHANGE_CONFIG on that service — which, from a defender's view, is good. Re-check with accesschk -uwcqv <user> <svc>. From a tester's view, that vector simply isn't available.sc config quoting. Run sc qc <svc> and inspect BINARY_PATH_NAME character by character; the outer value must be quoted and the file must exist at that exact path. Check Get-WinEvent -LogName System for SCM error 7000/7009 (timeout/failure) or 7011.icacls change seems to have no effect. Explicit ACEs and inheritance can conflict. After /remove:g, run icacls <path> again and confirm the broad group is gone; use /inheritance:e to re-enable inheritance from a protected parent, or /reset to restore inherited defaults.sc qc <svc> shows SERVICE_START_NAME. LocalSystem, NT AUTHORITY\SYSTEM, .\LocalSystem all mean SYSTEM. LocalService/NetworkService are lower privilege and change the impact rating.Questions to ask when a fix fails. Did I re-query after changing config? Am I testing as the low-privileged user, not as admin? Are all three vectors closed, or did I fix one and forget another? Does the service still start (did I break function while fixing security)?
Security & safety — detection and logging.
Because these escalations use legitimate administrative actions, detection depends on logging the actions and alerting on the ones that shouldn't happen.
What to log (and alert on):
sc.exe config, sc.exe sdset, and icacls invocations. Repointing a binPath shows up clearly here.SYSTEM process can be tied together..exe or a newly planted DLL.What to NEVER log: passwords, tokens, session cookies, private keys, full credential blobs, or unnecessary PII. When capturing command lines, be aware some tools accept secrets as arguments — scrub or avoid logging those fields.
Which events signal abuse: a standard (non-admin) user account associated with a 7045/4697 service install; a binPath that changes to an unusual location (C:\Users\..., C:\Temp, %TEMP%); a service that stops and immediately restarts right after an sc config (correlate 7040/7045 with 4688); a new SYSTEM process whose parent is services.exe but whose image is in a user-writable path.
How false positives arise: legitimate software updates and installers create and reconfigure services and change ACLs; patch windows produce bursts of 7045/7040. Reduce noise by baselining known installers, correlating with change-management/maintenance windows, and alerting on service changes made by non-administrative or interactive user accounts rather than on service changes in general.
Authorized real-world use case. During an internal penetration test with signed scope, a tester lands a low-privileged shell on a workstation via a phished user. Enumerating services, they find a third-party backup agent installed with an unquoted path under a folder the installer left writable by Authenticated Users. They document the finding — service name, sc qc output, icacls proof, and the SYSTEM service account — rate its severity from actual exploitability, and hand the client a remediation: quote the path, tighten the folder ACL, reset the service DACL, and a retest step. The client's blue team then adds a 7045/4688 alert so any future recurrence is caught.
Professional best-practice habits:
| Habit | Beginner | Advanced |
|---|---|---|
| Validation / enumeration | Run sc qc, accesschk, icacls and read the raw output |
Automate fleet-wide audits (PowerShell/GPO reporting), diff against a known-good baseline |
| Least privilege | Never grant Authenticated Users/Everyone config or write on services |
Design service accounts as gMSA/virtual accounts with only required rights; avoid LocalSystem when a lower account works |
| Secure defaults | Always quote binary paths; install under protected Program Files |
Ship installers that set correct DACLs/ACLs and pass a hardening lint before release |
| Logging / detection | Confirm 7045/7040 are being collected | Central SIEM rules correlating service change -> restart -> new SYSTEM process, tuned for installer noise |
| Error handling | Re-query after every change to confirm it applied | Fail-closed automation that refuses to leave a service half-reconfigured |
Across both levels the discipline is the same: least privilege, secure defaults, verify every change, and log the security-relevant ones.
All tasks are lab-only: use an isolated, snapshot-backed Windows VM (or an authorized course/CTF target) with no production data and, ideally, no network. Before starting any task, confirm the authorization checklist: (1) I own or have written permission for this machine; (2) it is isolated/snapshotted; (3) I will restore it afterward. Each task ends with a defensive conclusion: remediate and verify.
Beginner 1 — Read a service's configuration.
sc qc <service> on three services of your choice.BINARY_PATH_NAME (quoted or not, spaces or not) and SERVICE_START_NAME.sc config command that would quote it.Beginner 2 — Map who can change a service.
accesschk.exe -uwcqv <youruser> * (accept the EULA first).SERVICE_CHANGE_CONFIG or SERVICE_ALL_ACCESS.sc config."Authenticated Users" and Everyone as the principal.sc sdset fix and explain which SID you removed.Intermediate 1 — Build and detect the vulnerable lab service.
VulnSvc from the lesson, then detect all three problems purely by enumeration.sc qc, accesschk, icacls results with the red flags circled.icacls on the folder reveals inherited write access to the .exe.Intermediate 2 — Remediate and verify.
VulnSvc and prove each fix works.CHANGE_CONFIG->"No matching objects found", and (M)->(RX), plus a successful sc start.SYSTEM while the user can no longer influence it is the proof of success.Challenge — Author a remediation finding with retest and detection.
Main concepts. Windows services usually run as SYSTEM, so a low-privileged user who can influence what a service runs gains SYSTEM. Three independent misconfigurations do this: unquoted service paths (a writable earlier path segment gets executed), weak service permissions (SERVICE_CHANGE_CONFIG lets you sc config a new binPath), and writable service binaries/DLLs (overwrite the file or DLL-hijack). Service-object DACLs and NTFS file ACLs are separate and must both be checked.
Key commands. Enumerate with sc qc <svc> (path + account), accesschk.exe -uwcqv <user> * (who can reconfigure), and icacls <path> (writable binary/DLL). Remediate with sc config binPath= "\"...\"" (quote), sc sdset (fix service DACL), and icacls /remove:g + /inheritance:e (fix file ACL). Always re-run the enumeration commands afterward to verify.
Common mistakes. Rating every unquoted path critical without checking write access; fixing one ACL and forgetting the other; malformed sc config quoting; trusting a scanner "pass" as proof of security; leaving the weak lab service installed; and — never acceptable — testing systems you don't own.
What to remember. Enumerate before you act. Severity comes from real exploitability, not a pattern match. Every vulnerability needs a fix and a verification that the fix rejects the attack while the service still works. Log service installs/changes (7045, 7040, 4697, 4688), never log secrets, and alert on service changes made by non-admin users. Nothing is ever "completely secure" — but these three misconfigurations are cheap to close and easy to detect.