0

I am trying to get the user account control properties using library Novell.Directory.Ldap in ASP .NET Core 5. When I search the users attributes I found the attribute name userAccountControl which is set to some number. After searching solution I am able to find:

bool isUserActive = false;
bool userMustChangePassword = false;
bool passwordNeverExpires = false;
bool passwordCannotBeChanged = false;

var flags = Convert.ToInt32(attributeSet.GetAttribute("userAccountControl").StringValue);
isUserActive = !Convert.ToBoolean(flags & 0x0002); //1. checks if user is enabled
if ((flags == 66048)) //65536+512
{
  passwordNeverExpires = true; //2. Password never expires property
}
long value = Convert.ToInt64(attributeSet.GetAttribute("pwdLastSet").StringValue);
if (value == 0)
{
    userMustChangePassword = true; //3. User must change password at next login
}

But I am not able to figure out how to get the User cannot change password and if the account is locked properties? Or how can I compare the binary value like 0x0040? Please help

Edit:

I tried the steps given by @Gabriel Luci in https://www.gabescode.com/active-directory/2019/07/25/nt-security-descriptors.html and tried following code:

var act = attributeSet.GetAttribute("nTSecurityDescriptor").ByteValue;
ADsSecurityUtility secUtility = new ADsSecurityUtility();
IADsSecurityDescriptor convertAttrToSD = (IADsSecurityDescriptor)secUtility.ConvertSecurityDescriptor(act, (int)ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_RAW, (int)ADS_SD_FORMAT_ENUM.ADS_SD_FORMAT_IID);
var byteArray = (byte[])secUtility.ConvertSecurityDescriptor(
                            convertAttrToSD,
                            (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);

If I check the security it shows enter image description here

I am not getting where to look user cannot change the password settings?

Edit 2: According to @Gabriel Luci updated answer, it worked for me like this:

var constraints = new LdapSearchConstraints();
constraints.SetControls(new LdapControl("1.2.840.113556.1.4.801", true, new byte[] { 48, 3, 2, 1, 7 }));
var getNtSecurityByteValue=attributeSet.GetAttribute("nTSecurityDescriptor").ByteValue;
var security = new CommonSecurityDescriptor(true, true, getNtSecurityByteValue, 0);
var self = new SecurityIdentifier(WellKnownSidType.SelfSid, null);
var userChangePassword = new Guid("AB721A53-1E2F-11D0-9819-00AA0040529B");
foreach (var ace in security.DiscretionaryAcl)
{
   if(ace.GetType().Name == "ObjectAce")
   {
      ObjectAce objAce = (ObjectAce)ace;
      if (objAce.AceType == AceType.AccessDeniedObject && objAce.SecurityIdentifier == self && objAce.ObjectAceType == userChangePassword)
      {
          cannotChangePassword = true;
          break;
      }
   }
}
Varsh
  • 413
  • 9
  • 26
  • https://learn.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties#property-flag-descriptions – ProgrammingLlama Dec 21 '21 at 09:13

1 Answers1

2

The userAccountControl value is a bit flag, meaning that every bit in the binary representation of the number is an "on" or "off" depending on if it's a 1 or 0. So the decimal value is meaningless.

You are already checking the value properly when you're checking if it's enabled:

isUserActive = !Convert.ToBoolean(flags & 0x0002); //1. checks if user is enabled

Likewise, you should do the same when checking any of the other flags. The value of each is listed in the documentation.

When you're checking if the password is set to never expire, you're comparing the decimal value, which won't always give you a correct answer. Instead, check the bit value:

passwordNeverExpires = Convert.ToBoolean(flags & 0x10000);

Similar for account is locked:

var accountLocked = Convert.ToBoolean(flags & 0x0010);

For the user cannot change password setting, unfortunately that's more difficult and requires reading the permissions on the user account, which I have never done using the Novell.Directory.Ldap library. But I can try to point you in the right direction.

The account permissions are in the nTSecurityDescriptor attribute. Read this issue about how to get the byte array from that attribute: How to read/set NT-Security-Descriptor attributes?

I wrote an article about how to get the byte array into a usable format: Active Directory: Handling NT Security Descriptor attributes.

Then you'll be looking for two permissions that get added when the 'User cannot change password' checkbox is checked:

  1. Deny Change Password to 'Everyone'
  2. Deny Change Password to 'SELF'

You can probably get away with only looking for #2.

Update: I finally tried this out for myself. I had never used the Novell.Directory.Ldap library before, so this was new to me.

With the help of this answer, I figured out that you need to set an LDAP control for it to return the nTSecurityDescriptor attribute at all:

var constraints = new LdapSearchConstraints();
constraints.SetControls(new LdapControl("1.2.840.113556.1.4.801", true
                                           , new byte[] {48, 3, 2, 1, 7}));

Once you retrieve the object, you can check the permissions like this:

var byteValue = attributeSet.GetAttribute("nTSecurityDescriptor").ByteValue;
var security = new CommonSecurityDescriptor(true, true, byteValue, 0);

var self = new SecurityIdentifier(WellKnownSidType.SelfSid, null);
var userChangePassword = new Guid("AB721A53-1E2F-11D0-9819-00AA0040529B");

var cannotChangePassword = false;

foreach (var ace in (security.DiscretionaryAcl)) {
    if (ace is ObjectAce objAce && objAce.AceType == AceType.AccessDeniedObject
            && objAce.SecurityIdentifier == self && objAce.ObjectAceType == userChangePassword) {
        cannotChangePassword = true;
        break;
    }
}

The GUID of the User-Change-Password permission is taken from the Control Access Rights documentation.

Notice that you don't need to use IADsSecurityDescriptor, and thus you don't need a reference to Interop.ActiveDs. This is because we're given the value as a byte array already.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • thank you for the reply. ```var userCannotChangePassword = Convert.ToBoolean(flags & 0x0040);``` this always return false, though I have users with this option enabled. – Varsh Dec 22 '21 at 02:50
  • @Varsh Apparently you're right, and I knew that, and I forgot. I just did a quick search and found an answer that I wrote about this back in 2018: https://stackoverflow.com/a/53959606/1202807 – Gabriel Luci Dec 22 '21 at 04:11
  • Hey, I already go through this link https://learn.microsoft.com/en-ca/windows/win32/adsi/reading-user-cannot-change-password-ldap-provider before but didn't get actually how to implement it here. – Varsh Dec 22 '21 at 04:17
  • @Varsh I updated my answer with some resources that should point you in the right direction. It's not super easy, unfortunately. – Gabriel Luci Dec 22 '21 at 04:22
  • thank you @Gabriel Luci, but I cannot find the `nTSecurityDescriptor` attribute in `LdapAttributeSet` . May be Novell.Directory.Ldap don't have this attribute. – Varsh Dec 22 '21 at 06:34
  • @Varsh Is that the collection of returned attributes? It might be an attribute you have to specifically ask for. If you found the user by a search, you should be able to specify which attributes you want returned in that search. – Gabriel Luci Dec 22 '21 at 15:55
  • update: I get the attribute `nTSecurityDescriptor` value. According to https://www.gabescode.com/active-directory/2019/07/25/nt-security-descriptors.html I used `Interop.ActiveDs` DLL and trying to get the attribute value `(IADsSecurityDescriptor)(attributeSet.GetAttribute("nTSecurityDescriptor").ByteValue)` it is giving error ```cannot convert type byte[] to IADsSecurityDescriptor``` . – Varsh Dec 23 '21 at 07:19
  • I edited the question, Please check and guide. – Varsh Dec 23 '21 at 08:51
  • @Varsh I updated my answer with the code on how to identify this. I learned some new things that I can use to update my article. – Gabriel Luci Dec 23 '21 at 16:50
  • for the code ```foreach (ObjectAce ace in (security.DiscretionaryAcl))``` it is throwing the exception ```Unable to cast object of type 'System.Security.AccessControl.CommonAce' to type 'System.Security.AccessControl.ObjectAce'.``` – Varsh Dec 24 '21 at 02:25
  • Another thing, can we have alternative to ```CommonSecurityDescriptor``` and ```SecurityIdentifier``` because they are showing warning that they are only supported on ```windows``` – Varsh Dec 24 '21 at 02:28
  • Hey @Gabriel Luci thank you so much for your time and effort. I updated the question with the answer worked for me. I will accept your answer. Just help me to make this code cross platform if you can. – Varsh Dec 24 '21 at 04:23
  • 1
    @Varsh I updated the loop in my code with a better way of checking the type. – Gabriel Luci Dec 24 '21 at 15:03
  • @Varsh Did you remove the reference to the "Active DS Type Library"? – Gabriel Luci Dec 26 '21 at 20:32
  • Yes, I removed "Active DS Type Library". Using just two namespaces for this code, ```System.Security.AccessControl``` and ```System.Security.Principal``` but both are supporting only windows. – Varsh Dec 27 '21 at 02:31
  • @Varsh It appears so. I did some research today and I haven't been able to find an equivalent that is cross platform. I don't know if that has changed in .NET 6. You may only be able to run that part of your code from Windows. You could put an `if` statement around that part and only run it if it's running on Windows. Use [`OperatingSystem.IsWindows()`](https://learn.microsoft.com/en-us/dotnet/api/system.operatingsystem.iswindows?view=net-5.0) to check. – Gabriel Luci Dec 27 '21 at 03:15
  • Thanks @Gabriel Luci, but I need to deploy this code on linux environnment. So, checking the OS will not work for me. – Varsh Dec 27 '21 at 03:45