Windows Fundamentals · beginner · ~10 min
**What you will learn** - Explain what a Windows service is, how the Service Control Manager (SCM) launches it, and which service accounts (especially `LocalSystem`/SYSTEM) matter for security. - Read a service's real configuration (binary path, start type, account) with `sc qc` and the `services.msc` GUI. - Identify the three classic service misconfigurations that enable local privilege escalation: unquoted service paths, weak service reconfigure rights, and writable service binaries. - Use `accesschk` (Sysinternals) to audit who can modify a service or its executable, and interpret the ACL output. - Apply the defensive fix for each weakness and **verify** the fix with a repeatable check. - Know what to log and monitor so a service being tampered with is detected, not just prevented.
Security objective. The asset you are protecting is the integrity of code that runs as SYSTEM. On Windows, many services run under the all-powerful LocalSystem (SYSTEM) account. The threat is local privilege escalation: a low-privileged user who can influence what a SYSTEM service executes gains full control of the machine. In this lesson you will learn to detect those weaknesses and, more importantly, prevent and verify the fix for them.
A Windows service is a background program run by the operating system rather than by a logged-in user. It has no window, starts before (or without) any user logging in, and is supervised by the Service Control Manager (SCM) — the Windows equivalent of Linux's init/systemd. Every service is defined by three properties that together decide what runs and with what power:
This builds directly on your prerequisite, "Windows users, groups, and SIDs". A service account is just an identity with a SID, and the decision "can this user reconfigure this service?" is an access-control check against that user's SID and group memberships. If SIDs, well-known accounts (SYSTEM, Administrators, Authenticated Users), and ACLs are fuzzy for you, review that lesson first — everything here is an application of those ideas to a specific, high-value object type.
Where you meet this: every Windows workstation and server runs dozens of services, many installed by third-party software. Misconfigured third-party services are one of the most common real-world privilege-escalation paths, which is why service auditing appears early in any authorized Windows security assessment — and why hardening services is a standard part of secure system administration.
In authorized professional work — system hardening, a Windows security assessment, or a compliance audit — services are where a huge share of local privilege-escalation risk lives. Three weaknesses do most of the damage:
| Weakness | What an attacker gains | Root cause |
|---|---|---|
| Unquoted service path with spaces | Windows may run an attacker-planted binary earlier in the path | Missing quotes in the registry ImagePath |
| Weak reconfigure rights on the service | A normal user runs sc config to point the service at their own binary, then restarts it |
Over-permissive service DACL (SERVICE_CHANGE_CONFIG) |
| Writable service binary | The attacker overwrites the .exe the service runs |
Loose NTFS ACL on the executable or its folder |
Each one lets an ordinary user run code as SYSTEM. For a defender or sysadmin, understanding these means you can harden a fleet before an attacker or auditor finds them: quote paths, tighten service DACLs, install binaries into protected directories, and monitor for tampering. For an authorized assessor, enumerating services with sc qc and accesschk is a standard early step — but the value you deliver is the remediation and verification, not the finding alone. A report that says "service X is misconfigured" is worth little; a report that says "here is the exact fix and here is how to confirm it worked" is what gets systems hardened.
Definition. The Windows component (services.exe, exposed through the sc command and services.msc) that starts, stops, and stores the configuration of every service.
How it works. Each service is a registry key under HKLM\SYSTEM\CurrentControlSet\Services\<name>. Key values include ImagePath (the binary path), Start (start type), and ObjectName (the service account). The SCM reads these and launches the process with the specified identity.
When it matters. Any change to those values changes what runs as SYSTEM. Access to change them is controlled by the service's security descriptor (DACL) — an ACL just like the ones on files and registry keys.
Pitfall. People assume "only admins can change a service." Not true — a service's DACL can be set (by a sloppy installer) to grant SERVICE_CHANGE_CONFIG to Authenticated Users or Users. Always check the actual ACL, don't assume.
Definition. The security principal the service process runs under. Common ones:
| Account | Privilege level | Notes |
|---|---|---|
LocalSystem (SYSTEM) |
Highest — full control of the machine | The high-value target |
NetworkService |
Limited local; network as the computer account | |
LocalService |
Limited local; network as anonymous | |
| A named user (e.g. a service account) | Whatever that user has | Depends on configuration |
How it works. Whoever controls the code a SYSTEM service runs effectively is SYSTEM when that code executes.
When not to worry. A service running as LocalService that an attacker hijacks yields only LocalService — still a problem, but not full machine compromise. The account is what turns "run my code" into "run my code as SYSTEM."
Pitfall. Running third-party or line-of-business services as SYSTEM "to avoid permission problems" — this maximizes blast radius. Prefer the least-privileged account that works.
Definition. An ImagePath that contains spaces but no surrounding quotes, e.g. C:\Program Files\My App\service.exe.
How it works. When the path is unquoted, the SCM tries each candidate in turn: C:\Program.exe, then C:\Program Files\My.exe, then the full path. If a user can create C:\Program Files\My.exe (or C:\Program.exe), it runs first — as the service account.
When not exploitable. If no attacker-writable directory sits earlier in the path, it cannot be abused — but it is still a hardening defect worth fixing.
Pitfall. Assuming unquoted always equals exploitable. Exploitability requires write access to an earlier folder. Report accurately.
Definition. A service DACL that grants a low-privileged principal SERVICE_CHANGE_CONFIG (or WRITE_DAC/SERVICE_ALL_ACCESS).
How it works. With change-config rights, a normal user can run sc config <name> binPath= <their exe> and then restart the service; the SCM launches their binary as SYSTEM.
Pitfall. Overlooking SERVICE_CHANGE_CONFIG because it is not called "admin." It is the single most dangerous service right to hand out.
Definition. An NTFS ACL on the service's .exe (or its folder) that lets a low-privileged user modify or replace it.
How it works. The attacker overwrites the executable with their own; on next start, the SCM runs it as the service account. No sc config needed.
Pitfall. Checking only the file and not the folder. Write access to the containing folder often lets an attacker rename/replace the file even if the file's own ACL looks tight.
WINDOWS SERVICE — THREAT MODEL (single host)
TRUST BOUNDARY: standard user <-----> SYSTEM (LocalSystem)
ENTRY POINTS an unprivileged user can touch:
(1) Service DACL -- sc config / change binPath
(2) ImagePath (registry) -- unquoted path -> plant earlier .exe
(3) Service binary / folder-- NTFS write -> overwrite the .exe
|
v
SCM launches the (now attacker-influenced) code
|
v
ASSET: code execution as SYSTEM ==> full host compromise
DEFENDER CONTROLS:
- Quote ImagePath
- Tighten service DACL (remove CHANGE_CONFIG from non-admins)
- Restrict NTFS ACL on binary + folder
- Log 7045 (new service) / 4697 / config changes
Knowledge check.
The core enumeration commands. All are read-only and safe to run on a system you administer or are authorized to assess.
sc query # list running services (state, not full config)
sc query state= all # list ALL services incl. stopped (note the space after '=')
sc qc <ServiceName> # QUERY CONFIG: shows BINARY_PATH_NAME, START_TYPE,
# SERVICE_START_NAME (the account) -- the key command
sc qc "My Service" # quote names that contain spaces
Annotated sc qc output (what each line tells you):
SERVICE_NAME: ExampleSvc
TYPE : 10 WIN32_OWN_PROCESS
START_TYPE : 2 AUTO_START <- launches automatically
BINARY_PATH_NAME : C:\Program Files\App\svc.exe <- UNQUOTED + has a space (red flag)
SERVICE_START_NAME : LocalSystem <- runs as SYSTEM (high value)
Auditing who can do what, using Sysinternals accesschk (download from Microsoft's official Sysinternals site; run with -accepteula):
accesschk.exe -accepteula -uwcqv "Authenticated Users" ExampleSvc
# -u quiet errors, -w writable only, -c service, -q no banner, -v verbose
# Shows whether that group has SERVICE_CHANGE_CONFIG / SERVICE_ALL_ACCESS
accesschk.exe -accepteula -quv "C:\Program Files\App\svc.exe"
# Shows the NTFS ACL on the binary -- look for FILE_WRITE_DATA / (W) for non-admins
Defensive commands (require admin) used later to fix issues:
sc config ExampleSvc binPath= "\"C:\Program Files\App\svc.exe\"" # add quotes
sc sdset ExampleSvc <SDDL> # replace the service DACL
icacls "C:\Program Files\App" /inheritance:r ... # tighten NTFS ACL
A Windows service is a background program managed by the Service Control Manager (SCM). It is the Windows equivalent of a Linux daemon or a systemd unit.
Every service has three properties:
Services run under one of these accounts:
LocalSystem — the same as SYSTEM. All-powerful.LocalService / NetworkService — limited privileges.A service running as SYSTEM is the prize. If you control its code, you become SYSTEM.
The most common misconfigurations (covered in detail in the privesc track):
sc config, then restart the service.Use these to inspect services:
sc query and sc qc <name> — show service configuration from the command line.services.msc — the graphical interface.Listing services, their accounts, and the ACLs on their binaries is core Windows enumeration.
This example shows the INSECURE -> SECURE -> VERIFY shape for the writable service binary weakness, using only safe, standard commands. Run every step in an isolated lab (see the authorization checklist below), never on production.
Suppose a sloppy installer created a service whose binary folder is writable by ordinary users. We first observe the weakness read-only (we do NOT plant a malicious binary — observation is enough to prove the finding):
C:\> sc qc VulnSvc
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: VulnSvc
BINARY_PATH_NAME : C:\vuln\svc.exe
SERVICE_START_NAME : LocalSystem
C:\> accesschk.exe -accepteula -quv "C:\vuln\svc.exe"
C:\vuln\svc.exe
RW BUILTIN\Users <-- any user can WRITE/replace a SYSTEM binary
RW NT AUTHORITY\SYSTEM
The finding: Users has RW on a binary that runs as LocalSystem. That is a privilege-escalation path. We stop at proving it; we do not exploit it.
Run as an administrator. Strip inherited permissions, then grant only what is needed:
C:\> icacls "C:\vuln\svc.exe" /inheritance:r
C:\> icacls "C:\vuln\svc.exe" /grant:r "SYSTEM:(RX)" "Administrators:(F)" "Users:(RX)"
Users now has read/execute (needed to run the service) but not write. Do the same on the containing folder so nobody can rename/replace the file:
C:\> icacls "C:\vuln" /inheritance:r
C:\> icacls "C:\vuln" /grant:r "SYSTEM:(OI)(CI)(F)" "Administrators:(OI)(CI)(F)" "Users:(OI)(CI)(RX)"
Rejects bad input (a non-admin can no longer write the binary). From a standard-user shell:
C:\> echo test > "C:\vuln\svc.exe"
Access is denied.
C:\> accesschk.exe -accepteula -quv "C:\vuln\svc.exe"
C:\vuln\svc.exe
R BUILTIN\Users <-- write is gone; only Read/Execute remains
RW NT AUTHORITY\SYSTEM
RW BUILTIN\Administrators
Accepts good input (the service still starts and functions):
C:\> sc start VulnSvc
SERVICE_NAME: VulnSvc
STATE : 4 RUNNING
Expected result. The write attempt is denied (Access is denied.), accesschk shows Users reduced to R, and the service still reaches RUNNING. If all three hold, the weakness is remediated without breaking functionality. (A C11 note for cross-track learners: the same idea in code is opening the binary path with fopen(path, "rb") succeeding while fopen(path, "wb") fails with errno == EACCES for a non-privileged process — the OS ACL, not the program, enforces it.)
Walkthrough of the INSECURE -> SECURE -> VERIFY flow above.
| Step | Command | What happens | Why it matters |
|---|---|---|---|
| 1 | sc qc VulnSvc |
SCM prints BINARY_PATH_NAME and SERVICE_START_NAME |
Confirms the service runs as LocalSystem — so its binary is a SYSTEM-equivalent asset |
| 2 | accesschk -quv svc.exe |
Reads the NTFS DACL on the file | Shows Users : RW — an unprivileged group can replace SYSTEM code. This is the finding |
| 3 | (stop) | We do not overwrite the binary | Proving the ACL grants write is sufficient evidence; exploitation adds risk without adding information in a defensive audit |
| 4 | icacls ... /inheritance:r |
Removes inherited ACEs so no loose parent permission sneaks back in | Prevents the folder's permissive inheritance from re-granting write |
| 5 | icacls ... /grant:r Users:(RX) |
Re-grants Users only Read+Execute |
Least privilege: users can run the service but not modify it |
| 6 | echo test > svc.exe (std user) |
OS returns Access is denied. |
Demonstrates the fix rejects the malicious action (overwrite) |
| 7 | accesschk again |
Users : R |
Independent confirmation the write ACE is gone |
| 8 | sc start VulnSvc -> RUNNING |
Service launches normally | Demonstrates the fix accepts legitimate use — no functional regression |
How the values change: before, the file's DACL contained an ACE (A;;FA;;;BU)-style full/write for Users; after icacls /grant:r Users:(RX), that becomes read+execute only (RX). The state that matters — "can a non-admin write this file?" — flips from yes to no, which is exactly what closes the escalation path, while "can the service still start?" stays yes.
1. Assuming only administrators can change services.
SERVICE_CHANGE_CONFIG to Users or Authenticated Users.accesschk -uwcqv "Authenticated Users" * (or per service) to see the actual rights.2. Reporting an unquoted path as automatically exploitable.
ImagePath as a critical privesc.icacls "C:\Program Files"). Rate severity by actual exploitability.3. Fixing the file ACL but ignoring the folder.
svc.exe but leaving its folder writable..exe.4. Confusing "the service is stopped" with "the service is safe."
START_TYPE : AUTO_START or a user-triggerable start means the planted/replaced binary runs later, still as SYSTEM.sc qc start type.5. Running everything as SYSTEM "to be safe."
LocalSystem by default.LocalService/NetworkService or a dedicated low-privilege account with only the rights the service needs.sc qc returns OpenService FAILED 1060 — the service name is wrong. Use sc query state= all (note the space after =) to list exact names; service display names differ from the short name sc expects.
sc query state=all shows nothing / syntax error — sc requires a space after each = and none before it: state= all, binPath= "...". This is the single most common sc mistake.
accesschk prints a EULA prompt or does nothing — add -accepteula. If "not recognized," you are not in the folder where you unzipped Sysinternals, or you downloaded the wrong architecture (use the 64-bit accesschk64.exe on 64-bit Windows).
icacls says "Access is denied" — you are not running as administrator. Open an elevated prompt. Remember ACL changes need admin even though reading them does not.
The service won't start after you tightened the ACL — you removed an access the service genuinely needs. Re-check: the account in SERVICE_START_NAME needs at least Read+Execute on the binary and Read on its folder. Re-grant (RX) to that specific account.
Questions to ask when a finding "doesn't reproduce":
icacls to confirm the effective ACL.Security & safety — detection and logging.
Prevention closes the hole; logging tells you when someone tries it. On a system you administer, ensure these are captured and forwarded to a central log:
What to log (and alert on):
| Event | Windows source | Why it matters |
|---|---|---|
| New service installed | Security 4697, System 7045 | Attackers create services for SYSTEM execution/persistence |
| Service config changed | audit on the service object / 4697 | Detects sc config binPath= tampering |
| Service binary file modified | file-system audit (4663) on service folders | Catches a replaced .exe |
| Service start/stop | System 7036 | A restart right after a config change is a strong signal |
Each record should include: timestamp, source (which user/SID and host), resource (service name and binary path), result (success/failure), the security decision (was the change permitted?), and a correlation id so a config-change event and the subsequent start can be tied together.
Never log: passwords, service-account credentials or tokens, session cookies, private keys, or full secrets embedded in binPath arguments. If a service command line contains a secret, redact it (binPath= <redacted>); log the fact of the change, not the secret. Avoid logging unnecessary PII.
Events that signal abuse: a non-admin SID appearing as the actor on a 4697/7045; a service config change immediately followed by a 7036 start; a new .exe written into a service folder by a user account; a service binPath suddenly pointing at C:\Users\..., %TEMP%, or a world-writable path.
How false positives arise: legitimate software updates install/reconfigure services (expect 7045/4697 during patch windows); management tools (SCCM, Intune) change services routinely; administrators do maintenance. Reduce noise by allow-listing known deployment accounts and maintenance windows, and by alerting on the combination (non-privileged actor + config change + immediate start) rather than any single event.
Two misconceptions to correct: (1) passing an automated privesc scanner with no findings does not prove a host is secure — scanners miss context-dependent paths; combine tooling with manual review. (2) Nothing is ever "completely secure" — the goal is to raise cost and increase the chance of detection.
Authorized real-world use case. A sysadmin onboarding a fleet of Windows workstations runs a scheduled audit that enumerates every service with sc qc, checks each service DACL and binary ACL with accesschk, and flags unquoted paths, SERVICE_CHANGE_CONFIG granted to non-admins, and user-writable binaries. Findings are remediated (quote paths, tighten DACLs/NTFS ACLs, move binaries into C:\Program Files with inherited-admin-only write) and each fix is re-tested before the machine is marked compliant. This is standard CIS-benchmark-style hardening — done on systems the organization owns.
Professional best-practice habits:
| Habit | Beginner | Advanced |
|---|---|---|
| Validation | Read config with sc qc before changing anything |
Script the whole fleet; diff against a known-good baseline |
| Least privilege | Run new services as LocalService, not SYSTEM, when possible |
Use per-service managed/virtual accounts with minimal rights |
| Secure defaults | Always quote binary paths; install into protected dirs | Enforce via GPO / deployment templates so misconfig can't be introduced |
| Logging | Enable service-install auditing (4697/7045) | Central SIEM correlation of config-change + start events |
| Error handling | If a service breaks after hardening, restore least-priv access it needs | Automated verification tests that assert both "non-admin can't write" and "service still starts" |
For an authorized assessor, the deliverable is a finding with a safe reproduction, the exact remediation, and a retest step — not an exploit. The measure of quality is whether the client can fix and confirm the fix from your report.
All tasks are lab-only. Use a Windows VM you own or an intentionally-vulnerable training VM (e.g. a local Windows evaluation image or a purpose-built privesc lab), with networking isolated. Every task ends with remediate + verify.
Authorization checklist (complete before any task):
Beginner 1 — Enumerate.
LocalSystem.sc query state= all and sc qc <name>.SERVICE_START_NAME is the account line.Beginner 2 — Spot the unquoted path.
BINARY_PATH_NAME has a space but no surrounding quotes.sc qc output; note the path.icacls to check).C:\Program Files\... unquoted is the classic case.Intermediate 1 — Audit service DACLs.
SERVICE_CHANGE_CONFIG.accesschk -accepteula -uwcqv "Authenticated Users" * and "Users" *.sc config; auditing only.SERVICE_CHANGE_CONFIG / SERVICE_ALL_ACCESS.Intermediate 2 — Remediate a writable binary and verify.
accesschk (RW for Users), then icacls /inheritance:r + /grant:r to leave Users:(RX) on file and folder.Users : RW; after = Users : R; service still RUNNING.Challenge — Full service-hardening pass with detection.
sc config binPath=, tighten the DACL to remove non-admin CHANGE_CONFIG); (4) verify each fix (path now quoted in sc qc; accesschk shows the right removed; service still starts); (5) enable auditing so a future 4697/7045 or config change is logged.accesschk command you used to find it.Lab cleanup / reset (all tasks): revert the VM to the pre-task snapshot (or restore original ACLs with icacls <path> /reset and re-quote/re-config as needed), stop any test service you started, and confirm sc qc shows the original configuration before ending the session.
Main concepts. A Windows service is a background program the SCM launches with a defined binary path, start type, and service account. Because many services run as LocalSystem (SYSTEM), controlling the code a service runs can mean controlling the whole machine — this is the core local-privilege-escalation risk, and it builds directly on SIDs and ACLs from the prerequisite lesson.
The three weaknesses (and their fixes).
| Weakness | Fix | Verify |
|---|---|---|
| Unquoted service path | Quote binPath with sc config |
sc qc shows quoted path; no writable earlier folder |
Weak service DACL (SERVICE_CHANGE_CONFIG) |
Remove the right from non-admins (sc sdset) |
accesschk no longer shows the right for that group |
| Writable service binary/folder | Tighten NTFS ACL (icacls /inheritance:r + /grant:r ...:(RX)) |
Non-admin write denied AND service still starts |
Key commands. sc query state= all, sc qc <name>, accesschk -accepteula -uwcqv <group> <svc>, icacls <path>, sc config <name> binPath= "...".
Common mistakes. Assuming only admins can change services; reporting every unquoted path as exploitable; fixing the file but not the folder; running everything as SYSTEM.
What to remember. Every fix needs a verification step that proves it rejects the bad action and still accepts the legitimate one. Prevention closes the hole; logging (4697/7045, config-change + start correlation) tells you when someone tries. Decoding config is not the same as securing it, a clean scanner is not proof of security, and nothing is ever "completely secure." Only test systems you own or are explicitly authorized to test, in an isolated lab with a rollback snapshot.