Python- set attributes for "User cannot change password"

Koopman 1 Reputation point
2022-07-30T09:16:49.973+00:00

Hi all,

I have been trying to set an account attribute for an Active Directory user but this one attribute cannot be applied the same way as other account attributes (ACE type), im applying the other attributes but "User cannot change password" is the one attribute im unable to do with python programmatically. Im using code from a mixture of the below links to set the password in AD and set attributes for "Password never expires" and "Store password using reversable encyption"

Even know I get ‘success’ the checkbox is still unchecked.

My sources for the code came from here: https://blog.steamsprocket.org.uk/2011/07/04/user-cannot-change-password-using-python/

https://www.cnpython.com/qa/1397951#

Someone else other attempt was here but i'm unable to apply it:https://web.archive.org/web/20150829114442/http://www.robertmeany.com/programming/python-and-the-active-directory-security_descriptor/

Hopefully someone may be able to assist me, thank you. Hopefully someone is crazy enough to do this in python link me instead of 2 lines with powershell haha.

Windows for business | Windows Client for IT Pros | Directory services | Active Directory
0 comments No comments
{count} votes

6 answers

Sort by: Most helpful
  1. Zaid Ashraf 1 Reputation point
    2022-07-30T09:26:32.927+00:00

    I am able to bind and query Active Directory via python-ldap without any issues except when it comes to adding or modifying attributes on AD. I can add the attribute but the encoding seems to be way off as all the text is garbled.

    I've tried encoding my string with utf8 and a few others with no luck.

    I've also tried binding with a Domain Admin account along with binding with the user account to which I will be changing an attribute, same result regardless.

    Here is the method I use to update an attribute:

    class LdapHelpers:

    def init(self):
    import ldap

    # set globals  
    self.server = 'LDAP://dc.mycompany.com'  
    self.admin_dn = 'CN=Administrator,CN=users,DC=mycompany,DC=com'  
    self.admin_pass = 'coolpassword'  
    
    # init LDAP connection  
    #ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, 0)  
    ldap.set_option(ldap.OPT_REFERRALS, 0)  
    ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)  
    ldap.protocol_version = ldap.VERSION3  
    self.ldap = ldap.initialize(self.server)  
    

    def update_attribute(self, attrib, value):
    try:
    import ldap
    conn = self.ldap
    conn.simple_bind_s(self.admin_dn, self.admin_pass)
    mod_attrs = [( ldap.MOD_REPLACE, "mobile", "6306564123")]

        # I have tried other variations of the above  
        # mod_attrs = [( ldap.MOD_REPLACE, "mobile", "6306564123".encode('utf-8)]  
    
        conn.modify_s('CN=Mike Smith,OU=GoogleApps,DC=company,DC=com', mod_attrs)  
        print 'record updated'  
    
    except ldap.LDAPError as e:  
        return e.message  
    

    Doing a ldapsearch via terminal this is what the attribute looks like:

    mobile:: MC8sAQAAAAAQNA==
    This is what 'Hello World' looks like when I set mobile to it:

    mobile:: 77+9ehsCAAAAABDvv70V
    I've checked MSDN and it says that ldap attribute is just a Unicode string.

    System: Ubuntu 15.10 64bit Python: 2.7.10 python-ldap==2.4.21 brucia-essenze

    As a side note I can search AD without any issues and parse/display returned user attributes, the issue only seems to be with creating or modifying attributes that this encoding issue comes in to play.


  2. Koopman 1 Reputation point
    2022-07-30T09:35:52.107+00:00

    I’m able to get as far as encoding the SId values from bytes to usable strings etc, get a success message but nothing else. I’m wanting to set the ‘user cannot change password’ attribute also known as cannotchangepassword with powershell
    or PASSWD_Cannot_change as an ACE/ACL

    https://learn.microsoft.com/en-gb/windows/win32/adsi/modifying-user-cannot-change-password-ldap-provider?redirectedfrom=MSDN

    import ldap3
    from ldap3 import
    Connection,Server,ALL,SUBTREE,MODIFY_REPLACE
    Import struct
    from ldap3.protocol.microsoft import security_descriptor_control
    from ldap3.protocol.formatters.formatters import format_sid
    from ldap3.utils.conv import escape_filter_chars

    from impacket.ldap import ldaptypes
    from impacket.ldap.ldaptypes import ACCESS_ALLOWED_OBJECT_ACE, ACCESS_MASK, ACCESS_ALLOWED_ACE, ACE, OBJECTTYPE_GUID_MAP

    zid = input("username: ")
    zid = str(zid).lower()
    print(f'Searching for {zid}')
    server = Server('ldaps://IP_OF_MY_AD_SERVER', use_ssl=True, get_info=all)
    conn = Connection(server, user='DOMAIN\USERNAME', password='password', auto_bind=True)
    conn.bind()
    Path_Root = "DC=domain,DC=Wan"
    Filter = f'(&(objectclass=user)(&(sAMAccountName={zid})(!(objectclass=computer))))'
    conn.search(search_base = Path_Root,
    search_filter = Filter,
    search_scope = SUBTREE,
    attributes = ["cn", "sAMAccountName", "displayName", objectSid, nTSecurityDescriptor]
    )
    if len(conn.entries) == 1:
    USER_DN = conn.response[0].get("dn")
    print(USER_DN)

    Sid = conn.response[0].get("objectSid")
    NTSEC = conn.response[0].get(nTSecurityDescriptor)

    try:
    new_password = "A__PASSWORD22"
    print(new_password)
    print("New password successfully applied")
    except:
    print("New password could not be applied")

    setting the password:

    try:
    res = ldap3.extend.microsoft.modifyPassword.ad_modify_password(conn, USER_DN, new_password, old_password=None, controls=None)
    res = conn.extend.microsoft.modify_password(USER_DN, new_password)
    changeUACattribute = {'userAccountControl': [('MODIFY_REPLACE', 66236)]}
    conn.modify(USER_DN, changes=changeUACattribute)
    print(conn.result)
    print(res)
    if res:
    print('user %s change password Success.')
    print('password: %s' %new_password)
    else:
    print('user %s change password Failed.')
    except Exception as e:
    print(f'Error setting AD password: {e}')

    def create_object_ace(privguid, sid):      
    print("creating ace object")      
    nace = ldaptypes.ACE()      
    nace['AceType'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_TYPE      
    nace['AceFlags'] = 0x00      
    acedata = ldaptypes.ACCESS_DENIED_OBJECT_ACE()      
    acedata['Mask'] = ldaptypes.ACCESS_MASK()      
    acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS      
    acedata['ObjectType'] = string_to_bin(privguid)      
    acedata['InheritedObjectType'] = b''      
    acedata['Sid'] = ldaptypes.LDAP_SID()      
    acedata['Sid'].fromCanonical(sid)      
    assert sid == acedata['Sid'].formatCanonical()      
    acedata['Flags'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT      
    nace['Ace'] = acedata      
    return nace      
    

    def convert(binary):
    version = struct.unpack('B', binary[0:1])[0]
    # I do not know how to treat version != 1 (it does not exist yet)
    assert version == 1, version
    length = struct.unpack('B', binary[1:2])[0]
    authority = struct.unpack(b'>Q', b'\x00\x00' + binary[2:8])[0]
    string = 'S-%d-%d' % (version, authority)
    binary = binary[8:]
    assert len(binary) == 4 * length
    for i in range(length):
    value = struct.unpack('<L', binary[4*i:4*(i+1)])[0]
    string += '-%d' % value
    return string

    usersid = convert(Sid)

    controls = security_descriptor_control(sdflags=0x04)
    c.search(search_base="DC=testahs,DC=com", search_filter='(&(objectCategory=domain))',
    attributes=['SAMAccountName', 'nTSecurityDescriptor'], controls=controls)
    entry = c.entries[0]
    secDescData = NTSEC.raw_values[0]
    secDesc = ldaptypes.SR_SECURITY_DESCRIPTOR(data=secDescData)

    secDesc['Dacl']['Data'].append(create_object_ace('ab721a53-1e2f-11d0-9819-00aa0040529b', usersid)) # This GUID is for 'User cannot change password'

    dn = entry.entry_dn
    data = secDesc.getData()

    c.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls)

    print(c.result) # gives -> {'result': 0, 'description': 'success', 'dn': '', 'message': '', 'referrals': None, 'type': 'modifyResponse'}

    0 comments No comments

  3. Gary Reynolds 9,621 Reputation points
    2022-07-31T01:57:32.977+00:00

    Hi @Koopman

    My Python skill level is pretty basic, but I can give you some pointer from an AD side.

    I would 'Or' the existing value of UserAccountControl with ADS_UF_DONT_EXPIRE_PASSWD (0x10000), that way you preserve the user's existing settings. With this option set, the user will not be able to change their password, so there is no real reason to add a deny change password permission to the user object.

    Something like this (casting might need some work)

    Control = conn.response[0].get('useraccountcontrol') | 65536  
    changeUACattribute = {'userAccountControl': [('MODIFY_REPLACE', Control)]}  
    conn.modify(USER_DN, changes=changeUACattribute)  
    

    A couple things on the SD management based on the native API, but this might be different for the LDAP library:

    • A SD can be a SelfRelative or Absolute, AD will only accept an SelfRelative SD, but to edit an SD it must be Absolute
    • When adding an explicit deny permissions, it should be the first ACE in the ACL, your code adds it to the end
    • Rather than use the user's SID, it might be a easier to use SELF (S-1-5-10) as the trustee, then you can have a static data, rather than having to convert the user's SID. SID S-1-5-10 = 01 01 00 00 00 00 00 05 0A 00 00 00

    This is the ACE that needs to be set:

    226423-image.png

    Gary.


  4. Gary Reynolds 9,621 Reputation points
    2022-07-31T05:23:06.337+00:00

    When adding an explicit deny permissions, it should be the first ACE in the ACL, your code adds it to the end”

    The comment is based on the native API and how they add ACEs to the ACL, this might not be applicable for the LDAP library you are using.

    Reference from AddAccessDeniedAce function

    Remarks
    The AddAccessAllowedAce and AddAccessDeniedAce functions add a new ACE to the end of the list of ACEs for the ACL. These functions do not automatically place the new ACE in the proper canonical order. It is the caller's responsibility to ensure that the ACL is in canonical order by adding ACEs in the proper sequence.

    For the permissions to be evaluated correctly the Deny permissions must be the first in the ACL list as shown below.

    226511-image.png

    My bad on the UserAccountControl, I didn't check the details and made the assumption that setting ADS_UF_PASSWD_CANT_CHANGE on UserAccountControl would block the change. If you do try to set this value the update will succeed but the option will not be set.

    226472-image.png

    When setting the ADS_UF_PASSWD_CANT_CHANGE in ADUC, it adds the following permissions to the user object:

    226465-image.png

    The ADUC dialog will only show the option checked if the Self permission exists in the ACL. So if you are setting the permission based on the user's SID, then the ADUC dialog will not show the User cannot change password as ticked.

    In your screenshot, you don’t show the checkbox but I’m assuming the checkbox should still make once that group setting has been changed is that right?

    Yes with the Self permission assigned the User cannot change password is ticked
    226473-image.png


  5. Koopman 1 Reputation point
    2022-07-31T21:47:47.163+00:00

    import bonsai
    from bonsai import LDAPClient
    from bonsai.active_directory import SecurityDescriptor
    from bonsai.active_directory import UserAccountControl
    import uuid

    debug####

    bonsai.set_debug(True, -1)

    client = LDAPClient("ldap://IP_ADDRESS")
    client.set_credentials("SIMPLE", user="CN=God,OU=Admin,OU=Domain,DC=domain,DC=lan", password="password_for_admin")

    Secure if needed #####

    conn = client.connect()

    client = bonsai.LDAPClient("ldap://IP_ADDRESS", tls=True)

    client.set_ca_cert("ca.pem")

    client.set_cert_policy("ALLOW")

    client.set_credentials("DIGEST-MD5", "god", "password_for_admin", "domain.lan")

    entry = bonsai.LDAPEntry("CN=TEST,OU=Users,OU=Domain,DC=domain,DC=lan")

    entry = client.search("CN=TEST,OU=Users,OU=Domain,DC=domain,DC=lan", 0, attrlist=["ntSecurityDescriptor", 'userAccountControl'])[0]

    sec_desc = SecurityDescriptor.from_binary(entry["ntSecurityDescriptor"][0])

    with client.connect() as conn:
    entry = conn.search(
    entry.dn, 0, attrlist=["ntSecurityDescriptor", "userAccountControl"]
    )[0]
    uac = bonsai.active_directory.UserAccountControl(entry["userAccountControl"][0])
    sec_desc = bonsai.active_directory.SecurityDescriptor.from_binary(
    entry["ntSecurityDescriptor"][0]
    )
    new_dacl_aces = []
    for ace in sec_desc.dacl.aces:
    if ace.object_type == uuid.UUID("ab721a53-1e2f-11d0-9819-00aa0040529b"):
    # Find change password ACE and change it to deny.
    new_ace = bonsai.active_directory.ACE(
    bonsai.active_directory.ACEType.ACCESS_DENIED_OBJECT,
    ace.flags,
    ace.mask,
    ace.trustee_sid,
    ace.object_type,
    ace.inherited_object_type,
    ace.application_data,
    )
    # Insert new deny ACEs to the front of the list.
    new_dacl_aces.insert(0, new_ace)
    else:
    new_dacl_aces.append(ace)
    new_dacl = bonsai.active_directory.ACL(sec_desc.dacl.revision, new_dacl_aces)
    new_sec_desc = bonsai.active_directory.SecurityDescriptor(
    sec_desc.control,
    sec_desc.owner_sid,
    sec_desc.group_sid,
    sec_desc.sacl,
    new_dacl,
    sec_desc.revision,
    sec_desc.sbz1,
    )
    entry.change_attribute(
    "ntSecurityDescriptor", bonsai.LDAPModOp.REPLACE, new_sec_desc.to_binary()
    )
    uac.properties["accountdisable"] = False
    entry.change_attribute("userAccountControl", bonsai.LDAPModOp.REPLACE, uac.value)
    entry.modify()
    uac = UserAccountControl(entry['userAccountControl'][0])
    print(uac.properties)
    uac.properties['passwd_cant_change'] = True
    entry['userAccountControl'][0] = uac.value
    print(uac.properties)

    print(sec_desc.owner_sid)

    print(sec_desc.dacl)

    print('Thank you Bonsai')

    Apply the Flag for 'PASSWD_CANNOT_CHANGE'

    import ldap3
    from ldap3 import Connection,Server,ALL,SUBTREE,MODIFY_REPLACE,NTLM
    zid = input("username: ")
    zid = str(zid).lower()
    print(f'Searching for {zid}')
    server = Server('ldaps://IP_ADDRESS', use_ssl=True, get_info=all) #port 636 for secure to solve {'result': 53, 'description': 'unwillingToPerform', 'dn': '', 'message': '0000001F: SvcErr: DSID-031A1236, problem 5003 (WILL_NOT_PERFORM), data 0\n\x00', 'referrals': None, 'type': 'modifyResponse'}
    conn = Connection(server, user='Domain\god', password='password_for_admin', auto_bind=True)
    conn.bind()
    Path_Root = "DC=Domain,DC=lan"
    Filter = f'(&(objectclass=user)(&(sAMAccountName={zid})(!(objectclass=computer))))'
    conn.search(search_base = Path_Root,
    search_filter = Filter,
    search_scope = SUBTREE,
    attributes = ["cn", "sAMAccountName", "displayName",'nTSecurityDescriptor','objectSid']
    )
    if len(conn.entries) == 1:
    USER_DN = conn.response[0].get("dn")
    print(USER_DN)
    changeUACattribute = {'userAccountControl': [('MODIFY_REPLACE', 66236)]}
    conn.modify(USER_DN, changes=changeUACattribute)
    print('We have success')

    0 comments No comments

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.