DCSync can be categorized as one of the most effective technique on a domain, since retrieving the krbtgt NT hash allows to forge a valid ticket-granting ticket for any domain user. To manage to do so, a principal must possess the right privileges over the root of a domain: DS-Replication-Get-Changes and DS-Replication-Get-Changes-All. The combination renders it possible to successfully call MS-DRSR’s GetNCChanges to replicate secret attributes.

Recently, I stumbled upon a principal with only DS-Replication-Get-Changes over a domain, which lead to investigating the impact, along with its sibling DS-Replication-Get-Changes-In-Filtered-Set.

This post is dedicated to presenting the consequences of delegating these privileges, and includes a proof of concept PowerShell module to demonstrate the technique for defensive and adversarial simulation purposes.

A Quick Glance at What Can Be Done

Describing Concepts

Before we move on, some Active Directory concepts must first be described.

The searchFlags Attribute

The Schema Naming Context, located at CN=Schema,CN=Configuration,DC=contoso,DC=com contains an object for all attributes that can be set on objects found in Active Directory. Just as regular objects, these objects also have attributes set on them, effectively having attributes on an attribute object.

To make sense out of this, let us look at this concrete example.

At CN=ms-Mcs-AdmPwd,CN=Schema,CN=Configuration,DC=contoso,DC=com resides the object of an attribute that you may be familiar with: ms-Mcs-AdmPwd. This attribute is used to store the password of the local administrator managed by LAPS. When viewing its object, we can see some attributes that are either set or unset, and more specifically in our case, searchFlags:

searchFlags (CN=Search-Flags) includes an important feature: it can dictate whether an attribute is to be shown to a user that requests to see its value. Indeed, when the searchFlags attribute contains the flag fCONFIDENTIAL (0x00000080), a user requesting to see the value must have the explicit privileges to read it (RIGHT_DS_READ_PROPERTY and RIGHT_DS_CONTROL_ACCESS).

Going back to our example, since ms-Mcs-AdmPwd’s searchFlags has the flag fCONFIDENTIAL, a user that wants to read the attribute ms-Mcs-AdmPwd on a computer object must have RIGHT_DS_READ_PROPERTY and RIGHT_DS_CONTROL_ACCESS on it; otherwise, the attribute will be returned as empty. By doing so, LAPS ensures that only principals with the right privileges delegated over computer objects can read the attribute.

One last flag supported by searchFlags worth mentioning is fRODCFilteredAttribute (0x00000200). Essentially, this states that an attribute cannot be replicated to a RODC. Note that ms-Mcs-AdmPwd also has this flag, and will come into play later on.

LDAP Extended Controls

LDAP Extended Controls, often simply named LDAP Controls, is a feature that was introduced in LDAPv3. By specifically crafting a request message with an object identifier (OID), a Domain Controller (DC) can be asked to perform a certain set of operations. For instance, the control named LDAP_SERVER_CROSSDOM_MOVE_TARGET_OID (OID 1.2.840.113556.1.4.521) is used to request that a DC moves an object to another domain.

LDAP_SERVER_DIRSYNC_OID (1.2.840.113556.1.4.841)

This control is used to retrieve from a DC any changes made to Active Directory objects since the last time a synchronization was requested, and is the technique that will be used to read the values of attributes with the flags fCONFIDENTIAL and fRODCFilteredAttribute.

The documentation also contains a valuable piece of information: pseudocode of the security check performed when a directory synchronization is requested:

if AccessCheckCAR(msgIn.pNC, Ds-Replication-Get-Changes) = false then
   return insufficientAccessRights
endif
 
if msgIn.pPartialAttrSet.cAttrs  0 and
   IsFilteredAttributePresent(msgIn.pPartialAttrSet) = true and
   AccessCheckCAR(msgIn.pNC, 
                  Ds-Replication-Get-Changes-In-Filtered-Set) = false and
   AccessCheckCAR(msgIn.pNC, 
                  Ds-Replication-Get-Changes-All) = false
then
  return insufficientAccessRights
endif
 
return 0 /* success */

The Technique

Experimenting With LDAP_SERVER_DIRSYNC_OID

After going through some documentation, interesting points arise:

  • .NET’s System.DirectoryServices.Protocols.DirSyncRequestControl is an easy way to request a directory synchronization, and an example script can be found here.
  • The flag LDAP_DIRSYNC_OBJECT_SECURITY can be used to request a synchronization without having the DS-Replication-Get-Changes privileges, but only returns attributes that the requester can read. Since we wish to read attributes that we normally cannot, we will avoid sending this.
  • An LDAP filter can be specified to only synchronize objects that match it.
  • We can also request a specific set of attributes to be synchronized.

Before executing the aforementioned example script, the following has been altered:

  • The script handles a cookie to only get the changes made since our last request, but has been removed since this serves us no purpose.
  • Filtering is done on (samaccountname=Administrator).
  • We only print a few attributes from the response for the sake of clarity, despite requesting all of them.
Add-Type -AssemblyName System.DirectoryServices.Protocols

$RootDSE = [ADSI]"LDAP://RootDSE"
$LDAPConnection = New-Object System.DirectoryServices.Protocols.LDAPConnection($RootDSE.dnsHostName)
$Request = New-Object System.DirectoryServices.Protocols.SearchRequest($RootDSE.defaultNamingContext, "(samaccountname=Administrator)", "Subtree", $null)
$DirSyncRC = New-Object System.DirectoryServices.Protocols.DirSyncRequestControl
$Request.Controls.Add($DirSyncRC) | Out-Null

