Like many Windows related technologies Active Directory uses a security descriptor and the access check process to determine what access a user has to parts of the directory. Each object in the directory contains an nTSecurityDescriptor attribute which stores the binary representation of the security descriptor. When a user accesses the object through LDAP the remote user's token is used with the security descriptor to determine if they have the rights to perform the operation they're requesting.
Weak security descriptors is a common misconfiguration that could result in the entire domain being compromised. Therefore it's important for an administrator to be able to find and remediate security weaknesses. Unfortunately Microsoft doesn't provide a means for an administrator to audit the security of AD, at least in any default tool I know of. There is third-party tooling, such as Bloodhound, which will perform this analysis offline but from reading the implementation of the checking they don't tend to use the real access check APIs and so likely miss some misconfigurations.
I wrote my own access checker for AD which is included in my NtObjectManager PowerShell module. I've used it to find a few vulnerabilities, such as CVE-2021-34470 which was an issue with Exchange's changes to AD. This works "online", as in you need to have an active account in the domain to run it, however AFAIK it should provide the most accurate results if what you're interested in what access an specific user has to AD objects. While the command is available in the module it's perhaps not immediately obvious how to use it an interpret the result, therefore I decide I should write a quick blog post about it.
A Complex Process
The access check process is mostly documented by Microsoft in
[MS-ADTS]: Active Directory Technical Specification. Specifically in section
5.1.3. However, this leaves many questions unanswered. I'm not going to go through how it works in full either, but let me give a quick overview. I'm going to assume you have a basic knowledge of the structure of the AD and its objects.
An AD object contains many resources that access might want to be granted or denied on for a particular user. For example you might want to allow the user to create only certain types of child objects, or only modify certain attributes. There are many ways that Microsoft could have implemented security, but they decided on extending the ACL format to introduce the object ACE. For example the
ACCESS_ALLOWED_OBJECT_ACE structure adds two GUIDs to the normal
ACCESS_ALLOWED_ACE.
The first GUID, ObjectType indicates the type of object that the ACE applies to. For example this can be set to the schema ID of an attribute and the ACE will grant access to only that attribute nothing else. The second GUID, InheritedObjectType is only used during ACL inheritance. It represents the schema ID of the object's class that is allowed to inherit this ACE. For example if it's set to the schema ID of the computer class, then the ACE will only be inherited if such a class is created, it will not be if say a user object is created instead. We only need to care about the first of these GUIDs when doing an access check.
To perform an access check you need to use an API such as
AccessCheckByType which supports checking the object ACEs. When calling the API you pass a list of object type GUIDs you want to check for access on. When processing the DACL if an ACE has an
ObjectType GUID which isn't in the passed list it'll be ignored. Otherwise it'll be handled according to the normal access check rules. If the ACE isn't an object ACE then it'll also be processed.
If all you want to do is check if a local user has access to a specific object or attribute then it's pretty simple. Just get the access token for that user, add the object's GUID to the list and call the access check API. The resulting granted access can be one of the following specific access rights, not the names in parenthesis are the ones I use in the PowerShell module for simplicity:
- ACTRL_DS_CREATE_CHILD (CreateChild) - Create a new child object
- ACTRL_DS_DELETE_CHILD (DeleteChild) - Delete a child object
- ACTRL_DS_LIST (List) - Enumerate child objects
- ACTRL_DS_SELF (Self) - Grant a write-validated extended right
- ACTRL_DS_READ_PROP (ReadProp) - Read an attribute
- ACTRL_DS_WRITE_PROP (WriteProp) - Write an attribute
- ACTRL_DS_DELETE_TREE (DeleteTree) - Delete a tree of objects
- ACTRL_DS_LIST_OBJECT (ListObject) - List a tree of objects
- ACTRL_DS_CONTROL_ACCESS (ControlAccess) - Grant a control extended right
You can also be granted standard rights such as READ_CONTROL, WRITE_DAC or DELETE which do what you'd expect them to do. However, if you want see what the maximum granted access on the DC would be it's slightly more difficult. We have the following problems:
- The list of groups granted to a local user is unlikely to match what they're granted on the DC where the real access check takes place.
- AccessCheckByType only returns a single granted access value, if we have a lot of object types to test it'd be quick expensive to call 100s if not 1000s of times for a single security descriptor.
While you could solve the first problem by having sufficient local privileges to manually create an access token and the second by using an API which returns a list of granted access such as
AccessCheckByTypeResultList there's an "simpler" solution. You can use the Authz APIs, these allow you to manually build a security context with any groups you like without needing to create an access token and the
AuthzAccessCheck API supports returning a list of granted access for each object in the type list. It just so happens that this API is the one used by the AD LDAP server itself.
Therefore to perform a "correct" maximum access check you need to do the following steps.
- Enumerate the user's group list for the DC from the AD. Local group assignments are stored in the directory's CN=Builtin container.
- Build an Authz security context with the group list.
- Read a directory object's security descriptor.
- Read the object's schema class and build a list of specific schema objects to check:
- All attributes from the class and its super, auxiliary and dynamic auxiliary classes.
- All allowable child object classes
- All assignable control, write-validated and property set extended rights.
- Convert the gathered schema information into the object type list for the access check.
- Run the access check and handled the results.
- Repeat from 3 for every object you want to check.
Trust me when I say this process is actually easier said than done. There's many nuances that just produce surprising results, I guess this is why most tooling just doesn't bother. Also my code includes a fair amount of knowledge gathered from reverse engineering the real implementation, but I'm sure I could have missed something.
Using Get-AccessibleDsObject and Interpreting the Results
Let's finally get to using the PowerShell command which is the real purpose of this blog post. For a simple check run the following command. This can take a while on the first run to gather information about the domain and the user.
PS> Get-AccessibleDsObject -NamingContext Default
Name ObjectClass UserName Modifiable Controllable
---- ----------- -------- ---------- ------------
domain domainDNS DOMAIN\alice False True
This uses the NamingContext property to specify what object to check. The property allows you to easily specify the three main directories, Default, Configuration and Schema. You can also use the DistinguishedName property to specify an explicit DN. Also the Domain property is used to specify the domain for the LDAP server if you don't want to inspect the current user's domain. You can also specify the Recurse property to recursively enumerate objects, in this case we just access check the root object.
The access check defaults to using the current user's groups, based on what they would be on the DC. This is obviously important, especially if the current user is a local administrator as they wouldn't be guaranteed to have administrator rights on the DC. You can specify different users to check either by SID using the UserSid property, or names using the UserName property. These properties can take multiple values which will run multiple checks against the list of enumerated objects. For example to check using the domain administrator you could do the following:
PS> Get-AccessibleDsObject -NamingContext Default -UserName DOMAIN\Administrator
Name ObjectClass UserName Modifiable Controllable
---- ----------- -------- ---------- ------------
domain domainDNS DOMAIN\Administrator True True
The basic table format for the access check results shows give columns, the common name of the object, it's schema class, the user that was checked and whether the access check resulted in any modifiable or controllable access being granted. Modifiable is things like being able to write attributes or create/delete child objects. Controllable indicates one or more controllable extended right was granted to the user, such as allowing the user's password to be changed.
As this is PowerShell the access check result is an object with many properties. The following properties are probably the ones of most interest when determining what access is granted to the user.
- GrantedAccess - The granted access when only specifying the object's schema class during the check. If an access is granted at this level it'd apply to all values of that type, for example if WriteProp is granted then any attribute in the object can be written by the user.
- WritableAttributes - The list of attributes a user can modify.
- WritablePropertySets - The list of writable property sets a user can modify. Note that this is more for information purposes, the modifiable attributes will also be in the WritableAttributes property which is going to be easier to inspect.
- GrantedControl - The list of control extended rights granted to a user.
- GrantedWriteValidated - The list of write validated extended rights granted to a user.
- CreateableClasses - The list of child object classes that can be created.
- DeletableClasses - The list of child object classes that can be deleted.
- DistinguishedName - The full DN of the object.
- SecurityDescriptor - The security descriptor used for the check.
- TokenInfo - The user's information used in the check, such as the list of groups.
The command should be pretty easy to use. That said it does come with a few caveats. First you can only use the command with direct access to the AD using a domain account. Technically there's no reason you couldn't implement a gatherer like Bloodhound and doing the access check offline, but I just don't. I've not tested it in weirder setups such as complex domain hierarchies or RODCs.
If you're using a low-privileged user there's likely to be AD objects that you can't enumerate or read the security descriptor from. This means the results are going to depend on the user you use to enumerate with. The best results would be using a domain/enterprise administrator will full access to everything.
Based on my testing when I've found an access being granted to a user that seems to be real, however it's possible I'm not always 100% correct or that I'm missing accesses. Also it's worth noting that just having access doesn't mean there's not some extra checking done by the LDAP server. For example there's an explicit block on creating
Group Managed Service Accounts in
Computer objects, even though that will seem to be a granted child object.