Windows Fundamentals · beginner · ~11 min
**What you will learn** - Read a Windows NTFS Access Control List: identify the owner, each ACE, whether it is Allow or Deny, which SID it targets, and which rights it grants. - Apply the two rules that decide the outcome of every access check: **explicit Deny wins**, and permissions **inherit** from parent folders. - Spot the classic dangerous grant: a low-privileged user or broad group (`Users`, `Authenticated Users`, `Everyone`) with **Write** or **Modify** on something a privileged process trusts. - Use `icacls` and Sysinternals `accesschk` to audit "what can this account write?" on a lab machine. - Remediate a weak ACL and then **verify** the fix — prove the bad grant is gone and legitimate access still works. - Know what to log so that ACL tampering and abuse are detectable after the fact.
Security objective. The asset is the integrity of files that privileged Windows code depends on — service executables, scheduled-task scripts, DLLs, and their parent folders. The threat is local privilege escalation: a normal user who cannot directly become an administrator instead overwrites a file that a high-privilege process later runs, so their code executes as SYSTEM. In this lesson you will learn to detect that weakness by reading an ACL, and to prevent it by tightening the permission and verifying the fix.
Windows protects every securable object — files, folders, registry keys, services, processes — with a security descriptor. The part that controls access is the DACL (Discretionary Access Control List): an ordered list of ACEs (Access Control Entries). Each ACE says one thing: this SID is Allowed (or Denied) these rights. A SID is the unique identifier Windows assigns to a user or group — the concept you met in the prerequisite lesson "Windows users, groups, and SIDs" (win-users-groups). NTFS permissions are simply ACLs applied to files and folders on an NTFS volume.
Two rules decide the outcome of every check:
The single risk that matters most for security: a low-privileged principal that can write to a resource a privileged process trusts. Everything else in this lesson supports finding and fixing that one condition. This connects forward to win-services (your next lesson), where a writable service binary is the textbook escalation path.
Weak ACLs are the most common root cause of local privilege escalation on Windows. Attackers rarely need a memory-corruption exploit; a single writable file in the wrong place is enough.
The pattern is always the same. A privileged process — a service running as SYSTEM, a scheduled task, an installer — reads or executes a file. If a normal user can replace that file, the user's code runs with the privileged process's token. A writable service .exe, a writable script called by a task, or a writable directory on a DLL search path each turns an ordinary user into SYSTEM, the highest local account.
In authorized professional work this shows up constantly:
accesschk and icacls audits as a standard step because misconfigured third-party software installs weak ACLs so often.Everyone: Full Control.Getting ACLs right — and being able to read them quickly — is a core Windows security skill, not an advanced specialty.
Definition. Every securable object carries a security descriptor containing an owner SID, a DACL (who may do what), and a SACL (audit rules). NTFS permissions are the DACL of a file or folder.
Plain explanation. Think of the DACL as a guest list at a door. Each line names one guest (a SID) and says either "let them in for these things" (Allow) or "never let them in for these things" (Deny).
How it works. When a process opens a file, Windows walks the DACL and accumulates rights for the SIDs in the caller's token until it has enough to satisfy the request, honoring Deny first (see rule 3). The owner matters because the owner can always change the DACL — even if the DACL grants them nothing else. So "who owns this?" is a real security question.
When / when-not. A DACL that is present but empty denies everyone. A DACL that is absent (null) allows everyone — a serious misconfiguration, not the same thing.
Pitfall. People read the owner as harmless. If a low-privileged user owns a sensitive file, they can rewrite its ACL and grant themselves Modify, even if the current ACL looks locked down.
Definition. An ACE is one entry: a type (Allow/Deny), a SID, a set of rights (Read, Write, Modify, Full Control, Read & Execute, etc.), and inheritance flags.
Plain explanation. "BUILTIN\Users : Allow : Modify" means every member of the local Users group may read, change, and delete this file.
How it works. Rights are a bitmask. Modify includes Write, and Full Control additionally includes the power to change permissions (WRITE_DAC) and take ownership (WRITE_OWNER). In icacls shorthand: (F) full, (M) modify, (RX) read & execute, (W) write.
When / when-not. Grant the least right that works: a service account that only reads a config file needs (RX), never (M).
Pitfall. (W) alone looks minor but often lets a user replace file contents; combined with a privileged consumer, that is escalation.
Definition. In the access-check algorithm an explicit Deny ACE that matches is evaluated before Allow ACEs, so Deny overrides.
Plain explanation. One "never" beats any number of "yes" lines.
How it works. Windows canonical ordering places explicit Deny ACEs ahead of explicit Allow ACEs, and explicit ACEs ahead of inherited ones. The check stops as soon as the requested rights are all granted or any is denied.
When / when-not. Prefer removing an over-broad Allow to adding a Deny. A stray Deny can silently break legitimate access and is easy to forget.
Pitfall. "I added Deny for Everyone" can lock out administrators too, because admins are part of Everyone. Test after changing.
Definition. Child objects receive inherited ACEs from their parent folder unless inheritance is disabled on the child.
Plain explanation. Set a permission on a folder and, by default, every file and subfolder underneath gets it automatically.
How it works. icacls marks inherited ACEs with (I). Breaking inheritance either copies the current inherited ACEs as explicit ones or removes them.
When / when-not. Inheritance keeps large trees consistent — good. But one weak grant on a top-level folder (say C:\App) silently exposes everything beneath it.
Pitfall. Fixing only the child leaves the parent's bad inherited ACE in place; new files created later inherit it again.
Users, Authenticated Users, or Everyone on a service binary or the folder holding it. TRUST BOUNDARY: standard user -> SYSTEM privilege
================================================
ENTRY POINT ASSET (protected)
[ Standard user account ] [ Service binary C:\App\svc.exe ]
| ^ runs as SYSTEM at boot
| (1) has WRITE via weak ACE: |
| BUILTIN\Users:(M) |
v |
overwrite svc.exe ------------------------
|
v (2) service restarts / reboot
attacker code executes as SYSTEM <-- privilege escalation
DETECTION SURFACE
- NTFS auditing (SACL) on C:\App -> Event 4663 (write to svc.exe)
- Service binary hash change / unexpected modification time
- accesschk baseline diff: "Users can write svc.exe"
Knowledge check.
svc.exe, because a SYSTEM service trusts and executes it.SYSTEM account — writing the file crosses it.Users:(M) on a file executed at high privilege.Read an ACL with icacls:
icacls C:\App\svc.exe
# Example output, annotated:
# C:\App\svc.exe
# BUILTIN\Administrators:(F) <- Allow, Full Control, explicit
# NT AUTHORITY\SYSTEM:(F) <- Allow, Full Control
# BUILTIN\Users:(I)(M) <- (I)=inherited (M)=Modify <-- DANGER
#
# Shorthand rights: (F) Full (M) Modify (RX) Read&Execute (R) Read (W) Write
# Inheritance flags: (I) inherited from parent (OI) object inherit (CI) container inherit
Audit "what can this account write?" with Sysinternals accesschk (accept the EULA once with -accepteula):
# Files/dirs a given user can WRITE under a path (-w = write, -s = recurse):
accesschk.exe -accepteula -w -s Users "C:\App"
# Writable service binaries a user can modify:
accesschk.exe -accepteula -quvcw Users
# -u quiet errors -v verbose -c services -w writable (target: Users)
Remediate with icacls (remove a bad grant; do not just pile on a Deny):
icacls C:\App\svc.exe /remove:g "BUILTIN\Users" # drop the Users grant entirely
icacls C:\App /inheritance:r # break & drop inherited ACEs (then re-add needed ones)
icacls C:\App\svc.exe /grant "NT SERVICE\svc:(RX)" # least-privilege: service only reads/executes
All commands above are read/audit and repair operations on a local lab path you control. Nothing here targets a remote or third-party system.
Where Unix uses simple rwx bits, Windows uses much richer Access Control Lists (ACLs) on every file, registry key, and service.
A file's security descriptor holds a DACL (Discretionary Access Control List) — a list of Access Control Entries (ACEs).
Each ACE states a simple fact: this SID is Allowed or Denied these rights (Read, Write, Modify, Full Control, and so on). A SID is the unique identifier Windows uses for a user or group.
One rule overrides the rest: Deny ACEs win over Allow ACEs.
Permissions flow down from a folder to its children by inheritance. This continues unless inheritance is deliberately broken on a child object.
This is why a single misconfigured top-level folder can expose an entire tree below it.
Everyone, Authenticated Users, or Users on sensitive paths.icacls <path> shows the ACL for a given path.accesschk (from Sysinternals) is the go-to tool for auditing the question "what can this user write?" — exactly the question a Windows privesc check asks.The example uses a self-contained lab folder so you can see an insecure ACL, fix it, and verify the fix. Run in an elevated PowerShell on a Windows VM you own. It never touches a real service.
WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.
# --- create a lab "privileged" binary and grant a weak ACL ---
New-Item -ItemType Directory -Path C:\Lab\App -Force | Out-Null
Set-Content -Path C:\Lab\App\svc.exe -Value 'placeholder-binary' # stand-in, not a real service
# INSECURE: give the broad local Users group Modify on the "service" file
icacls C:\Lab\App\svc.exe /grant "BUILTIN\Users:(M)"
# Observe the weakness
icacls C:\Lab\App\svc.exe
Expected: the listing now includes a line like BUILTIN\Users:(M). That is the dangerous grant — any standard user could overwrite svc.exe.
# Remove the over-broad grant (prefer removing an Allow over adding a Deny)
icacls C:\Lab\App\svc.exe /remove:g "BUILTIN\Users"
# Re-assert least privilege: admins/SYSTEM manage it; a real service account only reads+executes
icacls C:\Lab\App\svc.exe /grant "BUILTIN\Administrators:(F)" "NT AUTHORITY\SYSTEM:(F)"
# (In a real system the service's own account would get :(RX), not Users.)
# CHECK A (reject): confirm the Users group can no longer write the file.
# accesschk returns nothing / 'No matching objects' when Users has NO write access.
accesschk.exe -accepteula -w Users C:\Lab\App\svc.exe
# PASS = empty result. FAIL = it lists C:\Lab\App\svc.exe as writable.
# CHECK B (reject, functional): simulate a standard user trying to overwrite it.
# Run this in a NON-elevated shell as a limited test account -> expect Access Denied.
try { Set-Content C:\Lab\App\svc.exe 'tampered' -ErrorAction Stop; 'FAIL: write succeeded' }
catch { 'PASS: write denied -> ' + $_.Exception.Message }
# CHECK C (accept): a privileged/read path still works.
Get-Content C:\Lab\App\svc.exe # admins/SYSTEM can still read+run it -> expected: placeholder text
# Cleanup / reset the lab
Remove-Item -Recurse -Force C:\Lab
Reading the output. In step 1, icacls shows the Users:(M) line — the vulnerability. In step 3, Check A returns nothing (Users lost write), Check B raises UnauthorizedAccessException for a limited user (bad write rejected), and Check C still prints the placeholder (good access preserved). Together those three prove the remediation both closed the hole and kept legitimate use working.
Walkthrough of the insecure → secure → verify example.
| Step | Command | What happens | Why it matters |
|---|---|---|---|
| 1 | New-Item ... C:\Lab\App + Set-Content ... svc.exe |
Creates a lab folder and a stand-in "service binary" (just text). | A safe target that imitates a privileged file without touching real services. |
| 2 | icacls svc.exe /grant "BUILTIN\Users:(M)" |
Adds an Allow / Users / Modify ACE to the DACL. | This is the planted weakness: any standard user can now replace the file. |
| 3 | icacls svc.exe |
Prints the DACL; you see BUILTIN\Users:(M). |
You are reading the ACL exactly as an auditor would — spotting the bad ACE. |
| 4 | icacls svc.exe /remove:g "BUILTIN\Users" |
Deletes the Users grant ACE. | Removing the over-broad Allow is cleaner than layering a Deny, which could break other access. |
| 5 | icacls ... /grant Administrators:(F) SYSTEM:(F) |
Ensures admins/SYSTEM retain Full Control. | Confirms least privilege: only trusted principals manage the file. |
| 6 | accesschk -w Users svc.exe |
Queries whether the Users group has write. Empty result = none. | Automated proof the specific bad capability is gone (Check A). |
| 7 | Set-Content in a limited shell (try/catch) |
A non-admin write attempt throws UnauthorizedAccessException. |
Functional proof the fix actually blocks tampering (Check B). |
| 8 | Get-Content svc.exe |
Reads the file successfully as admin. | Proof legitimate access still works — no over-correction (Check C). |
| 9 | Remove-Item -Recurse -Force C:\Lab |
Deletes the lab. | Cleanup so the vulnerable artifact never lingers. |
How the values change. Before step 4 the DACL contains {Administrators:F, SYSTEM:F, Users:M}. After step 4 it is {Administrators:F, SYSTEM:F}. The access check for a standard-user token therefore accumulates no write right and returns ACCESS_DENIED, which is exactly what Checks A and B observe.
1. Adding a Deny instead of removing the bad Allow.
Users:(M) and add Users:(Deny Write).Users includes almost everyone, so a Deny can lock out admins or scripts, and future admins won't understand why access breaks.icacls file /remove:g "BUILTIN\Users" to delete the grant, then grant least privilege explicitly.icacls after every change.2. Fixing the file but not the parent's inheritance.
svc.exe while C:\App still grants Users:(M) inheritably.icacls C:\App /inheritance:r then re-add only needed ACEs.(I) flags on the child.3. Ignoring the owner.
Administrators/SYSTEM (icacls file /setowner Administrators).Get-Acl.4. Trusting a scanner's green checkmark.
5. Granting Everyone: Full Control to "make it work."
(RX) for read/run).icacls prints Access is denied. You are not elevated, or you lack READ_CONTROL on the object. Open an elevated PowerShell; if you own the object you can still read the DACL.
accesschk says command not found. It is a Sysinternals download, not built in. Place accesschk.exe (or accesschk64.exe) in your path and run -accepteula once. Verify the architecture matches (64-bit tool on a 64-bit OS).
A change "didn't take." The child still shows the old right because it is inherited ((I)). Fix the parent, or break inheritance on the child, then re-list.
You locked yourself out. A Deny on Everyone/Users can block admins. Take ownership as an administrator (icacls file /setowner Administrators / takeown), then rewrite the DACL.
The functional write test didn't fail as expected. You probably ran it in an elevated/admin shell. Test rejection from a standard user context, or the token still carries admin rights.
Questions to ask when an ACL behaves unexpectedly: Is this ACE explicit or inherited? Is there a Deny higher in the order? Which SIDs is this token actually a member of (whoami /groups)? Who is the owner? Am I testing as the right identity?
Security & safety — detection and logging for ACL abuse.
Remediating the ACL closes today's hole; logging is how you notice tomorrow's attempt. NTFS supports object auditing via the SACL (System Access Control List), separate from the DACL.
Enable detection. Turn on "Audit File System" (Advanced Audit Policy) and set an audit ACE on the sensitive folder for write/change-permission by broad groups. Successful/failed writes then generate Windows Security Event ID 4663 (an attempt to access an object) and 4670 (permissions on an object were changed). Service-binary tampering also shows up as service anomalies you will study in win-services.
What to log for each security-relevant event:
Never log: passwords, tokens, session cookies, private keys, full file contents of sensitive files, or unnecessary PII. An audit log records that an access happened, not the secret material involved.
Events that signal abuse: a standard user writing to a service binary or its folder; ACL/owner changes on privileged paths (4670) outside a change window; a new Everyone/Users Full-Control ACE appearing; writes into a directory on a privileged DLL search path.
False positives arise from legitimate installers and updaters (which do write under program folders), backup/AV agents touching many files, and admins doing sanctioned maintenance. Reduce noise by scoping audit ACEs to the broad groups you don't expect to write, and by correlating with known change windows and software-deployment logs before raising an alert.
Authorized use case. During a sanctioned internal security review, an engineer runs accesschk -w Users -s "C:\Program Files" and finds a third-party app installed with Users:(M) on its service .exe. That is a reportable local privilege escalation finding. The remediation: remove the Users grant, set least privilege for the service account, confirm the owner is Administrators, and enable a SACL audit on the folder. Then retest with accesschk (empty result) and a limited-user write attempt (Access Denied) to prove closure.
Professional best-practice habits.
| Habit | Beginner | Advanced |
|---|---|---|
| Validation | Read the ACL with icacls before and after any change. |
Baseline ACLs with Get-Acl/accesschk and diff on a schedule to catch drift. |
| Least privilege | Grant (RX) where read/run is enough; never (M) "just in case." |
Use dedicated per-service virtual accounts (NT SERVICE\<name>) scoped to only their paths. |
| Secure defaults | Keep default inheritance; don't broaden top-level folders. | Break inheritance deliberately on sensitive trees and document why. |
| Logging | Turn on file-system auditing on sensitive folders. | Forward 4663/4670 to a SIEM with alert rules and known-good change windows. |
| Error handling | Never respond to a permissions error with Everyone:Full. |
Trace the actual missing right and grant only that to the specific SID. |
Ethics/authorization. Only test systems you own or are explicitly authorized to assess. All hands-on practice here runs on your own VM, a container, or an intentionally-vulnerable lab image — never on production or third-party machines.
All tasks are lab-only, on a Windows VM you own. Each ends with remediation and verification.
Authorization checklist (before you start):
Beginner 1 — Read an ACL.
icacls C:\Windows\System32\drivers\etc\hosts. Write, in plain English, each ACE: type, SID, rights, and whether it is inherited.(I)(RX) to "inherited, read & execute."(I)=inherited, (F)/(M)/(RX)/(R)/(W) are rights.Beginner 2 — Spot the dangerous grant.
svc.exe (a text stand-in) and grant BUILTIN\Users:(M). Run icacls and identify the exact line that represents privilege-escalation risk; explain in one sentence why./remove:g and re-run icacls to confirm it's gone.icacls /grant and /remove.Intermediate 1 — Audit with accesschk and remediate.
accesschk -accepteula -w Users <path> to detect it. Remediate, then re-run accesschk.accesschk, least privilege, mitigation verification.Intermediate 2 — Inheritance trap.
Users:(M) on a folder C:\Lab\App (inheritable), observe a new file inheriting (I)(M), fix only the file, then create another new file and show it still inherits the weak grant. Finally fix the folder correctly./inheritance:r then re-add needed ACEs); verify a newly created file no longer gets Modify for Users.Challenge — Detection & logging.
Users. Trigger a write, then locate the resulting Event 4663 in the Security log and identify the fields you would log (timestamp, source SID, resource, requested right, result, event record id). Note two realistic false-positive sources.Cleanup / reset: Remove-Item -Recurse -Force C:\Lab; revert any audit-policy and SACL changes; restore your VM snapshot if you took one.
Main concepts. Every NTFS object has a security descriptor with an owner, a DACL (allow/deny ACEs), and an optional SACL (auditing). Each ACE targets a SID with specific rights. Two rules decide access: explicit Deny wins, and permissions inherit from parent folders. The security question that matters is always: can a low-privileged principal write to something a privileged process trusts? — because that is local privilege escalation.
Key commands.
icacls <path> — read a DACL; /grant, /remove:g, /inheritance:r, /setowner to change it.accesschk -accepteula -w Users <path> — audit "what can this account write?"Get-Acl / whoami /groups — inspect ACLs and your token's group SIDs.Common mistakes. Adding a Deny instead of removing the bad Allow; fixing the file but not the parent's inheritance; ignoring the owner; trusting one green scan; and slapping on Everyone:Full Control to silence an error.
What to remember. Least privilege, remove over-broad grants, watch inheritance and ownership, then verify every fix two ways — the audit tool returns empty and a limited user is actually denied, while legitimate access still works. Turn on auditing so future abuse is detectable, log the security decision (never the secrets), and practice only on systems you own or are authorized to test. No system is ever "completely secure."