Windows File System, Permissions, ACLs & Security Descriptors
Windows File System, Permissions, ACLs & Security Descriptors
This is Chapter 2 of the Windows Internals series. In Chapter 1 we sketched the Windows security model in one sentence: your identity is a token, and permission is an ACL checked when you open something. This chapter turns that one sentence into a complete, working understanding, using the most tangible thing on any computer — files and folders — as the teaching ground. By the end you will be able to look at any file on any Windows machine and answer, precisely: who can read it, who can change it, who owns it, where those rights came from, and how an attacker might abuse a mistake in them.
We start deliberately slowly, in plain English, so a complete newcomer can follow every step. Then we climb — layer by layer — to the level a security engineer at a large tech company needs: NTFS internals, the exact byte-layout intuition of a security descriptor, SDDL, inheritance rules, effective-access auditing, alternate data streams, and the ACL misconfigurations that turn an ordinary user into SYSTEM. Nothing here requires you to have memorized Chapter 1; we re-introduce each idea as we need it.
Start Here: The Nightclub Analogy (Plain English)
Forget computers for a moment. Imagine a nightclub.
- Every guest has an ID card that says who they are and which groups they belong to ("staff", "VIP", "over-21").
- Every door has a guest list taped to it. Each line on the list says something like "VIPs: allowed in" or "Under-21s: not allowed".
- A bouncer stands at each door. When you try to walk in, the bouncer looks at your ID card, reads down the guest list line by line, and decides: let you in, or turn you away.
That is the entire Windows permission model. Translating the analogy:
- Your ID card is your access token (from Chapter 1) — it names you and lists your groups.
- The guest list on each door is an ACL (Access Control List). It belongs to the file or folder.
- Each line on the guest list is an ACE (Access Control Entry) — one rule, like "the Administrators group may do anything" or "the user bob may not delete this".
- The bouncer is the Security Reference Monitor, the part of the Windows kernel that actually makes the decision.
Everything else in this chapter is just detail added onto this picture: what exactly is on the ID card, how the guest list is stored, the order the bouncer reads the lines in, and where the lists come from when nobody wrote them by hand. Keep the nightclub in your head — we will keep pointing back to it.
One-line summary to memorize now: An ACL is a list of ACEs; each ACE grants or denies specific rights to one SID (person/group); the bouncer checks your token against that list when you open the object.
Part 1: What Is a File System, and Why NTFS?
Before we can talk about permissions on files, we need to know what a "file" even is to Windows. A file system is the scheme the operating system uses to organize bytes on a disk into named files and folders, and to remember facts about each file (its size, its dates, and — crucially for us — who is allowed to touch it). Those "facts about a file" are called metadata.
Windows has used several file systems over the years, but modern Windows runs on NTFS (New Technology File System). Here is why NTFS matters for security specifically, compared to the older FAT32 / exFAT you might see on a USB stick:
| Capability | FAT32 / exFAT | NTFS |
|---|---|---|
| Per-file permissions (ACLs) | ❌ none | ✅ full ACLs |
| Ownership | ❌ | ✅ every object has an owner |
| Auditing (log who accessed what) | ❌ | ✅ via SACLs |
| Journaling (crash recovery) | ❌ | ✅ $LogFile |
| Encryption (EFS) / compression | ❌ | ✅ |
| Alternate Data Streams | ❌ | ✅ (a security-relevant quirk) |
| Max file size | 4 GB (FAT32) | 16 EB (practically unlimited) |
The single most important row for us is the first one: FAT32 has no concept of permissions at all. Anyone who can reach the disk can read every file. This is why plugging a locked laptop's drive into another machine can bypass "permissions" if the data isn't encrypted — the permissions live in NTFS metadata, and another OS can choose to ignore them. Permissions are an access-control mechanism enforced by a running Windows kernel, not a confidentiality guarantee against someone with raw disk access. Hold onto that distinction; it is a common beginner (and interview) trap.
How NTFS stores everything: the $MFT
NTFS keeps a master index called the Master File Table ($MFT). Think of it as the library's card catalog: one record per file and folder. Each $MFT record stores the file's name, its timestamps, where its data lives on disk, and a block of security information (the ACL, or a reference to one). You do not edit the $MFT by hand, but you should know it exists for two reasons:
- Forensics/DFIR: the
$MFTis a goldmine — it can reveal files that were deleted, renamed, or time-stomped, because old records and timestamps linger. Tools likeMFTECmdparse it. - Performance of ACLs: identical security descriptors are stored once in a shared metadata file (
$Secure) and referenced by many files, so setting the same permissions on a million files does not store a million copies.
flowchart TD
DISK["Physical disk / volume (NTFS)"] --> MFT["$MFT — one record per file"]
MFT --> REC["File record:<br/>name, timestamps, data location,<br/>security id"]
REC --> SEC["$Secure — shared table of<br/>Security Descriptors"]
SEC --> SD["Security Descriptor:<br/>Owner + DACL + SACL"]
That "Security Descriptor" box is the heart of this chapter. Parts 3–6 open it up completely. But first, permissions the way you will actually meet them: in the file's Properties dialog.
Part 2: Permissions as a Newcomer First Meets Them
Right-click any file in Windows Explorer, choose Properties → Security, and you are looking at the ACL — the guest list — in a friendly form. Let's read it the way a beginner should, then connect each word to the precise concept.
You will see two panes. The top pane lists "Group or user names" — these are the SIDs (people and groups) that have some rule about this file. The bottom pane shows, for whichever name you select, a set of checkboxes: Full control, Modify, Read & execute, List folder contents, Read, Write. Those six are not the real, low-level permissions — they are friendly bundles Microsoft groups together so humans don't have to think in raw bits. Here is what each bundle actually means:
| Friendly permission | Plain meaning | Roughly includes |
|---|---|---|
| Read | Look at the file's contents and attributes | ReadData, ReadAttributes, ReadEA, ReadPermissions |
| Write | Change contents / create files in a folder | WriteData, AppendData, WriteAttributes |
| Read & execute | Read, plus run it if it's a program | Read + Execute/Traverse |
| List folder contents | See the names inside a folder | ListDirectory (folders only) |
| Modify | Read, write, and delete the file | Read & execute + Write + Delete |
| Full control | Everything, including changing the permissions and taking ownership | Modify + WriteDAC + WriteOwner |
The two rows that should make a security person sit up are the last two abilities inside Full control: WriteDAC (change the guest list itself) and WriteOwner (make yourself the owner). If you can do either, you can grant yourself anything else — so "Full control" is really "I own the door and can rewrite its guest list." We return to why that is an attacker's dream in Part 8.
Newcomer checkpoint. If all you remember so far is: Read = look, Write = change, Modify = change + delete, Full control = also rewrite the rules — you already understand 80% of day-to-day Windows permissions. Everything below is making that precise and showing where it breaks.
The command-line view: icacls
The Properties dialog is fine for one file, but professionals use the command line so they can inspect thousands of files and script fixes. The core tool is icacls (Improved Change ACLs). It ships with every Windows install. We will teach it from scratch and use it throughout. First, just read a file's ACL:
icacls C:\Windows\System32\drivers\etc\hostsTypical output:
C:\Windows\System32\drivers\etc\hosts BUILTIN\Administrators:(F)
NT AUTHORITY\SYSTEM:(F)
BUILTIN\Users:(RX)
APPLICATION PACKAGES...:(RX)
Successfully processed 1 files; Failed processing 0 files.Read it like the nightclub guest list. Each line is one ACE: a principal (BUILTIN\Administrators), then its rights in parentheses. The letters are icacls shorthand — (F) = Full control, (M) = Modify, (RX) = Read & execute, (R) = Read, (W) = Write. So this says: admins and SYSTEM can do anything; ordinary Users can only read and run it. That is exactly what you want for a sensitive system file — and if you ever see BUILTIN\Users:(F) or Everyone:(F) on a system file, that is a misconfiguration and possibly a privilege-escalation path.
Here is the full icacls letter cheat, which we'll lean on all chapter:
| Code | Meaning | Code | Meaning |
|---|---|---|---|
F |
Full control | RX |
Read & execute |
M |
Modify | R |
Read-only |
W |
Write-only | D |
Delete |
(OI) |
Object Inherit (files) | (CI) |
Container Inherit (subfolders) |
(IO) |
Inherit Only (rule not applied here) | (I) |
Inherited from parent |
(NP) |
No Propagate | (N) |
No access |
Part 3: SIDs — How Windows Names People and Groups
The nightclub bouncer needs to match your ID card against names on the list. But Windows never matches by username text like "bob" — usernames can be renamed and reused. Internally it matches by a permanent number called a SID (Security Identifier). Understanding SIDs is essential because ACLs store SIDs, not names, and because certain SIDs are the keys to the kingdom.
A SID looks like this:
S-1-5-21-3623811015-3361044348-30300820-1013
│ │ │ └──────────── domain/machine identifier ───────────┘ └RID┘
│ │ └ authority (5 = NT Authority)
│ └ revision (always 1)
└ literally the letter SThe last chunk, the RID (Relative Identifier), distinguishes principals within the same machine or domain. Some SIDs and RIDs are universal and worth memorizing because you will see them in every ACL:
| SID / RID | Who it is | Why it matters |
|---|---|---|
S-1-5-18 |
SYSTEM (LocalSystem) | The OS itself; most powerful local identity. |
S-1-5-32-544 |
BUILTIN\Administrators | Local admins group. |
S-1-5-32-545 |
BUILTIN\Users | All normal interactive users. |
S-1-1-0 |
Everyone | Literally everyone, including guests. Everyone:(F) is a red flag. |
S-1-5-11 |
Authenticated Users | Anyone who logged in (not anonymous). |
S-1-3-0 |
Creator Owner | Placeholder that becomes whoever creates a child object. |
RID 500 |
Built-in Administrator account | The classic target account. |
RID 512 |
Domain Admins | Full control of an AD domain (later notebook). |
Translate SIDs to names and back on any host:
# Name -> SID
$sid = (New-Object System.Security.Principal.NTAccount("BUILTIN\Users")).Translate([System.Security.Principal.SecurityIdentifier])
$sid.Value # S-1-5-32-545
# SID -> Name
([System.Security.Principal.SecurityIdentifier]"S-1-5-18").Translate([System.Security.Principal.NTAccount]).Value # NT AUTHORITY\SYSTEM
# Your own identity and groups
whoami /user
whoami /groupsWhy attackers care about SIDs. Because ACLs are written in SIDs, an attacker who can get their SID onto a powerful ACL — or who controls a group SID that is already on many ACLs — inherits all that access. In Active Directory this becomes SID History abuse and ACL-based attacks (BloodHound maps exactly these edges). Even locally, adding your user's SID to a service's ACL is a persistence and privesc trick. The name on the door doesn't matter; the SID does.
Part 4: The Security Descriptor — Opening the Box
Every securable object in Windows — a file, folder, registry key, service, process, printer — carries a Security Descriptor (SD). This is the real data structure behind the friendly Properties dialog. It has four parts, and once you know them, the whole model clicks:
- Owner — the SID of who owns the object. The owner always has an implicit right to change the object's permissions, even if the DACL says otherwise. (This is why "take ownership" is so powerful.)
- Group — a mostly-legacy POSIX-compatibility field; largely ignored on modern Windows. Don't worry about it.
- DACL (Discretionary Access Control List) — the guest list that grants/denies access. This is what 99% of "permissions" work touches.
- SACL (System Access Control List) — the auditing list: it doesn't grant access, it says "log it when someone does X." Used for security monitoring and compliance.
graph TD
SD["Security Descriptor"] --> OWN["Owner SID<br/>(can always rewrite the DACL)"]
SD --> GRP["Group SID (legacy, ignored)"]
SD --> DACL["DACL — access rules<br/>list of ACEs (Allow/Deny)"]
SD --> SACL["SACL — auditing rules<br/>list of ACEs (Audit)"]
DACL --> A1["ACE: Allow Administrators Full"]
DACL --> A2["ACE: Allow Users ReadExecute"]
DACL --> A3["ACE: Deny Bob Delete"]
SACL --> S1["ACE: Audit Everyone Write (Failure)"]
The distinction to nail down: DACL controls access; SACL controls auditing. A blank (null) DACL means "no rules present" which Windows treats as everyone gets everything — a dangerous state that sometimes appears through bugs and is worth flagging when you see it. A blank SACL just means "nothing is being logged," which is common.
Anatomy of a single ACE
Each ACE (one line on the guest list) contains:
- Type —
AlloworDeny(orAuditfor SACLs). - Trustee SID — who the rule is about.
- Access mask — a 32-bit set of flags naming the exact rights (ReadData, WriteData, Delete, WriteDAC, WriteOwner, etc.).
- Inheritance flags — whether and how this rule flows down to child files/folders (
OI,CI,IO,NP).
The access mask is where the friendly bundles from Part 2 dissolve into their real bits. For files, the notable specific rights are:
| Right | Meaning | Abuse relevance |
|---|---|---|
FILE_READ_DATA |
Read contents | Read secrets/config |
FILE_WRITE_DATA |
Overwrite contents | Plant payloads, poison config |
FILE_APPEND_DATA |
Add to end | Log injection |
FILE_EXECUTE |
Run as program | Execute planted binary |
DELETE |
Delete the object | Remove evidence / enable replace |
WRITE_DAC |
Change the ACL | Grant yourself anything |
WRITE_OWNER |
Take ownership | Then change the ACL |
READ_CONTROL |
Read the ACL | Recon of who has access |
WRITE_DAC and WRITE_OWNER are the "meta" rights: they let you edit the rules themselves. In privilege escalation, finding a low-privileged user who holds WRITE_DAC or WRITE_OWNER on a file, service, or registry key that a high-privileged process trusts is often the whole game.
Part 5: How the Bouncer Decides — The Access-Check Algorithm
Now the crucial mechanics: given your token and an object's DACL, exactly how does Windows decide yes or no? The Security Reference Monitor runs a specific, ordered algorithm. Getting the order right is what separates someone who guesses at permissions from someone who can reason about them.
The rules, in order:
- Owner shortcut: if you are the owner, you implicitly get
READ_CONTROLandWRITE_DACno matter what the DACL says. (You can always read and rewrite your own object's rules.) - Null DACL: if there is no DACL at all, access is granted (everyone, everything). An empty DACL (present but zero ACEs) means the opposite — deny everyone. This present-but-empty vs absent distinction trips up even experienced people.
- Walk the ACEs in order. Collect the rights you requested. For each ACE that applies to a SID in your token:
- If it's an Access-Denied ACE matching a right you asked for → immediate DENY, stop.
- If it's an Access-Allowed ACE → accumulate those granted rights.
- After the walk: if every right you requested has been granted by Allow ACEs and none was denied → ALLOW. Otherwise → DENY.
flowchart TD
START["Thread requests access mask (e.g. Read+Write)"] --> OWNER{"Are you the owner?"}
OWNER -->|yes| GRANTOC["Grant READ_CONTROL + WRITE_DAC implicitly"]
OWNER -->|no| NULLD{"DACL present?"}
GRANTOC --> NULLD
NULLD -->|"No DACL (null)"| ALLOWALL["ALLOW everything"]
NULLD -->|"Empty DACL"| DENYALL["DENY (no rules grant anything)"]
NULLD -->|"Has ACEs"| WALK["Walk ACEs top to bottom"]
WALK --> ACE{"ACE type?"}
ACE -->|"Deny matches requested right"| DENY["DENY immediately"]
ACE -->|"Allow"| ACC["Accumulate granted rights"]
ACC --> MORE{"More ACEs?"}
MORE -->|yes| WALK
MORE -->|no| CHECK{"All requested rights granted?"}
CHECK -->|yes| ALLOW["ALLOW"]
CHECK -->|no| DENY2["DENY"]
Why ACE order matters: the canonical order
Because a Deny ACE stops the walk immediately, the order of ACEs changes the outcome. To make behavior predictable, Windows enforces a canonical order when tools write ACLs:
- Explicit Deny ACEs (set directly on this object)
- Explicit Allow ACEs
- Inherited Deny ACEs (from the parent)
- Inherited Allow ACEs
So explicit rules beat inherited ones, and within each group, deny beats allow. This is why you can grant broad access to a folder (inherited Allow) but carve out one user with an explicit Deny on a specific file — the explicit Deny is evaluated first and wins. If ACEs are out of canonical order (which malware or sloppy tooling can create), the effective access can be surprising — a real detection and audit concern.
Interview-level nuance (FAANG). A common misconception is "Deny always beats Allow." Not quite — an explicit Allow beats an inherited Deny, because explicit ACEs are ordered before inherited ones. The precise statement is: earlier ACE in canonical order wins, and canonical order puts explicit-deny first, then explicit-allow, then inherited-deny, then inherited-allow. Being able to state this correctly is a genuine differentiator.
Part 6: Inheritance — Where Permissions Actually Come From
Nobody sets permissions on every file by hand. Instead, folders pass rules down to the files and subfolders inside them. This is inheritance, and misunderstandings about it cause a huge share of real-world permission bugs.
When you set an ACE on a folder, its inheritance flags decide how it propagates:
OI(Object Inherit): applies to files created inside.CI(Container Inherit): applies to subfolders created inside.IO(Inherit Only): the ACE does not apply to the folder itself, only flows to children.NP(No Propagate): children inherit it, but grandchildren do not.
A child object's DACL is therefore usually a mix of inherited ACEs (marked (I) in icacls) and explicit ACEs set directly on it. You can break inheritance on a child (the "Disable inheritance" button, or icacls /inheritance:d), which either copies the inherited rules in as explicit ones or removes them.
flowchart TD
P["C:\\Data (folder)<br/>ACE: Allow Users Modify (OI)(CI)"] --> C1["report.docx<br/>inherits: Users Modify (I)"]
P --> C2["C:\\Data\\HR (subfolder)<br/>inherits: Users Modify (I)"]
C2 --> C3["salaries.xlsx<br/>inherits Users Modify (I)<br/>+ explicit Deny Users (wins!)"]
See inheritance in action with icacls:
icacls C:\DataC:\Data BUILTIN\Administrators:(OI)(CI)(F)
NT AUTHORITY\SYSTEM:(OI)(CI)(F)
BUILTIN\Users:(OI)(CI)(RX)The (OI)(CI) means these rules flow to both files (OI) and subfolders (CI). Now look at a file inside — its rules carry (I) to show they were inherited:
C:\Data\report.docx BUILTIN\Administrators:(I)(F)
NT AUTHORITY\SYSTEM:(I)(F)
BUILTIN\Users:(I)(RX)Common real-world bug. An admin grants a service account
ModifyonC:\Appwith inheritance, intending it for alogssubfolder — but(OI)(CI)flows it to every file and folder underneath, including the app's binaries. Now the service account (often reachable by an attacker) can overwrite the app's.exe. This is the single most common way "convenience" ACLs become privilege escalation. Inheritance is powerful; scope it deliberately.
Part 7: Integrity Levels — A Second Fence Around Every Object
Even if the DACL says "yes," there is a second check: Mandatory Integrity Control (MIC). Introduced with UAC, MIC labels every process and every securable object with an integrity level: Untrusted < Low < Medium < High < System. The rule is simple and one-directional: a lower-integrity process cannot write to a higher-integrity object, even if the DACL would allow it. (Reads and executes are generally allowed; the default policy is "no write-up.")
This is why:
- A web browser's sandboxed renderer runs at Low integrity, so even if it's compromised it cannot modify your Medium-integrity documents or system files.
- A normal admin's Explorer runs at Medium until you approve a UAC prompt, which launches the process at High — only then can it write to
C:\Windows.
You can see and set integrity labels with icacls:
icacls C:\some\file.txt /setintegritylevel High
icacls C:\some\file.txt # a Mandatory Label line appears for non-default levelsA file marked Mandatory Label\High Mandatory Level:(NW) means "no lower-integrity process may write here." MIC is the layer that makes browser sandboxes and UAC meaningful. Attackers care because UAC bypasses and sandbox escapes are fundamentally about getting a higher integrity level than you were given — and some do it by finding a High-integrity process that carelessly reads a Medium-integrity, user-writable file or registry key.
Part 8: The Offensive View — Weak ACLs Are Privilege Escalation
Everything above becomes an attack surface the moment a permission is too generous. On Windows, local privilege escalation is very often "find the one object with a weak ACL and abuse it." Here is the map, tied back to the rights from Part 4. All of this is for systems you own or are authorized to test.
- Writable service binary or folder. If a service runs as
SYSTEMbut its.exe(or a folder in its path) is writable by your user, replace/plant the binary and it runs as SYSTEM at next start. (Rights:FILE_WRITE_DATA/WRITE_OWNERon the file orFILE_ADD_FILEon the dir.) WRITE_DAC/WRITE_OWNERon a privileged object. You can rewrite the ACL to grant yourself Full control, then do whatever you like. This is why those "meta" rights are so dangerous.- Weak registry ACLs. A service's configuration key (
HKLM\SYSTEM\CurrentControlSet\Services\<svc>) writable by a normal user lets you repoint itsImagePath— same idea, registry edition (Chapter 1, Part 7). SeBackupPrivilege/SeRestorePrivilegetokens bypass DACLs entirely for read/write — a token, not an ACL, but it defeats every ACL in this chapter, so it belongs on the map.- Alternate Data Streams (ADS) for hiding payloads (next part).
The standard way to find these is to enumerate ACLs looking for your own low-privileged identity (or broad groups like Users/Authenticated Users/Everyone) holding write-class rights on sensitive targets. The tools that automate it — AccessChk (Sysinternals), winPEAS, PowerUp's Invoke-AllChecks — are all just walking DACLs and comparing SIDs to your token, exactly as you now understand.
:: AccessChk (Sysinternals): does 'Users' have write to anything under Program Files?
accesschk.exe -uwqs Users "C:\Program Files\*"
:: -w write access, -u suppress errors, -q no banner, -s recurseRW C:\Program Files\SloppyApp\service.exe
MEDIUM MANDATORY LEVEL [No-Write-Up]
RW BUILTIN\Users
FILE_ALL_ACCESSThat output — ordinary Users holding FILE_ALL_ACCESS on a service binary — is a textbook SYSTEM-privilege-escalation finding.
Red Team — the ACL-weakness playbook, in order of reliability: (1) writable service binary or its folder → replace it, restart the service → SYSTEM; (2)
WRITE_DAC/WRITE_OWNERon a privileged file/key → rewrite the ACL, then own it; (3) writable.lnk/scheduled-task action that a higher-priv user runs → swap the target; (4) writable%PATH%directory earlier than a system one → DLL/exe hijack (Chapter 1). Enumerate withPowerUp'sInvoke-AllChecks,accesschk -uwqs Users, andwinPEAS; weaponize by planting a benign proof (a file drop as SYSTEM), never real malware, and only where you are authorized. The mechanism in every case is a low-priv SID sitting on an ACL where a write-class or meta right (Part 4) shouldn't be.
Bug Bounty Angle. For installed desktop software (in scope for many vendor VDPs and thick-client programs), weak filesystem/registry ACLs are among the most reliably payable bugs, reported as local privilege escalation. Method: install the app, then run
accesschk.exe -uwqs Users "C:\Program Files\Vendor\*"andaccesschk.exe -uwqs Users "C:\ProgramData\Vendor\*", plus the registry (accesschk -kwqs Users HKLM\SOFTWARE\Vendor). AnyRWforUsers/Authenticated Users/Everyoneon an executable, DLL, scheduled-task action, or service config that a higher-privileged process runs is a finding. Demonstrate impact by planting a benign proof (e.g. a file drop as SYSTEM) — never real malware — and document the exact ACL. Updater services and always-on agents are especially fruitful because they run as SYSTEM and are often installed to weakly-permissionedProgramDatapaths.
Part 9: Alternate Data Streams, Links, and NTFS Tricks
NTFS has a few features that are invisible in everyday use but matter enormously for security. A newcomer can skip nothing here — these show up constantly in CTFs and real incidents.
Alternate Data Streams (ADS)
On NTFS, a file can carry extra, hidden streams of data besides its main content. The main content is technically the stream named $DATA; you can attach others like file.txt:secret. Explorer and a normal dir do not show them, which is exactly why malware and CTF authors love them.
:: Hide data in a stream on an innocent-looking file
echo you found the flag > report.txt:hidden.txt
:: The file still looks 0-ish/normal size; the stream is invisible to plain dir
dir report.txt
:: Reveal streams with /r, and read one back
dir /r report.txt
more < report.txt:hidden.txtreport.txt
report.txt:hidden.txt:$DATA <-- the hidden stream, only shown with /rPowerShell reads them natively:
Get-Item .\report.txt -Stream * # list all streams
Get-Content .\report.txt -Stream hidden.txt # read the hidden oneThe most famous benign ADS is Zone.Identifier — the "Mark of the Web" (MOTW). When you download a file, Windows tags it with a Zone.Identifier stream noting it came from the internet, which is why you get "protected view" and SmartScreen warnings. Attackers try to strip MOTW (extract from certain archive types, or copy to FAT) to make a downloaded payload run without warnings — and defenders hunt for MOTW-stripping.
Get-Content .\installer.exe -Stream Zone.Identifier # ZoneId=3 means "Internet"Hard links, junctions, and symbolic links
NTFS supports several kinds of "this path points at that content" objects. They matter because they enable link-following attacks (redirecting a privileged write to a target the attacker couldn't otherwise touch):
| Type | Command | Points to | Security note |
|---|---|---|---|
| Hard link | mklink /H |
another file (same volume) | Two names, one data; deleting one keeps the other. |
| Symbolic link | mklink |
any file/path | Creating them needs privilege or Dev Mode — because they enable attacks. |
| Junction | mklink /J |
a folder | Classic in privesc: redirect a folder a SYSTEM process writes into. |
One more NTFS detail that pairs with the $MFT from Part 1: every file carries two sets of timestamps — the $STANDARD_INFORMATION set (what Explorer and dir show, and what apps can change) and the $FILE_NAME set (harder to alter). Attackers time-stomp — backdate a dropped file's $STANDARD_INFORMATION times to blend in — but the $FILE_NAME times often stay honest, so a mismatch between the two is a classic tampering tell. You can view the visible set and note the concept:
Get-Item C:\lab\service.exe | Select-Object CreationTime, LastWriteTime, LastAccessTime
# Forensics tools (MFTECmd) parse BOTH $SI and $FN times; a $SI earlier than $FN = stompedCTF Angle. Windows privesc and forensics challenges lean on this part constantly. Fast checklist when you get a Windows foothold or a challenge file: (1)
dir /randGet-Item -Stream *on suspicious files — flags and second-stage payloads hide in ADS; (2) checkZone.Identifierto prove where a file came from; (3) enumerate weak ACLs withaccesschk -uwqs UsersandPowerUp; (4) look for writable service binaries and unquoted paths (Chapter 1). On HackTheBox/TryHackMe, an ADS-hidden flag or aUsers-writable SYSTEM binary is an extremely common intended path. Tooling:accesschk,icacls,PowerUp.ps1,winPEAS; flag format usuallyflag{...}orHTB{...}.
Part 10: Hands-On Lab — Reading, Fixing, and Abusing an ACL (Safely)
This lab runs entirely on a Windows VM you own. It has four parts: read an ACL properly, create a deliberately weak one and detect it, fix it, and demonstrate the hidden-stream trick. Every command is explained.
Setup
# Run as a NORMAL user first (we'll note when admin is needed).
mkdir C:\lab 2>$null
"top secret budget" | Out-File C:\lab\budget.txt
whoami # note your username; we'll refer to it as <you>Part A — Read the ACL three ways
# 1) icacls — the quick professional view
icacls C:\lab\budget.txt
# 2) PowerShell object view — structured, scriptable
Get-Acl C:\lab\budget.txt | Format-List
(Get-Acl C:\lab\budget.txt).Access # one row per ACE: rights, type, SID, inheritance
# 3) Owner specifically
(Get-Acl C:\lab\budget.txt).OwnerRealistic (Get-Acl ...).Access row:
FileSystemRights : ReadAndExecute, Synchronize
AccessControlType : Allow
IdentityReference : BUILTIN\Users
IsInherited : True
InheritanceFlags : NoneRead it as an ACE: Allow — BUILTIN\Users — ReadAndExecute — inherited from the parent folder. You are now reading raw ACLs fluently.
Part B — Create a weak ACL and detect it (the mistake)
# Grant the broad 'Users' group Modify on a "service binary" we simulate.
"fake service" | Out-File C:\lab\service.exe
icacls C:\lab\service.exe /grant "Users:(M)" # <-- the misconfiguration
# Now detect it exactly as an auditor/attacker would:
icacls C:\lab\service.exe
# Look for BUILTIN\Users:(M) or (F) on an executable -> privesc risk
# AccessChk phrasing (install Sysinternals AccessChk first):
# accesschk.exe -uwq Users C:\lab\service.exe -> prints RW if Users can writeExpected icacls after the grant:
C:\lab\service.exe BUILTIN\Administrators:(I)(F)
NT AUTHORITY\SYSTEM:(I)(F)
BUILTIN\Users:(M) <-- explicit, dangerous on an .exeThe absence of (I) on the Users:(M) line tells you it was set explicitly here (not inherited) — a deliberate, and in this case unsafe, grant.
Part C — Fix it (least privilege)
# Remove the over-broad grant; keep only what's needed.
icacls C:\lab\service.exe /remove:g "Users"
icacls C:\lab\service.exe # confirm Users no longer has write
# If inheritance had pushed a bad rule down, you could reset to the parent's intent:
icacls C:\lab\service.exe /reset # re-inherit from parent, dropping explicit ACEsPart D — Ownership and the "take ownership" power
# See the owner, then (as admin) take ownership and note you can now rewrite the DACL.
takeown /f C:\lab\service.exe # makes YOU the owner (needs admin or SeTakeOwnership)
icacls C:\lab\service.exe /setowner "<you>"
# Because the OWNER always has implicit WRITE_DAC, you can now grant anything:
icacls C:\lab\service.exe /grant "<you>:(F)"This is the concrete demonstration of Part 4's warning: owner ⇒ can rewrite the guest list ⇒ effectively full control. In privesc, SeTakeOwnershipPrivilege + this sequence turns "I can't touch that file" into "I own it."
Part E — The hidden stream
# Attach and read a hidden ADS, then prove dir hides it.
Set-Content C:\lab\budget.txt -Stream note -Value "raise everyone's pay"
Get-Item C:\lab\budget.txt -Stream * # shows :note and :$DATA
Get-Content C:\lab\budget.txt -Stream note
cmd /c "dir C:\lab\budget.txt" # note: stream not shown
cmd /c "dir /r C:\lab\budget.txt" # /r reveals :noteClean up:
Remove-Item C:\lab -Recurse -ForcePart F — The same skill on a registry key
ACLs aren't just for files — Part 14 promised the identical model secures registry keys, so let's prove it. PowerShell exposes the registry as a drive, so Get-Acl/Set-Acl work exactly as they did on files. This is the literal mechanism behind a whole class of privilege escalations (a normal user who can write a service's config key).
# Create a lab key and inspect its ACL just like a file:
New-Item -Path 'HKCU:\Software\lab' -Force | Out-Null
Get-Acl 'HKCU:\Software\lab' | Format-List
# Grant a broad right (simulating the misconfig) and read it back:
$acl = Get-Acl 'HKCU:\Software\lab'
$rule = New-Object System.Security.AccessControl.RegistryAccessRule(
"Everyone","SetValue","Allow")
$acl.AddAccessRule($rule); Set-Acl 'HKCU:\Software\lab' $acl
(Get-Acl 'HKCU:\Software\lab').Access |
Where-Object IdentityReference -match 'Everyone' |
Select-Object RegistryRights, AccessControlType
# AccessChk phrasing for registry (note -k for keys):
# accesschk.exe -kwqs Users HKLM\SYSTEM\CurrentControlSet\ServicesExpected:
RegistryRights AccessControlType
-------------- -----------------
SetValue AllowIf that Everyone: SetValue were on HKLM\SYSTEM\CurrentControlSet\Services\<svc> instead of a harmless HKCU test key, a normal user could rewrite the service's ImagePath and gain SYSTEM at next start (Chapter 1, Part 9). Same descriptor, same access check, higher stakes. Clean up: Remove-Item 'HKCU:\Software\lab' -Recurse -Force.
What you proved. You read ACLs three ways, created and detected the most common privesc misconfiguration (broad group with write on an executable), fixed it with least privilege, saw why ownership equals control, and hid/recovered data in an alternate stream — the exact skills used in Windows hardening, pentest privesc, and DFIR triage.
Part 11: The Defensive View — Auditing, SACLs, and Detection
For a blue-teamer or a security engineer hardening a fleet, this chapter's structures are what you monitor and enforce. Four concrete practices:
1. Turn on object-access auditing (SACLs). The SACL from Part 4 is how you log access. Set a SACL to audit, then enable the category, and Windows writes Event ID 4663 ("An attempt was made to access an object") to the Security log whenever the audited access happens.
# Audit successful writes to a sensitive file by Everyone:
$acl = Get-Acl C:\Data\salaries.xlsx
$rule = New-Object System.Security.AccessControl.FileSystemAuditRule(
"Everyone","Write","Success")
$acl.AddAuditRule($rule); Set-Acl C:\Data\salaries.xlsx $acl
# Enable the audit category so the SACL actually produces events:
auditpol /set /subcategory:"File System" /success:enableRelevant Security-log events: 4663 (object access), 4670 (permissions changed), 4907 (auditing settings changed), 4720/4732 (account/group changes that alter who's on ACLs).
A resulting Event ID 4663 looks like this (trimmed) — learn to read it, since it's the primary "who touched the file" record:
An attempt was made to access an object.
Subject:
Account Name: bob
Account Domain: CORP
Object:
Object Type: File
Object Name: C:\Data\salaries.xlsx
Process Information:
Process Name: C:\Program Files\Microsoft Office\...\EXCEL.EXE
Access Request Information:
Accesses: WriteData (or AddFile)
Access Mask: 0x2That single event ties a SID (bob) to an access mask (0x2 = WriteData) on a specific object via a named process — the four nouns of this entire chapter, captured at the moment the bouncer said yes.
2. Baseline and diff ACLs. Export ACLs across a system and compare over time; unexpected new write grants for broad groups are a strong signal (persistence, privesc setup, or ransomware prep).
Get-ChildItem C:\ -Recurse -File -EA SilentlyContinue |
Get-Acl | Select-Object Path, Owner, @{n='Access';e={$_.AccessToString}} |
Export-Csv acl-baseline.csv -NoTypeInformation3. Hunt for the danger patterns. Alert on: Everyone/Users/Authenticated Users with write-class rights on Program Files, Windows, service binaries, or scheduled-task actions; ACLs in non-canonical order; null DACLs; and new WRITE_DAC/WRITE_OWNER grants.
4. Watch Mark-of-the-Web and ADS. Alert when downloaded executables lose their Zone.Identifier, or when uncommon ADS appear on user files — both are evasion signals.
Blue Team — the detection mindset. Access control is prevention; SACL auditing is detection; ACL baselining is change detection. A mature environment uses all three. Sysmon complements the built-in log (e.g. Event ID 2 for file-creation-time changes catches "time-stomping," a common
$MFTanti-forensics move tied to Part 1). The through-line: every alert here is just noticing when the structures in Parts 3–7 change in a way legitimate administration wouldn't.
Part 12: Linux Contrast — So the Model Sticks
If you know Linux permissions, contrasting the two cements both. Linux uses a compact model; Windows uses an expressive one, and the trade-offs matter.
| Question | Linux | Windows |
|---|---|---|
| Who can act? | owner / group / others (3 classes) | any number of users and groups, via ACEs |
| What rights? | r w x (3 bits ×3) |
~14 specific rights per object (Read/Write/Delete/WriteDAC/...) |
| Explicit deny? | not in classic mode (POSIX ACLs add it) | yes — first-class Deny ACEs |
| Inheritance | umask at create; setgid dirs; POSIX-ACL default |
rich OI/CI/IO/NP inheritance flags |
| Ownership power | owner can chmod; root overrides all |
owner implicitly gets WRITE_DAC; SeBackup/SeRestore override |
| "Superuser" bypass | root (UID 0) ignores permission bits |
SYSTEM + SeBackup/SeRestore/SeTakeOwnership bypass DACLs |
| Auditing built in? | via auditd (separate) |
via SACLs in the same descriptor |
The key insight: Linux answers "who" with three fixed classes and "what" with three bits, which is simple but coarse. Windows answers "who" with an unbounded list of SIDs and "what" with a dozen-plus precise rights, which is powerful but far easier to misconfigure. More expressiveness means more ways to make a dangerous mistake — which is exactly why Windows privilege escalation so often comes down to one over-permissive ACE, and why the enumeration tools in Part 8 exist.
Part 13: SDDL — The Text Language of Security Descriptors
So far we've read ACLs in the friendly icacls letters and in PowerShell objects. But Windows also has a compact string representation of an entire security descriptor called SDDL (Security Descriptor Definition Language). You will meet SDDL whenever you look at services (sc sdshow), Group Policy, PowerShell remoting endpoints, scheduled tasks, and event-log channel permissions — so learning to decode it is a real professional skill, and it forces the Part 4–5 concepts to become concrete.
An SDDL string looks intimidating but is just the four parts of a security descriptor written out:
O:BAG:BAD:(A;;FA;;;BA)(A;;0x1200a9;;;BU)(D;;WD;;;S-1-5-...)S:(AU;SAFA;FA;;;WD)
│ │ │ └────────── DACL: a run of ACEs ──────────┘ └── SACL ──┘
│ │ └ D: introduces the DACL
│ └ G:BA = primary group (Built-in Administrators) — legacy
└ O:BA = Owner = Built-in AdministratorsEach ACE inside is itself a semicolon-delimited tuple: (type;flags;rights;object_guid;inherit_object_guid;trustee). You mostly care about three fields — type, rights, and trustee:
| Field | Example values | Meaning |
|---|---|---|
| ACE type | A Allow · D Deny · AU Audit |
Same Allow/Deny/Audit from Part 4 |
| Rights | FA Full · FR read · FW write · FX execute · or hex 0x1200a9 |
The access mask |
| Trustee | BA Admins · SY SYSTEM · BU Users · WD Everyone · AU Auth Users · or a raw SID |
Who the ACE applies to |
Those two-letter trustee/right codes are SID/rights abbreviations ("SDDL well-known strings"). The handful you'll actually reuse:
| Code | Trustee | Code | Right |
|---|---|---|---|
SY |
SYSTEM | FA |
Full access |
BA |
Builtin Administrators | FR |
Generic read |
BU |
Builtin Users | FW |
Generic write |
WD |
Everyone (World) | FX |
Generic execute |
AU |
Authenticated Users | CC/DC/LC/SW/RP/WP |
granular (create/delete/list/…) |
So (A;;FA;;;BA) reads as "Allow — Full Access — to Builtin Administrators." And a dangerous service ACE like (A;;RPWPCR;;;BU) reads as "Allow — start/stop/reconfigure — to ordinary Users," which is a privilege-escalation red flag because WP on a service is SERVICE_CHANGE_CONFIG (repoint the binary — Chapter 1, Part 9).
Generate and parse SDDL yourself:
# Convert a file's descriptor to SDDL:
(Get-Acl C:\Windows\System32\drivers\etc\hosts).Sddl
# Turn an SDDL string back into a readable object (great for auditing service ACLs):
ConvertFrom-SddlString "O:BAG:BAD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;0x1200a9;;;BU)"
# -> lists Owner, Group, and each DiscretionaryAcl entry in plain English:: A service's live descriptor, as you'd see it in the wild:
C:\> sc.exe sdshow Spooler
D:(A;;CCLCSWRPWPDTLOCRRC;;;SY)(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;BA)(A;;CCLCSWLOCRRC;;;IU)(A;;CCLCSWLOCRRC;;;SU)Reading that last line: SYSTEM (SY) and Admins (BA) get broad rights; Interactive/Service users (IU/SU) get only query/enumerate. If you ever saw WP (change config) granted to BU or WD there, that service is a local-privesc vector — this is exactly what AccessChk -c and PowerUp's Get-ModifiableService check, now demystified.
FAANG-level takeaway. SDDL is how security descriptors travel through configuration systems (GPO, DSC, IaC templates, service manifests). Being able to eyeball an SDDL string and spot
WP/FAgranted toWD/BU/AUis a fast, high-value audit skill — you can review hundreds of service and task ACLs from their SDDL without opening a single GUI.
Part 14: Beyond Files — Everything Securable Uses the Same Model
The reason we spent a whole chapter on files is that the exact same security descriptor + token + access-check machinery secures every other object type in Windows. Once you can read a file's ACL, you can read them all — only the specific rights in the access mask change per object type. This generalization is what makes this chapter a foundation rather than a one-off.
mindmap
root((Securable object<br/>= Owner + DACL + SACL))
Files & folders
icacls / Get-Acl
Registry keys
Get-Acl HKLM:\... regedit Permissions
Services
sc sdshow AccessChk -c
Processes & threads
OpenProcess access mask
Scheduled tasks
task XML + SD
Printers & shares
share vs NTFS perms
AD objects
GenericAll, WriteDACL (BloodHound)
Named pipes / kernel objects
pipe security descriptor
The same DACL you now understand secures each of these, with object-specific rights:
| Object | Inspect with | Dangerous grant to a low-priv SID |
|---|---|---|
| Registry key | Get-Acl HKLM:\SOFTWARE\... |
SetValue/WriteDAC on a service/Run key |
| Service | sc sdshow, accesschk -c |
SERVICE_CHANGE_CONFIG (WP) → repoint binary |
| Process | opened via OpenProcess |
PROCESS_VM_WRITE/CREATE_THREAD → injection |
| Scheduled task | task folder ACL + XML | write to the action target → run as task's user |
| File share | share perms and NTFS perms | the more restrictive of the two wins |
That last row is a frequent real-world gotcha worth calling out for newcomers: a network share has two independent permission layers — the share permissions and the underlying NTFS permissions — and the effective access is the intersection (the more restrictive). A folder shared with "Everyone: Full" at the share layer but "Users: Read" at the NTFS layer is still only Read over the network. Both the offensive (why can't I write?) and defensive (why is this exposed?) sides depend on remembering there are two gates.
A worked access-check, end to end
Tie Parts 5, 6 and 13 together with one concrete example. Suppose salaries.xlsx has this DACL (in canonical order), and user bob (a member of Users, not an Admin) requests Write:
1. (D) Deny bob Write <- explicit deny
2. (A) Allow Administrators FullControl <- explicit allow
3. (A) Allow Users Read (I) <- inherited allowThe bouncer walks top-down. ACE 1 is an explicit Deny for a right bob requested (Write) and bob is the trustee → immediate DENY, stop. Bob cannot write, even though he's in Users and even if a later ACE would allow it — because a matching Deny appears first in canonical order. Now flip it: bob requests Read. ACE 1 (Deny Write) doesn't match the requested right (Read), so it's skipped; ACE 2 doesn't apply (bob isn't an Admin); ACE 3 grants Users: Read and bob is in Users → the requested right is satisfied → ALLOW. Same file, same user, different answer depending only on which right was requested and the order of ACEs. That is the whole model, working exactly as Part 5 described.
Part 15: Effective Access — Answering "Can This User Actually Touch It?"
Reading a DACL tells you the rules; it does not directly tell you the answer for a specific person, because a user's real access is the combination of every group they're in, minus every Deny, filtered by integrity level. Computing that by hand across a dozen inherited ACEs is error-prone — so Windows gives you an Effective Access calculator, and it's a skill worth having because "who can really write here?" is the question that matters in both hardening and privesc.
In the GUI: Properties → Security → Advanced → Effective Access tab, pick a user or group, click "View effective access," and Windows simulates the full access check (including group membership and inheritance) and shows a per-right ✔/�’ grid. This is the authoritative "what would the bouncer decide" answer without needing to log in as that user.
From code, reason about it the way the tools do — resolve the token's groups, then evaluate the DACL:
# List every ACE and whether it's inherited, then think in terms of the requesting token's SIDs
(Get-Acl C:\Data\salaries.xlsx).Access |
Select-Object IdentityReference, FileSystemRights, AccessControlType, IsInherited |
Format-Table -Auto
# AccessChk answers effective access directly for a principal:
# does user 'bob' have write to this tree?
accesschk.exe -w "bob" -s "C:\Data" # prints each object bob can writeRealistic accesschk answer:
RW C:\Data\logs
bob FILE_ALL_ACCESS
R C:\Data\salaries.xlsx
bob FILE_GENERIC_READThat output resolves the whole puzzle: bob can write the logs folder (inherited Modify) but only read salaries.xlsx (the explicit Deny from Part 14's example stripped his write). You just watched inheritance, explicit-deny precedence, and group resolution combine into a single real answer — which is exactly the reasoning an auditor documents and an attacker exploits.
Bug Bounty / hardening tip. When auditing an installed app, run
accesschk.exe -w "Authenticated Users" -s "C:\Program Files\Vendor"(and theProgramDataandHKLM\SOFTWARE\Vendorequivalents). Effective-access output for a broad group directly enumerates every writable object a normal user could abuse — no manual ACE math, no false positives from Deny ACEs you missed.
Part 16: Encryption vs Permissions — Closing the Confidentiality Gap
Part 1 warned that NTFS permissions are an access-control mechanism enforced by a running kernel, not a guarantee against someone with raw disk access. This part makes the fix explicit, because conflating the two is one of the most consequential mistakes a newcomer (or an org) can make.
Two encryption controls close the gap, and they solve different threats:
| Control | Protects against | Scope | Key tied to |
|---|---|---|---|
| BitLocker | Whole-disk theft (stolen/discarded drive, offline attack) | Entire volume | TPM + optionally PIN/USB |
| EFS (Encrypting File System) | Other users on the same running machine reading your files | Per file/folder | The user's certificate/keys |
BitLocker encrypts the whole volume, so pulling the drive and reading it in another machine yields ciphertext — it defeats exactly the "ignore the ACLs by using another OS" attack from Part 1. It's transparent once the volume is unlocked at boot (via the TPM). The residual risks are things like DMA/cold-boot attacks and, historically, unattended unlocked machines — but for lost-laptop confidentiality it is the correct control, and permissions alone are not.
EFS encrypts individual files with a per-user key, so even another administrator on the same box can't read them without the user's private key (barring the configured recovery agent). It shows up as green filenames in Explorer. EFS is niche in modern deployments (BitLocker covers most needs) but appears in CTFs and DFIR: recovering EFS-encrypted files hinges on possessing the user's key material, and cipher.exe manages it.
cipher /e /s:C:\secret :: EFS-encrypt a folder tree
cipher /c C:\secret\file.txt :: show which certificates/keys can decrypt it
manage-bde -status C: :: BitLocker state of the volumeThe mental model to carry forward: permissions decide who the running OS lets in; encryption decides who can read the bytes at all. Defense in depth uses both — ACLs for day-to-day least privilege on a live system, BitLocker for the drive-theft/offline threat, and EFS (or app-layer encryption) for per-user confidentiality where required. An interview answer that cleanly separates "access control" from "confidentiality at rest" signals real understanding.
Part 17: Common Pitfalls and Misconceptions
- "Permissions protect my data from anyone." No — they are enforced by a running Windows kernel. Someone with raw disk access (another OS, a pulled drive) can ignore NTFS ACLs. Confidentiality needs encryption (BitLocker/EFS), not just permissions.
- "Deny always wins over Allow." Only within canonical order. An explicit Allow beats an inherited Deny (Part 5). State it precisely.
- "An empty DACL means locked down." Backwards for the null case: no DACL = everyone gets everything; an empty (present) DACL = nobody gets anything. Two very different states.
- "Full control is just a lot of read/write." No — Full control includes
WRITE_DACandWRITE_OWNER, i.e. the power to rewrite the rules and take ownership. That's a categorical jump, not a bigger quantity. - "The owner is whoever created it, and that's cosmetic." The owner has an implicit right to change the DACL forever. Ownership is power, not a label.
- "
icaclsand Explorer show the same thing." Explorer hides specific rights inside friendly bundles and hides some inheritance detail. For security work, trusticacls/Get-Acl/accesschk. - "ADS is exotic and rare."
Zone.Identifieris on nearly every file you download. ADS is everyday NTFS, and a routine hiding spot. - "Integrity levels are the same as permissions." They're a separate gate (MIC) layered on top: a Low process can't write up even with an allowing DACL.
- "Timestamps are trustworthy." The times Explorer shows (
$STANDARD_INFORMATION) are app-writable and routinely time-stomped; corroborate with$FILE_NAMEtimes via$MFTparsing before trusting a file's age (Part 9).
Part 18: Final Revision — The One-Page Mental Model
Read this until it's automatic. Every later Windows chapter (authentication, Active Directory ACLs, GPO, service hardening) builds on it:
- A file lives in NTFS, indexed by the
$MFT; its permissions ride in a Security Descriptor (Owner + DACL + SACL). - Identity is a token of SIDs; permission is a DACL of ACEs; each ACE = (Allow/Deny, SID, access mask, inheritance flags).
- The access check walks ACEs in canonical order (explicit-deny → explicit-allow → inherited-deny → inherited-allow); first match that resolves your requested rights wins; the owner can always rewrite the DACL; null DACL = open, empty DACL = closed.
- Inheritance (
OI/CI/IO/NP) is where most real ACLs come from — and where the most common dangerous mistakes happen. - Integrity levels (MIC) add a "no write-up" fence independent of the DACL; it's what makes UAC and sandboxes real.
- Weak ACLs = privilege escalation: broad groups (
Users/Everyone) with write on service binaries/config, orWRITE_DAC/WRITE_OWNERon privileged objects. Find them withaccesschk/PowerUp/icacls. - NTFS extras — ADS (hidden streams, Mark-of-the-Web) and links/junctions — are everyday features with big security implications.
- Defense = DACLs (prevent) + SACL auditing / Event 4663 (detect) + ACL baselining (detect change).
- Encryption ≠ permissions: BitLocker defeats drive theft/offline reads; EFS gives per-user confidentiality; ACLs alone protect neither against raw-disk access.
Memory hooks: "permissions gatekeep the live OS; encryption gatekeeps the bytes" · "token vs ACL, checked at open" · "owner rewrites the door" · "explicit beats inherited, deny beats allow — in that order" · "null open, empty closed" · "broad group + write on an .exe = SYSTEM."
Part 19: Cheat Sheet
Reading ACLs
icacls C:\path\file # quick view (F/M/RX/R/W, (I) inherited)
Get-Acl C:\path\file | Format-List # structured
(Get-Acl C:\path\file).Access # one row per ACE
(Get-Acl C:\path\file).Owner # owner SID/name
accesschk.exe -uwqs Users "C:\Program Files\*" # who-can-write hunt (Sysinternals)Changing ACLs
icacls file /grant "User:(M)" # grant Modify
icacls file /remove:g "User" # remove all granted ACEs for User
icacls file /deny "User:(W)" # explicit deny write
icacls folder /grant "User:(OI)(CI)(RX)" # grant with inheritance to files+subfolders
icacls file /inheritance:d # disable inheritance (copy or remove)
icacls file /reset # re-inherit from parent (drop explicit ACEs)
icacls file /setowner "User" # set owner (needs privilege)
takeown /f file # take ownership (admin/SeTakeOwnership)
icacls file /setintegritylevel High # set MIC labelAlternate Data Streams
dir /r file # reveal streams (cmd)
Get-Item file -Stream * # list streams (PowerShell)
Get-Content file -Stream name # read a stream
Get-Content file -Stream Zone.Identifier # Mark-of-the-Web (ZoneId=3 = Internet)Key SIDs: S-1-5-18 SYSTEM · S-1-5-32-544 Administrators · S-1-5-32-545 Users · S-1-1-0 Everyone · S-1-5-11 Authenticated Users · S-1-3-0 Creator Owner.
icacls letters: F=Full · M=Modify · RX=Read+Execute · R=Read · W=Write · D=Delete · (I)=inherited · (OI)=object-inherit · (CI)=container-inherit · (IO)=inherit-only · (NP)=no-propagate.
Access-check order (canonical): explicit Deny → explicit Allow → inherited Deny → inherited Allow. Owner always gets implicit WRITE_DAC. Null DACL = allow-all; empty DACL = deny-all.
Audit events: 4663 (object access) · 4670 (perms changed) · 4907 (audit settings changed) · 4720/4732 (account/group changes).
Part 20: Practice Labs & Resources
Train these skills on real, legal targets:
- TryHackMe — "Windows PrivEsc" and "Windows PrivEsc Arena": weak service ACLs, writable binaries, registry ACLs — the entire Part 8 skill set, hands-on.
- TryHackMe — "Windows Fundamentals 2": NTFS, permissions, and the Properties/Security dialog for absolute beginners.
- HackTheBox — Optimum, Devel, Access, Bastard: classic footholds where an ACL or writable path is the intended escalation.
- HackTheBox Academy — "Windows Privilege Escalation" module: structured coverage of ACL-based privesc matching Parts 4–8.
- PortSwigger / general: not web-focused here, but pair with Sysinternals
AccessChkandicaclspractice on your own VM until reading an ACL is instant. - Tools to learn by using:
icacls,Get-Acl/Set-Acl, Sysinternals AccessChk, PowerUp.ps1 (Invoke-AllChecks), winPEAS, MFTECmd (Eric Zimmerman) for$MFTforensics. - MITRE ATT&CK to read alongside: T1222 (File and Directory Permissions Modification), T1564.004 (Hide Artifacts: NTFS ADS), T1548 (Abuse Elevation Control Mechanism), T1553.005 (Subvert Trust Controls: Mark-of-the-Web).
- Reference: Windows Internals, 7th ed. (Russinovich et al.), the "Security" chapter, for the authoritative treatment of tokens, SIDs, ACLs and the access check.
Practice questions:
-
In plain English, then in precise terms, explain why an explicit Allow can beat an inherited Deny. Reference the canonical order.
-
You find
BUILTIN\Users:(M)(not inherited) onC:\Program Files\App\updater.exe, and the updater runs as SYSTEM. Describe the escalation and the exacticacls/accesschkcommands you'd use to confirm it. -
What is the difference between a null DACL and an empty DACL, and why is one dangerous?
-
A downloaded
.exeruns with no SmartScreen warning even though it came from the internet. Which NTFS feature was likely stripped, and how would you check for it? -
Give two reasons NTFS permissions do not protect data against an attacker who removes the disk, and name the correct control that does.
-
Decode this service SDDL ACE and say whether it's a privesc risk for a normal user:
(A;;RPWPCR;;;BU). Which single right makes it dangerous, and what would you do with it?