Windows Fundamentals · beginner · ~11 min

NTFS permissions and ACLs

**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.

Overview

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:

  • Explicit Deny wins over Allow.
  • Rights inherit from a parent folder to its children unless inheritance is broken.

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.

Why it matters

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:

  • Penetration testers and red teams run accesschk and icacls audits as a standard step because misconfigured third-party software installs weak ACLs so often.
  • System administrators and blue teams harden ACLs during build hardening and catch drift where an app "fixes" a permission problem by granting Everyone: Full Control.
  • Incident responders read ACLs and file-modification logs to reconstruct how an attacker escalated.

Getting ACLs right — and being able to read them quickly — is a core Windows security skill, not an advanced specialty.

Core concepts

1. Security descriptor, owner, and DACL

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.

2. ACE: Allow or Deny, a SID, and rights

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.

3. Deny wins, and ACE order

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.

4. Inheritance

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.

5. Dangerous grants to recognize

  • Write/Modify for Users, Authenticated Users, or Everyone on a service binary or the folder holding it.
  • Write into any directory on a privileged process's DLL search path (DLL hijacking).
  • A low-privileged principal that owns a sensitive file (they can rewrite the ACL).
  • Writable scheduled-task scripts run by a privileged account.

Threat model (text diagram)

            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.

  1. What asset is protected here? The integrity of svc.exe, because a SYSTEM service trusts and executes it.
  2. Where is the trust boundary? Between the standard user and the SYSTEM account — writing the file crosses it.
  3. What insecure assumption caused this? That anyone able to write the file is trusted; the ACL granted Users:(M) on a file executed at high privilege.
  4. Which logs detect it? An NTFS SACL audit on the folder produces Event ID 4663 (an attempt to access/write the object); unexpected changes to the binary's hash or timestamp corroborate it.
  5. Why only in an authorized lab? Modifying service binaries and escalating privilege on a machine you do not own is a crime; this is practiced only on systems you own or are explicitly authorized to test.

Syntax notes

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.

Lesson

Where Unix uses simple rwx bits, Windows uses much richer Access Control Lists (ACLs) on every file, registry key, and service.

ACL structure

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.

Inheritance

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.

Dangerous permissions to spot

  • A low-privileged user with Write or Modify on a file that a privileged process runs — a service binary, a scheduled task, or a DLL. This leads to privilege escalation.
  • Full Control granted to broad groups such as Everyone, Authenticated Users, or Users on sensitive paths.
  • Writable directories inside a privileged process's search path (DLL hijacking, covered in the privesc lessons).

Tools

  • 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.

Code examples

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.

(1) Insecure setup

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.

(2) Secure fix

# 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.)

(3) Verify — the fix must REJECT bad access and ACCEPT good access

# 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.

Line by line

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.

Common mistakes

1. Adding a Deny instead of removing the bad Allow.

  • WRONG: leave Users:(M) and add Users:(Deny Write).
  • WHY: Deny is broad and order-sensitive; Users includes almost everyone, so a Deny can lock out admins or scripts, and future admins won't understand why access breaks.
  • CORRECTED: icacls file /remove:g "BUILTIN\Users" to delete the grant, then grant least privilege explicitly.
  • RECOGNIZE/PREVENT: if your fix adds ACEs rather than removing the offending one, reconsider. Re-list with icacls after every change.

2. Fixing the file but not the parent's inheritance.

  • WRONG: tighten svc.exe while C:\App still grants Users:(M) inheritably.
  • WHY: new files in the folder inherit the weak grant; the hole reopens.
  • CORRECTED: fix the folder, or icacls C:\App /inheritance:r then re-add only needed ACEs.
  • RECOGNIZE/PREVENT: audit the folder, not just the file; look for (I) flags on the child.

