The other day I needed a way to test DACL and SACL entries for some files, registry keys, and Active Directory objects. I needed a way to make sure there wasn’t any extra access being granted to users, to make sure certain principals weren’t granted any access at all, and to be able to ensure that certain access was audited.
If you’ve ever tried to validate that sort of thing, I’m sure you would agree that to do it right is no easy task. Access control in Windows is an incredibly flexible, but complicated, topic.
For my first stab at it, I turned to Get-PacAccessControlEntry, but quickly found the boilerplate code I was copy/pasting for the different checks was huge. So I of course made a simple function to try to reduce the duplicate code. This ended up being terrible because I kept having to tweak the function, and even when it worked how I wanted it to, crafting the inputs was way too ugly, since creating ACE objects requires a lot of text (even if you use New-PacAccessControlEntry), and that makes it hard to read.
Then it hit me: the .NET access control methods are perfect for this scenario. When you call Get-Acl, you get back a very versatile in-memory representation of a security descriptor (SD). The object has a few different methods that allow you to add or remove access or audit rights. Notice I said rights, and not entries. While the methods to modify access control take access control entries (ACEs) as input, they don’t actually take those ACEs and append or remove them from the access control lists (ACLs) on the SD (well, the methods that end with ‘Specific’ do actually just add/remove entries, but the AddAccessRule, AddAuditRule, RemoveAccessRule, RemoveAuditRule don’t). They actually look at the input ACE, then, to steal a Star Trek and DSC term, “make it so”.
This is AMAZING, because, as I said, access control is complicated. ACEs contain all of this information:
- Principal
- AccessMask
- Flags
- AceType (Allow/Deny access or Audit)
- Inheritance flags
- Propagation flags
- (Optional) Active Directory object information
- Object ACE type GUID
- Inherited object ACE type GUID
- Callback information (The .NET methods don’t actually handle this)
I promise you don’t want to deal with that stuff. Here’s some output from a PS session that hopefully demos what I’m talking about when I say that the methods just take your ACE and make it so:
# Start with a blank SD:
PS C:\> $SD = [System.Security.AccessControl.DirectorySecurity]::new()
PS C:\> $SD.SetSecurityDescriptorSddlForm('D:')
# Add an ACE granting Users Modify rights:
PS C:\> $Ace = [System.Security.AccessControl.FileSystemAccessRule]::new('Users', 'Modify', 'ContainerInherit, ObjectInherit', 'None', 'Allow')
PS C:\> $SD.AddAccessRule($Ace)
PS C:\> $SD | Get-PacAccessControlEntry
Path : (Coerced from .NET DirectorySecurity object)
Owner :
Inheritance: DACL Inheritance Enabled
AceType Principal AccessMask AppliesTo
------- --------- ---------- ---------
Allow Users Modify, Synchronize O CC CO
# Notice that if we add it multiple times, there's no effect on the DACL
PS C:\> $SD.AddAccessRule($Ace)
PS C:\> $SD.AddAccessRule($Ace)
PS C:\> $SD | Get-PacAccessControlEntry
Path : (Coerced from .NET DirectorySecurity object)
Owner :
Inheritance: DACL Inheritance Enabled
AceType Principal AccessMask AppliesTo
------- --------- ---------- ---------
Allow Users Modify, Synchronize O CC CO
# That applies to a folder, its subfolders, and its subfiles. What if we wanted
# to remove the ability to delete the folder and subfolders?
PS C:\> $Ace = [System.Security.AccessControl.FileSystemAccessRule]::new('Users', 'Delete', 'ContainerInherit', 'None', 'Allow')
PS C:\> $SD.RemoveAccessRule($Ace)
True
PS C:\> $SD | Get-PacAccessControlEntry
Path : (Coerced from .NET DirectorySecurity object)
Owner :
Inheritance: DACL Inheritance Enabled
AceType Principal AccessMask AppliesTo
------- --------- ---------- ---------
Allow Users Write, ReadAndExecute, Synchronize O CC CO
Allow Users Delete CO
Removing access took us from one ACE to two! If you look, you’ll see that there’s one ACE granting Write, ReadAndExecute, and Synchronize to the folder, subfolders, and files, and another granting Delete just to files. It removed the access I wanted, and it took all of the ACE components into account for me.
How does this help with the original problem of validating SDs? I mentioned three scenarios above. Here they are again, and with a way to use the .NET SD concept to handle each one.
- Required Access: For each required ACE, do this:
- Remember the SDDL representation of the SD
- Add the ACE’s access to the SD
- Check the SDDL against the remembered value. If there’s no change, you know that the ACE was already in the SD. If there is a change, the test failed. If you want to know all ACEs that fail, you could reset the SD with your starting SDDL and repeat.NOTE: It turns out this doesn’t work well when the Inheritance/Propagation flags aren’t the default. The SD’s structure can change sometimes, even keeping the same effective access. Not to worry, though: we’ll be able to fix it so these false negatives don’t happen.
- Disallowed Access (blacklist): I originally wanted to do something similar to -RequiredAccess, but it ended up being more trouble than it was worth. Instead, I made a helper function to do this for me, and it will eventually be used to fix the problem mentioned above with -RequiredAccess.
- Allowed Access (whitelist): You can take the list of allowed ACEs and remove each one from the SD representation. If the DACL/SACL is empty after doing that, then you know that only access defined in your allowed ACEs list was specified, so the SD passed the test. This has the added benefit of immediately telling you the access that wasn’t allowed (just look at the ACEs in the DACL/SACL.
You’d have to make a decision on how to treat Deny ACEs (I’m leaning to ignoring them by default)
I’m skipping lots and lots of details there, like figuring out if the ACEs are for the DACL or SACL, and what to do with Deny DACL ACEs. You also have to fix the fact that inherited ACEs won’t get removed. But it’s a start 🙂
I took those ideas, and came up with TestAcl, which is a module that exports one command: Test-Acl
. This test module doesn’t depend on the PAC module, even though I plan on putting every bit of this functionality into the module.
One of the coolest things about it is that you provide the rules in string form. The README on the project page covers the syntax, but here are a few examples:
# Look at the DACL for C:\Windows
PS C:\> Get-PacAccessControlEntry C:\Windows
Path : C:\Windows
Owner : NT SERVICE\TrustedInstaller
Inheritance: DACL Inheritance Disabled
AceType Principal AccessMask AppliesTo
------- --------- ---------- ---------
Allow CREATOR OWNER FullControl CC CO
Allow SYSTEM FullControl CC CO
Allow SYSTEM Modify, Synchronize O
Allow Administrators FullControl CC CO
Allow Administrators Modify, Synchronize O
Allow Users ReadAndExecute, Synchronize O CC CO
Allow NT SERVICE\TrustedInstaller FullControl O CC
Allow ALL APPLICATION PACKAGES ReadAndExecute, Synchronize O CC CO
Allow ALL RESTRICTED APPLICATION PACKAGES ReadAndExecute, Synchronize O CC CO
# Notice the comma separated principals and the wildcards
PS C:\> Test-Acl C:\Windows -AllowedAccess '
Allow "CREATOR OWNER", SYSTEM, Administrators, "NT SERVICE\TrustedInstaller" FullControl
Allow * ReadAndExecute
' -DisallowedAccess '
Allow Everyone FullControl
'
True
# Take out TrustedInstaller and see what happens:
PS C:\> $Results = Test-Acl C:\Windows -AllowedAccess '
Allow "CREATOR OWNER", SYSTEM, Administrators FullControl
Allow * ReadAndExecute
' -DisallowedAccess '
Allow Everyone FullControl
' -Detailed
PS C:\> $Results.Result
False
# Ignore the Format-List properties. A future update will handle string representation.
PS C:\> $Results.ExtraAces | fl AceType, @{N='Principal'; E={$_.SecurityIdentifier.Translate([System.Security.Principal.NTAccount])}}, @{N='Rights'; E={$_.AccessMask -as [System.Security.AccessControl.FileSystemRights]}}
AceType : AccessAllowed
Principal : NT SERVICE\TrustedInstaller
Rights : DeleteSubdirectoriesAndFiles, Write, Delete, ChangePermissions, TakeOwnership
# Having to specify O, CC for registry keys is a bug that will be fixed later
PS C:\> Test-Acl HKCU:\SOFTWARE\Subkey -RequiredAccess '
Audit F Everyone RegistryRights: FullControl O, CC
'
True
You can even provide AD object and inherited object GUIDs for object ACEs (see the README on GitHub). It shouldn’t be too hard to extend the parser to make it so you can do something like this, too:
Allow SELF ActiveDirectoryRights: WriteProperty (Public-Information) O, CC (user)
That way you wouldn’t have to look the GUIDs up. For now, though, you can just add the comma separated GUIDs at the end of the string if you need to work with AD object ACEs.
It’s still definitely a work in progress, but I’d love it if people could test it out and provide some feedback and/or contribute to it.