Stuff I've learned along the way
Active Directory has several attributes that store permissions. The attribute type is called “NT Security Descriptor”, or String(NT-Sec-Desc). These are the attributes I know of:
fRSRootSecurity
msDFS-LinkSecurityDescriptorv2
msDS-AllowedToActOnBehalfOfOtherIdentity
msDS-GroupMSAMembership
nTSecurityDescriptor
pKIEnrollmentAccess
The nTSecurityDescriptor
attribute is a special one. It contains the access permissions for the AD object itself. It’s what you see when you look at the ‘Security’ tab in AD Users and Computers. Most methods of accessing AD objects will have an easy way to read this data. For example, DirectoryEntry
has an ObjectSecurity
attribute to read this. But there is no obvious way to work with the other ones.
In these examples, I’ll focus on the msDS-AllowedToActOnBehalfOfOtherIdentity
attribute, since this is used when configuring Resource-Based Kerberos Constrained Delegation, which can, for example, help you solve PowerShell’s dreaded double-hop problem.
PowerShell makes this easier by exposing a property called PrincipalsAllowedToDelegateToAccount
in Get-ADUser
and Set-ADUser
, which just reads and writes the msDS-AllowedToActOnBehalfOfOtherIdentity
attribute. Even if we try to access the raw data, it gives us an ActiveDirectorySecurity
object. That’s handy.
PS C:\> $u = Get-ADUser SomeUsername -Properties "msDS-AllowedToActOnBehalfOfOtherIdentity"
PS C:\> $u."msDS-AllowedToActOnBehalfOfOtherIdentity".GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ActiveDirectorySecurity System.Security.AccessControl.DirectoryObjectSecurity
But it’s not so obvious how to work with this attributes (and the others) in .NET. So here we’ll look at two options.
DirectoryEntry
The documentation for String(NT-Sec-Desc) says that we’ll get “A COM object that can be cast to an IADsSecurityDescriptor
.” That’s exactly what we see when we try to get the value from DirectoryEntry
: a COM object.
But to be able to use the IADsSecurityDescriptor
interface, we will need to add a COM reference in Visual Studio to Active DS Type Library:
Visual Studio will add a new DLL file to your compiled application called
Interop.ActiveDS.dll
. That is a wrapper around the Windows nativeactiveds.dll
. Make sure you deploy that DLL with your application.
Now we can do this:
var act = (IADsSecurityDescriptor)
user.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value;
You can just work with it like that, but it would be a little easier if we get it into a managed type.
Remeber I mentioned that DirectoryEntry
gives us a handy property to read/write the ntSecurityDescriptor
attribute called ObjectSecurity
, which is of type ActiveDirectorySecurity
. How does DirectoryEntry
translate the ntSecurityDescriptor
attribute into an ActiveDirectorySecurity
attribute? And more importantly, how to we do it?
The source code for .NET Core is now available, so we can look at the source code for DirectoryEntry
to find out. The key is in the GetObjectSecurityFromCache
method. It does a few things:
NativeObject
property to IAdsPropertyList
.IAdsPropertyList::GetPropertyItem
to retrieve the attribute, and casts it to IADsPropertyEntry
.IADsPropertyEntry.Values
array to IADsPropertyValue
.IADsPropertyValue.OctetString
.byte[]
to create a new ActiveDirectorySecurity
object.Unfortunately, we can’t do step 5 ourselves because the constructor that it uses is marked internal
.
internal ActiveDirectorySecurity(byte[] sdBinaryForm, SecurityMasks securityMask)
: base(new CommonSecurityDescriptor(true, true, sdBinaryForm, 0))
{
_securityMaskUsedInRetrieval = securityMask;
}
So what’s the next best thing?
ActiveDirectorySecurity
inhertis from DirectoryObjectSecurity
, but that’s an abstract
class, so we can’t use that. But it does create a CommonSecurityDescriptor
object from the byte[]
, and that is something we can do. So we’ll do that.
So we just need to get the raw, binary data of the attribute and push it into the constructor for CommonSecurityDescriptor
. We can replicate steps 1-4 of what DirectoryEntry.GetObjectSecurityFromCache
does, but it turns out that there’s an even easier way.
The IADsSecurityUtility
interface is another COM object, designed to work with security descriptors. One of the methods it provides is ConvertSecurityDescriptor
, which “converts a security descriptor from one format to another”. We can use that to convert an IADsSecurityDescriptor
to a byte[]
.
var secUtility = new ADsSecurityUtility();
var byteArray = (byte[]) secUtility.ConvertSecurityDescriptor(
act,
(int)ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_IID,
(int)ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_RAW
);
var security = new CommonSecurityDescriptor(true, true, byteArray, 0);
Now you can read/write the security descriptor using the CommonSecurityDescriptor
object. You’ll likely want to work with the DiscretionaryAcl
property.
Note that you can also create a
RawSecurityDescriptor
object usingnew RawSecurityDescriptor(byteArray, 0)
. I really don’t know the difference betweenRawSecurityDescriptor
andCommonSecurityDescriptor
. If you know why you would use one over the other, let me know in the comments.
If you plan on updating the value, you need to get it back into a byte[]
. Both RawSecurityDescriptor
and CommonSecurityDescriptor
inherit from GenericSecurityDescriptor
, which has a GetBinaryForm
method, which will put a binary represenation of the security descriptor into a byte[]
you already created. For example:
var descriptor_buffer = new byte[security.BinaryLength];
security.GetBinaryForm(descriptor_buffer, 0);
Then you can write it back into the DirectoryEntry
object:
user.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value = descriptor_buffer;
user.CommitChanges();
Here is the code all together. This assumes you already have a DirectoryEntry
object called user
and you have added a COM reference to “Active DS Type Library” to your project.
var act = (IADsSecurityDescriptor)
user.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value;
var secUtility = new ADsSecurityUtility();
var byteArray = (byte[]) secUtility.ConvertSecurityDescriptor(
act,
(int) ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_IID,
(int) ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_RAW
);
var security = new CommonSecurityDescriptor(true, true, byteArray, 0);
//modify security object here
var descriptor_buffer = new byte[security.BinaryLength];
security.GetBinaryForm(descriptor_buffer, 0);
user.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value = descriptor_buffer;
user.CommitChanges();
DirectorySearcher
There are a few cases (in general) where DirectorySearcher
will gives you values in a different format than DirectoryEntry
. This is one of those cases. In fact, DirectorySearcher
makes it even easier on us, since it gives us the raw binary value without a fight. That means we don’t need to add a COM reference to our project.
This example will find an account with the username myUsername
, and create a CommonSecurityDescriptor
object:
var search = new DirectorySearcher(
new DirectoryEntry(),
$"(sAMAccountName=myUsername)"
);
//Tell it that we only want the one attribute returned
search.PropertiesToLoad.Add("msDS-AllowedToActOnBehalfOfOtherIdentity");
var result = search.FindOne();
var security = new CommonSecurityDescriptor(
true,
true,
(byte[]) result.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"][0],
0
);
If we need to write the value back, we need a DirectoryEntry
object for the account. We can use SearchResult.GetDirectoryEntry()
to do that:
var descriptor_buffer = new byte[security.BinaryLength];
security.GetBinaryForm(descriptor_buffer, 0);
var user = result.GetDirectoryEntry();
user.Properties["msDS-AllowedToActOnBehalfOfOtherIdentity"].Value = descriptor_buffer;
user.CommitChanges();
Leave a comment