$Response = $LDAPConnection.SendRequest($Request)
$Attributes = $Response.Entries.Attributes

"samaccountname: " + $Attributes['samaccountname'][0]
"description: " + $Attributes['description'][0]
"objectcategory: " + $Attributes['objectcategory'][0]

Once the executing user has been delegated DS-Replication-Get-Changes, the directory synchronization succeeds:

Without these delegated privileges, the SendRequest function fails, and returns the error message The user has insufficient access rights, as we also saw from the security check pseudocode.

So far so good, but nothing impressive; these can be retrieved with a standard LDAP query.

Accessing Confidential Attributes With DS-Replication-Get-Changes

In the previous searchFlags section, we have established that some attributes are confidential, and cannot be read without having explicit privileges over the object. While some attributes are confidential by default, it is possible to alter existing searchFlags to set some other attributes to confidential, or to create your own confidential attribute.

You can query for confidential attributes like so:

Get-AdObject -SearchBase 'CN=Schema,CN=Configuration,DC=contoso,DC=com' -LdapFilter '(&(searchflags:1.2.840.113556.1.4.804:=128)(!(searchflags:1.2.840.113556.1.4.804:=512)))'
  • 1.2.840.113556.1.4.804 is a bitwise OR operator ensuring that we look for the flag fCONFIDENTIAL within all the other flags, and not just this value.
  • searchFlags 128 (0x00000080) refers to fCONFIDENTIAL.
  • We exclude 512 (0x00000200) indicating fRODCFilteredAttribute, requiring more privileges.

As a proof of concept, the attribute unixUserPassword will be used. It is sometimes used to store a user’s UNIX password hash.

In our example, when querying for this attribute through a regular LDAP query for the user Administrator, nothing is returned, confirming that we do not have the right privileges over the object:

(Get-ADUser -Identity Administrator -Properties *).unixUserPassword

However, if we modify our script to print this attribute, we see that it was indeed synchronized:

Accessing RODC Filtered Attributes With DS-Replication-Get-Changes-In-Filtered-Set

In order to successfully use these privileges, one must also have DS-Replication-Get-Changes, otherwise the LDAP control will deny the request, as denoted in the pseudocode.

Similarly to the last section, we can query for attributes with the flag fRODCFilteredAttribute. We do not need to exclude confidential attributes; we also have the required privileges for these:

Get-AdObject -SearchBase 'CN=Schema,CN=Configuration,DC=contoso,DC=com' -LdapFilter '(searchflags:1.2.840.113556.1.4.804:=512)'

Other than ms-Mcs-AdmPwd, these attributes should all be present in a modern, vanilla installation of Active Directory.

Let us focus on ms-Mcs-AdmPwd. As briefly mentioned in the impact section, this attribute is used by LAPS to store the current password of the managed local administrator, which will either be the default Administrator account, or a custom one.

With our new privileges, we attempt to retrieve the attribute of a LAPS-enabled computer object through a LDAP query, but to no avail:

(Get-ADComputer -Identity wks1$ -Properties *).'ms-Mcs-AdmPwd'

What if we mimic the same procedure as before, and simply print the attribute through our script? The value of the attribute turns out to be empty.

To successfully get the value of a fRODCFilteredAttribute attribute, we must explicitly specify that we wish to synchronize it when constructing the SearchRequest. It can be done by replacing the $null value in the SearchRequest object of our previous script to an array containing the list of attributes, for instance ('ms-Mcs-AdmPwd'), that we desire to fetch.

Once edited and executed again, the password is printed:

If attempting to run this modified script without DS-Replication-Get-Changes-In-Filtered-Set, we will be greeted with the same error message The user has insufficient access rights as before, due to attempting to synchronize a specific attribute with fRODCFilteredAttribute. Again, this is on par with what was written in the security check pseudocode.

DirSync: A Proof of Concept Tool

DirSync is a simple proof of concept PowerShell module to demonstrate the impact of delegating these privileges. It offers two functions: Sync-LAPS to directly focus on LAPS, and Sync-Attributes to synchronize any desired attributes. Usage can be found in the README.md file, and via the usual Get-Help PowerShell command.

A Remark for Blue Teamers

Keep in mind that this technique uses an LDAP request, and not RPC like DCSync. While the tool is designed by default to communicate over plain text for compatibility reasons, it also supports LDAPS. If your DCs have a certificate installed allowing to use LDAPS, do not rely on over-the-wire detection, unless you hold decryption capabilities.

The Wireshark website contains an example capture file of the DirSync control over plain LDAP, filtering on (ObjectClass=*) and requesting no specific attributes.

Some Unorganized Notes

  • It might very well be possible to achieve the same result with these privileges using MS-DRSR’s GetNCChanges.
  • I did not manage to read secret attributes such as unicodePwd using DS-Replication-Get-Changes-All through the DirSync LDAP control.
  • As mentioned in the pseudocode, DS-Replication-Get-Changes-All can be used instead of DS-Replication-Get-Changes-In-Filtered-Set to read RODC filtered attributes. As you can also perform a full-blown DCSync with those privileges, I do not expect DirSync to be the technique of choice, unless it offers some operational security.
  • There are multiple ways to track changes in a directory.
  • I have not investigated the significance of being able to read the built-in confidential and RODC filtered attributes.

To Conclude

While clearly not as impactful as DCSync, DirSync presents an alternative to synchronize confidential and RODC filtered attributes. The demonstrated LAPS scenario could be used to introduce a new edge into BloodHound, and is a good candidate to look for during Active Directory privileges assessments.

A thank you to @joewaredotnet for the article Replicating Changes Control Access Right series that contributed to my curiousness.