3. Ignoring the owner.

  • WRONG: assume a locked-down DACL is enough.
  • WHY: the owner can rewrite the DACL. A low-priv owner can re-grant themselves Modify.
  • CORRECTED: ensure sensitive files are owned by Administrators/SYSTEM (icacls file /setowner Administrators).
  • RECOGNIZE/PREVENT: check the owner line in the security dialog or Get-Acl.

4. Trusting a scanner's green checkmark.

  • WRONG: "accesschk showed nothing, so the box is secure."
  • WHY: passing one automated audit does not prove a system is secure; the tool only answered one question on one path. Nothing is ever "completely secure."
  • CORRECTED: treat tool output as evidence for a specific claim, and combine audits (services, tasks, DLL paths, registry).

5. Granting Everyone: Full Control to "make it work."

  • WRONG: an app fails with a permissions error, so you open it wide.
  • WHY: this is the exact misconfiguration attackers hunt for.
  • CORRECTED: grant the specific service account the specific right it needs ((RX) for read/run).

Debugging tips

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?

Memory safety

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:

  • Timestamp (UTC) and host.
  • Source principal — the account/SID and, ideally, the logon/session id.
  • Resource — full object path and the right requested (write, WRITE_DAC, WRITE_OWNER).
  • Result — allowed or denied (the security decision).
  • A correlation id / event record id to tie related events together.

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.

Real-world uses

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.

Practice tasks

All tasks are lab-only, on a Windows VM you own. Each ends with remediation and verification.

Authorization checklist (before you start):

  • The VM/machine is yours or you have explicit written permission.
  • It is isolated (local/snapshot), not production.
  • You can take a snapshot and revert.
  • You will run cleanup/reset steps when done.

Beginner 1 — Read an ACL.

  • Objective: interpret a real DACL.
  • Requirements: run icacls C:\Windows\System32\drivers\etc\hosts. Write, in plain English, each ACE: type, SID, rights, and whether it is inherited.
  • Output: a short list mapping shorthand like (I)(RX) to "inherited, read & execute."
  • Hints: (I)=inherited, (F)/(M)/(RX)/(R)/(W) are rights.
  • Concepts: DACL, ACE, inheritance flags.

Beginner 2 — Spot the dangerous grant.

  • Objective: recognize a weak ACE.
  • Requirements: in a lab folder create 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.
  • Constraints: stand-in file only; no real service.
  • Defensive conclusion: remove the grant with /remove:g and re-run icacls to confirm it's gone.
  • Concepts: dangerous grants, icacls /grant and /remove.

Intermediate 1 — Audit with accesschk and remediate.

  • Objective: find, fix, and verify a writable "service" file.
  • Requirements: reproduce the weak grant from Beginner 2, then run accesschk -accepteula -w Users <path> to detect it. Remediate, then re-run accesschk.
  • Input/Output: before = path listed as writable; after = empty result.
  • Hints: prefer removing the Allow to adding a Deny.
  • Defensive conclusion: the audit tool must return empty and a limited-user write must fail. Verify both.
  • Concepts: accesschk, least privilege, mitigation verification.

Intermediate 2 — Inheritance trap.

  • Objective: prove that fixing the child alone is insufficient.
  • Requirements: grant 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.
  • Constraints: lab folder only.
  • Defensive conclusion: remediate at the parent (remove grant or /inheritance:r then re-add needed ACEs); verify a newly created file no longer gets Modify for Users.
  • Concepts: inheritance, parent vs child remediation, verification.

Challenge — Detection & logging.

  • Objective: make ACL abuse detectable, not just closed.
  • Requirements: on a lab folder, enable file-system auditing and add a SACL audit ACE for write by 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.
  • Constraints: lab VM; revert the audit policy afterward.
  • Defensive conclusion: document what you log, what you must never log (passwords, keys, secret file contents), and how you'd distinguish an installer's legitimate write from abuse.
  • Concepts: SACL auditing, Event 4663/4670, detection engineering, false positives.

Cleanup / reset: Remove-Item -Recurse -Force C:\Lab; revert any audit-policy and SACL changes; restore your VM snapshot if you took one.

Summary

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."