Test-Acl

Posted: May 8, 2017 in PowerShell, Security
Tags: , , ,

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:
    1. Remember the SDDL representation of the SD
    2. Add the ACE’s access to the SD
    3. 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.

Comments
  1. Noah Sherwin says:

    Awesome! This helped me figure out how to re-apply “ALL RESTRICTED APPLICATION PACKAGES”

  2. Noah Sherwin says:

    Awesome! This helped me figure out how to re-apply “ALL RESTRICTED APPLICATION PACKAGES” to a UWP app folder that was missing this principal.

Leave a comment