noirello / bonsai

Simple Python 3 module for LDAP, using libldap2 and winldap C libraries.
MIT License
116 stars 32 forks source link

Any way to change ace.properties for an object? #65

Closed iHaagcom closed 1 year ago

iHaagcom commented 1 year ago

using this:

from bonsai.active_directory import UserAccountControl uac = UserAccountControl(entry['userAccountControl'][0]) print(uac.properties)

I can see 'passwd_cant_change': False, how to change this to true with bonsai?

noirello commented 1 year ago

Have you tried something like this?

uac.properties['passwd_cant_change'] = True
entry['userAccountControl'][0] = uac.value
iHaagcom commented 1 year ago

How can I apply that to the object now?

I tried entry.apply but I get 'unwilling to perform' and cannot connect with ldaps so that could be the reason for the unwillingness?

This specific UAC PASSWD_CANT_CHANGE is a little challenging: https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/useraccountcontrol-manipulate-account-properties

noirello commented 1 year ago

Yes, it could be. The AD server usually requires secure connection for modifying specific attributes.

Unfortunately, I have got very little experience with Active Directory. If you can access the server logs it might have some better info about the unwillingness.

iHaagcom commented 1 year ago

I did try LDAPS with your module but it wouldn't bind. Should I try using the port 636 instead? Was really hoping to make this change with python. https://docs.microsoft.com/en-us/answers/questions/948475/python-set-attributes-ldap.html

Would you know enough to help convert these? I was really hoping your module would be the answer :(

https://docs.microsoft.com/en-us/windows/win32/adsi/modifying-user-cannot-change-password-winnt-provider

noirello commented 1 year ago

I did try LDAPS with your module but it wouldn't bind. Should I try using the port 636 instead?

If you didn't specify a port in your connection URL, then using the ldaps:// will use port 636 by default. You have to set the proper CA cert (or lower the verification policy with the LDAPClient.set_cert_policy method), otherwise the connection will fail.

I dug in a little bit more based on the links you provided (mostly using this), but no luck denying password change.😞

One important thing is that you should use LDAPEntry's change_attribute method for changing security related attributes (like userAccountControl or ntSecurityDescriptor). Simple assigning would not give you the proper LDAP modification, and it could also cause UnwillingToPerform error when try to modify the entry.

I share my code snippets which I was able to add the deny access control elements to the AD user. The entry's passwd_cant_change was still False and I was still able to change the user's password after it, but maybe it could be useful to you somehow.


import bonsai
import bonsai.active_directory
import uuid

cli = bonsai.LDAPClient("ldap://ad-server.bonsai.test", tls=True)
cli.set_ca_cert("./tests/testenv/certs/cacert.pem")
cli.set_credentials("DIGEST-MD5", "admin", "password", "BONSAI.TEST")

def add_test_user(cli):
    entry = bonsai.LDAPEntry("cn=ad_user,dc=bonsai,dc=test")
    entry["objectclass"] = [
        "top",
        "inetOrgPerson",
        "person",
        "organizationalPerson",
        "user",
    ]
    entry["sn"] = "ad_user"
    entry["cn"] = "ad_user"
    entry["givenName"] = "AD"
    entry["userPrincipalName"] = "ad_user"
    entry["displayName"] = "ad_user"
    entry["sAMAccountName"] = "ad_user"
    entry["userPassword"] = "Sup3rP4ssW0rd123"
    entry["mail"] = "ad_user@bonsai.test"

    with cli.connect() as conn:
        conn.add(entry)

def deny_password_change(cli: LDAPClient, entry_dn: str):
    with cli.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)
        # Adding the deny ACE for Everyone to the list. This ACE was missing from the original list 
        # only once while I was testing.
        #new_dacl_aces.insert(
        #    0,
        #    bonsai.active_directory.ACE(
        #        bonsai.active_directory.ACEType.ACCESS_DENIED_OBJECT,
        #        set(),
        #        bonsai.active_directory.ACERight.DS_CONTROL_ACCESS,
        #        bonsai.active_directory.SID("S-1-1-0"),
        #        uuid.UUID("ab721a53-1e2f-11d0-9819-00aa0040529b"),
        #        None,
        #        b"",
        #    ),
        #)
        # Copying every aspect of the old security descriptor except the DACL.
        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()

And this is the security settings for the user after I ran the code above:

access

Update: add dn parameter to deny_password_change function

iHaagcom commented 1 year ago

Thank you for sharing, can you just modify the user not create a new user?? Did you say you were still able to change the password for the user (as the user on the domain?)?

noirello commented 1 year ago

Updated the function above with a dn parameter.

Did you say you were still able to change the password for the user (as the user on the domain?)?

Yes, I was able to bind with the user using the original password, then change it.

iHaagcom commented 1 year ago

Can cer for the certificate be used or does it have to be pem? Attempting with cer gives me: bonsai.errors.LDAPError: Local Error. (0x0052 [82])

noirello commented 1 year ago

It depends on the used tls implementation that the module is using. See: https://bonsai.readthedocs.io/en/latest/advanced.html#tls-settings

iHaagcom commented 1 year ago

still unable to connect using the above method, i get bonsai.errors.LDAPError: Local Error. (0x0052 [82]) assuming that means unable over a vpn connection and must be local to the domain?

I exported the certificate this way, renamed it from cer to pem cert-export


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

#client = LDAPClient("ldap://IP_ADDRESS")
#client.set_credentials("SIMPLE", user="CN=GOD,OU=Admin,OU=Domain,DC=Domain,DC=lan", password="passwordforadmin!")
#conn = client.connect()

client = bonsai.LDAPClient("ldap://IP_ADDRESS", tls=True)
client.set_ca_cert("domainca.pem")
client.set_credentials("DIGEST-MD5", "GOD", "passwordforadmin", "domain.lan")
print("Authenticated")
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()`

``
noirello commented 1 year ago

You can use bonsai.set_debug(True, -1) to get some debug information about the connection or try cli.set_cert_policy("ALLOW"), to not verify the server cert.

iHaagcom commented 1 year ago

with this flag: cli.set_cert_policy("ALLOW") it at least tells me that: bonsai.errors.AuthenticationError: Invalid Credentials. (0x0031 [49])

iHaagcom commented 1 year ago

okay, i was able to connect using the old method (the hashed out stuff), and get the same result you do. deny is applied but uac flag is still false however, this changeUACattribute = {'userAccountControl': [('MODIFY_REPLACE', 66236)]} I think now ticks the box (just ran my old code)

iHaagcom commented 1 year ago

FINALLY!!!!! The complete code (still have to pretty it up but the best thing is IT WORKS!!!!) THANK YOU THANK You Thank you!!

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('THANK YOU Noirello')
iHaagcom commented 1 year ago

I'm not sure yet, but i think this script may be applying the ace to the domain ou not the account? On your test account can you check if your script applies to "cn=ad_user,dc=bonsai,dc=test" or also changes the ",dc=bonsai,dc=test" permissions?

noirello commented 1 year ago

No, the top level permissions are not changed.