#!/usr/bin/env python3 # Impacket - Collection of Python classes for working with network protocols. # # Copyright Fortra, LLC and its affiliated companies # # All rights reserved. # # This software is provided under a slightly modified version # of the Apache Software License. See the accompanying LICENSE file # for more information. # # Description: # Python script to read and manage the Discretionary Access Control List of an object # # Authors: # Charlie BROMBERG (@_nwodtuhs) # Guillaume DAUMAS (@BlWasp_) # Lucien DOUSTALY (@Wlayzz) # import argparse import binascii import codecs import json import logging import os import sys import traceback import datetime import ldap3 import ldapdomaindump from enum import Enum from ldap3.protocol.formatters.formatters import format_sid from impacket import version from impacket.examples import logger, utils from impacket.ldap import ldaptypes from impacket.msada_guids import SCHEMA_OBJECTS, EXTENDED_RIGHTS from ldap3.utils.conv import escape_filter_chars from ldap3.protocol.microsoft import security_descriptor_control from impacket.uuid import string_to_bin, bin_to_string from impacket.examples.utils import init_ldap_session, parse_identity OBJECT_TYPES_GUID = {} OBJECT_TYPES_GUID.update(SCHEMA_OBJECTS) OBJECT_TYPES_GUID.update(EXTENDED_RIGHTS) # Universal SIDs WELL_KNOWN_SIDS = { 'S-1-0': 'Null Authority', 'S-1-0-0': 'Nobody', 'S-1-1': 'World Authority', 'S-1-1-0': 'Everyone', 'S-1-2': 'Local Authority', 'S-1-2-0': 'Local', 'S-1-2-1': 'Console Logon', 'S-1-3': 'Creator Authority', 'S-1-3-0': 'Creator Owner', 'S-1-3-1': 'Creator Group', 'S-1-3-2': 'Creator Owner Server', 'S-1-3-3': 'Creator Group Server', 'S-1-3-4': 'Owner Rights', 'S-1-5-80-0': 'All Services', 'S-1-4': 'Non-unique Authority', 'S-1-5': 'NT Authority', 'S-1-5-1': 'Dialup', 'S-1-5-2': 'Network', 'S-1-5-3': 'Batch', 'S-1-5-4': 'Interactive', 'S-1-5-6': 'Service', 'S-1-5-7': 'Anonymous', 'S-1-5-8': 'Proxy', 'S-1-5-9': 'Enterprise Domain Controllers', 'S-1-5-10': 'Principal Self', 'S-1-5-11': 'Authenticated Users', 'S-1-5-12': 'Restricted Code', 'S-1-5-13': 'Terminal Server Users', 'S-1-5-14': 'Remote Interactive Logon', 'S-1-5-15': 'This Organization', 'S-1-5-17': 'This Organization', 'S-1-5-18': 'Local System', 'S-1-5-19': 'NT Authority', 'S-1-5-20': 'NT Authority', 'S-1-5-32-544': 'Administrators', 'S-1-5-32-545': 'Users', 'S-1-5-32-546': 'Guests', 'S-1-5-32-547': 'Power Users', 'S-1-5-32-548': 'Account Operators', 'S-1-5-32-549': 'Server Operators', 'S-1-5-32-550': 'Print Operators', 'S-1-5-32-551': 'Backup Operators', 'S-1-5-32-552': 'Replicators', 'S-1-5-64-10': 'NTLM Authentication', 'S-1-5-64-14': 'SChannel Authentication', 'S-1-5-64-21': 'Digest Authority', 'S-1-5-80': 'NT Service', 'S-1-5-83-0': 'NT VIRTUAL MACHINE\\Virtual Machines', 'S-1-16-0': 'Untrusted Mandatory Level', 'S-1-16-4096': 'Low Mandatory Level', 'S-1-16-8192': 'Medium Mandatory Level', 'S-1-16-8448': 'Medium Plus Mandatory Level', 'S-1-16-12288': 'High Mandatory Level', 'S-1-16-16384': 'System Mandatory Level', 'S-1-16-20480': 'Protected Process Mandatory Level', 'S-1-16-28672': 'Secure Process Mandatory Level', 'S-1-5-32-554': 'BUILTIN\\Pre-Windows 2000 Compatible Access', 'S-1-5-32-555': 'BUILTIN\\Remote Desktop Users', 'S-1-5-32-557': 'BUILTIN\\Incoming Forest Trust Builders', 'S-1-5-32-556': 'BUILTIN\\Network Configuration Operators', 'S-1-5-32-558': 'BUILTIN\\Performance Monitor Users', 'S-1-5-32-559': 'BUILTIN\\Performance Log Users', 'S-1-5-32-560': 'BUILTIN\\Windows Authorization Access Group', 'S-1-5-32-561': 'BUILTIN\\Terminal Server License Servers', 'S-1-5-32-562': 'BUILTIN\\Distributed COM Users', 'S-1-5-32-569': 'BUILTIN\\Cryptographic Operators', 'S-1-5-32-573': 'BUILTIN\\Event Log Readers', 'S-1-5-32-574': 'BUILTIN\\Certificate Service DCOM Access', 'S-1-5-32-575': 'BUILTIN\\RDS Remote Access Servers', 'S-1-5-32-576': 'BUILTIN\\RDS Endpoint Servers', 'S-1-5-32-577': 'BUILTIN\\RDS Management Servers', 'S-1-5-32-578': 'BUILTIN\\Hyper-V Administrators', 'S-1-5-32-579': 'BUILTIN\\Access Control Assistance Operators', 'S-1-5-32-580': 'BUILTIN\\Remote Management Users', } # GUID rights enum # GUID thats permits to identify extended rights in an ACE # https://docs.microsoft.com/en-us/windows/win32/adschema/a-rightsguid class RIGHTS_GUID(Enum): WriteMembers = "bf9679c0-0de6-11d0-a285-00aa003049e2" ResetPassword = "00299570-246d-11d0-a768-00aa006e0529" DS_Replication_Get_Changes = "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2" DS_Replication_Get_Changes_All = "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2" # ACE flags enum # New ACE at the end of SACL for inheritance and access return system-audit # https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-addauditaccessobjectace class ACE_FLAGS(Enum): CONTAINER_INHERIT_ACE = ldaptypes.ACE.CONTAINER_INHERIT_ACE FAILED_ACCESS_ACE_FLAG = ldaptypes.ACE.FAILED_ACCESS_ACE_FLAG INHERIT_ONLY_ACE = ldaptypes.ACE.INHERIT_ONLY_ACE INHERITED_ACE = ldaptypes.ACE.INHERITED_ACE NO_PROPAGATE_INHERIT_ACE = ldaptypes.ACE.NO_PROPAGATE_INHERIT_ACE OBJECT_INHERIT_ACE = ldaptypes.ACE.OBJECT_INHERIT_ACE SUCCESSFUL_ACCESS_ACE_FLAG = ldaptypes.ACE.SUCCESSFUL_ACCESS_ACE_FLAG # ACE flags enum # For an ACE, flags that indicate if the ObjectType and the InheritedObjecType are set with a GUID # Since these two flags are the same for Allowed and Denied access, the same class will be used from 'ldaptypes' # https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-access_allowed_object_ace class OBJECT_ACE_FLAGS(Enum): ACE_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT ACE_INHERITED_OBJECT_TYPE_PRESENT = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_INHERITED_OBJECT_TYPE_PRESENT # Access Mask enum # Access mask permits to encode principal's rights to an object. This is the rights the principal behind the specified SID has # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/7a53f60e-e730-4dfe-bbe9-b21b62eb790b # https://docs.microsoft.com/en-us/windows/win32/api/iads/ne-iads-ads_rights_enum?redirectedfrom=MSDN class ACCESS_MASK(Enum): # Generic Rights GenericRead = 0x80000000 # ADS_RIGHT_GENERIC_READ GenericWrite = 0x40000000 # ADS_RIGHT_GENERIC_WRITE GenericExecute = 0x20000000 # ADS_RIGHT_GENERIC_EXECUTE GenericAll = 0x10000000 # ADS_RIGHT_GENERIC_ALL # Maximum Allowed access type MaximumAllowed = 0x02000000 # Access System Acl access type AccessSystemSecurity = 0x01000000 # ADS_RIGHT_ACCESS_SYSTEM_SECURITY # Standard access types Synchronize = 0x00100000 # ADS_RIGHT_SYNCHRONIZE WriteOwner = 0x00080000 # ADS_RIGHT_WRITE_OWNER WriteDACL = 0x00040000 # ADS_RIGHT_WRITE_DAC ReadControl = 0x00020000 # ADS_RIGHT_READ_CONTROL Delete = 0x00010000 # ADS_RIGHT_DELETE # Specific rights AllExtendedRights = 0x00000100 # ADS_RIGHT_DS_CONTROL_ACCESS ListObject = 0x00000080 # ADS_RIGHT_DS_LIST_OBJECT DeleteTree = 0x00000040 # ADS_RIGHT_DS_DELETE_TREE WriteProperties = 0x00000020 # ADS_RIGHT_DS_WRITE_PROP ReadProperties = 0x00000010 # ADS_RIGHT_DS_READ_PROP Self = 0x00000008 # ADS_RIGHT_DS_SELF ListChildObjects = 0x00000004 # ADS_RIGHT_ACTRL_DS_LIST DeleteChild = 0x00000002 # ADS_RIGHT_DS_DELETE_CHILD CreateChild = 0x00000001 # ADS_RIGHT_DS_CREATE_CHILD # Simple permissions enum # Simple permissions are combinaisons of extended permissions # https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783530(v=ws.10)?redirectedfrom=MSDN class SIMPLE_PERMISSIONS(Enum): FullControl = 0xf01ff Modify = 0x0301bf ReadAndExecute = 0x0200a9 ReadAndWrite = 0x02019f Read = 0x20094 Write = 0x200bc # Mask ObjectType field enum # Possible values for the Mask field in object-specific ACE (permitting to specify extended rights in the ObjectType field for example) # Since these flags are the same for Allowed and Denied access, the same class will be used from 'ldaptypes' # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum): ControlAccess = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS CreateChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CREATE_CHILD DeleteChild = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_DELETE_CHILD ReadProperty = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP WriteProperty = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF class DACLedit(object): """docstring for setrbcd""" def __init__(self, ldap_server, ldap_session, args): super(DACLedit, self).__init__() self.ldap_server = ldap_server self.ldap_session = ldap_session self.target_sAMAccountName = args.target_sAMAccountName self.target_SID = args.target_SID self.target_DN = args.target_DN self.principal_sAMAccountName = args.principal_sAMAccountName self.principal_SID = args.principal_SID self.principal_DN = args.principal_DN self.ace_type = args.ace_type self.rights = args.rights self.rights_guid = args.rights_guid self.filename = args.filename self.inheritance = args.inheritance if self.inheritance: logging.info("NB: objects with adminCount=1 will no inherit ACEs from their parent container/OU") logging.debug('Initializing domainDumper()') cnf = ldapdomaindump.domainDumpConfig() cnf.basepath = None self.domain_dumper = ldapdomaindump.domainDumper(self.ldap_server, self.ldap_session, cnf) if args.mask is not None: if args.mask.startswith("0x"): self.force_mask = int(args.mask, 16) elif args.mask == "readwrite": self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP + \ ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP elif args.mask == "write": self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP elif args.mask == "self": self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF elif args.mask == "allext": self.force_mask = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_CONTROL_ACCESS else: self.force_mask = None if self.target_sAMAccountName or self.target_SID or self.target_DN: # Searching for target account with its security descriptor self.search_target_principal_security_descriptor() # Extract security descriptor data self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor) # Searching for the principal SID if any principal argument was given and principal_SID wasn't if self.principal_SID is None and self.principal_sAMAccountName is not None or self.principal_DN is not None: _lookedup_principal = "" if self.principal_sAMAccountName is not None: _lookedup_principal = self.principal_sAMAccountName self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid']) elif self.principal_DN is not None: _lookedup_principal = self.principal_DN self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid']) try: self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) logging.debug("Found principal SID: %s" % self.principal_SID) except IndexError: logging.error('Principal SID not found in LDAP (%s)' % _lookedup_principal) exit(1) # Main read funtion # Prints the parsed DACL def read(self): parsed_dacl = self.parseDACL(self.principal_security_descriptor['Dacl']) self.printparsedDACL(parsed_dacl) return # Main write function # Attempts to add a new ACE to a DACL def write(self): # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified # Append the ACEs in the DACL locally if self.rights == "FullControl" and self.rights_guid is None: logging.debug("Appending ACE (%s --(FullControl)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type)) elif self.rights == "Custom" and self.force_mask is not None: logging.debug("Appending ACE (%s --(Custom)--> %s)" % (self.principal_SID, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_ace(self.force_mask, self.principal_SID, self.ace_type)) else: for rights_guid in self.build_guids_for_rights(): logging.debug("Appending ACE (%s --(%s)--> %s)" % (self.principal_SID, rights_guid, format_sid(self.target_SID))) self.principal_security_descriptor['Dacl'].aces.append(self.create_object_ace(rights_guid, self.principal_SID, self.ace_type, force_mask=self.force_mask)) # Backups current DACL before add the new one self.backup() # Effectively push the DACL with the new ACE self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) return # Attempts to remove an ACE from the DACL # To do it, a new DACL is built locally with all the ACEs that must NOT BE removed, and this new DACL is pushed on the server def remove(self): compare_aces = [] # Creates ACEs with the specified GUIDs and the SID, or FullControl if no GUID is specified # These ACEs will be used as comparison templates if self.rights == "FullControl" and self.rights_guid is None: compare_aces.append(self.create_ace(SIMPLE_PERMISSIONS.FullControl.value, self.principal_SID, self.ace_type)) elif self.rights == "Custom" and self.force_mask is not None: compare_aces.append(self.create_ace(self.force_mask, self.principal_SID, self.ace_type)) else: for rights_guid in self.build_guids_for_rights(): compare_aces.append(self.create_object_ace(rights_guid, self.principal_SID, self.ace_type, force_mask=self.force_mask)) new_dacl = [] i = 0 dacl_must_be_replaced = False for ace in self.principal_security_descriptor['Dacl'].aces: ace_must_be_removed = False for compare_ace in compare_aces: # To be sure the good ACEs are removed, multiple fields are compared between the templates and the ACEs in the DACL # - ACE type # - ACE flags # - Access masks # - Revision # - SubAuthorityCount # - SubAuthority # - IdentifierAuthority value if ace['AceType'] == compare_ace['AceType'] \ and ace['AceFlags'] == compare_ace['AceFlags']\ and ace['Ace']['Mask']['Mask'] == compare_ace['Ace']['Mask']['Mask']\ and ace['Ace']['Sid']['Revision'] == compare_ace['Ace']['Sid']['Revision']\ and ace['Ace']['Sid']['SubAuthorityCount'] == compare_ace['Ace']['Sid']['SubAuthorityCount']\ and ace['Ace']['Sid']['SubAuthority'] == compare_ace['Ace']['Sid']['SubAuthority']\ and ace['Ace']['Sid']['IdentifierAuthority']['Value'] == compare_ace['Ace']['Sid']['IdentifierAuthority']['Value']: # If the ACE has an ObjectType, the GUIDs must match if 'ObjectType' in ace['Ace'].fields.keys() and 'ObjectType' in compare_ace['Ace'].fields.keys(): if ace['Ace']['ObjectType'] == compare_ace['Ace']['ObjectType']: ace_must_be_removed = True dacl_must_be_replaced = True else: ace_must_be_removed = True dacl_must_be_replaced = True # If the ACE doesn't match any ACEs from the template list, it is added to the DACL that will be pushed if not ace_must_be_removed: new_dacl.append(ace) elif logging.getLogger().level == logging.DEBUG: logging.debug("This ACE will be removed") self.printparsedACE(self.parseACE(ace)) i += 1 # If at least one ACE must been removed if dacl_must_be_replaced: self.principal_security_descriptor['Dacl'].aces = new_dacl self.backup() self.modify_secDesc_for_dn(self.target_principal.entry_dn, self.principal_security_descriptor) else: logging.info("Nothing to remove...") # Permits to backup a DACL before a modification # This function is called before any writing action (write, remove or restore) def backup(self): backup = {} backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode('utf-8') backup["dn"] = self.target_principal.entry_dn if not self.filename: self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") else: if os.path.exists(self.filename): logging.info("File %s already exists, I'm refusing to overwrite it, setting another filename" % self.filename) self.filename = 'dacledit-%s.bak' % datetime.datetime.now().strftime("%Y%m%d-%H%M%S") with codecs.open(self.filename, 'w', 'utf-8') as outfile: json.dump(backup, outfile) logging.info('DACL backed up to %s', self.filename) # Permits to restore a saved DACL def restore(self): # Opens and load the file where the DACL has been saved with codecs.open(self.filename, 'r', 'utf-8') as infile: restore = json.load(infile) assert "sd" in restore.keys() assert "dn" in restore.keys() # Extracts the Security Descriptor and converts it to the good ldaptypes format new_raw_security_descriptor = binascii.unhexlify(restore["sd"].encode('utf-8')) new_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=new_raw_security_descriptor) self.target_DN = restore["dn"] # Searching for target account with its security descriptor self.search_target_principal_security_descriptor() # Extract security descriptor data self.principal_raw_security_descriptor = self.target_principal['nTSecurityDescriptor'].raw_values[0] self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor) # Do a backup of the actual DACL and push the restoration self.backup() logging.info('Restoring DACL') self.modify_secDesc_for_dn(self.target_DN, new_security_descriptor) # Attempts to retrieve the DACL in the Security Descriptor of the specified target def search_target_principal_security_descriptor(self): _lookedup_principal = "" # Set SD flags to only query for DACL controls = security_descriptor_control(sdflags=0x04) if self.target_sAMAccountName is not None: _lookedup_principal = self.target_sAMAccountName self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['nTSecurityDescriptor'], controls=controls) elif self.target_SID is not None: _lookedup_principal = self.target_SID self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) elif self.target_DN is not None: _lookedup_principal = self.target_DN self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['nTSecurityDescriptor'], controls=controls) try: self.target_principal = self.ldap_session.entries[0] logging.debug('Target principal found in LDAP (%s)' % _lookedup_principal) except IndexError: logging.error('Target principal not found in LDAP (%s)' % _lookedup_principal) exit(0) # Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName # Not used for the moment # - samname : a sAMAccountName def get_user_info(self, samname): self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(samname), attributes=['objectSid']) try: dn = self.ldap_session.entries[0].entry_dn sid = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) return dn, sid except IndexError: logging.error('User not found in LDAP: %s' % samname) return False # Attempts to resolve a SID and return the corresponding samaccountname # - sid : the SID to resolve def resolveSID(self, sid): # Tries to resolve the SID from the well known SIDs if sid in WELL_KNOWN_SIDS.keys(): return WELL_KNOWN_SIDS[sid] # Tries to resolve the SID from the LDAP domain dump else: self.ldap_session.search(self.domain_dumper.root, '(objectSid=%s)' % sid, attributes=['samaccountname']) try: dn = self.ldap_session.entries[0].entry_dn samname = self.ldap_session.entries[0]['samaccountname'] return samname except IndexError: logging.debug('SID not found in LDAP: %s' % sid) return "" # Parses a full DACL # - dacl : the DACL to parse, submitted in a Security Desciptor format def parseDACL(self, dacl): parsed_dacl = [] logging.info("Parsing DACL") i = 0 for ace in dacl['Data']: parsed_ace = self.parseACE(ace) parsed_dacl.append(parsed_ace) i += 1 return parsed_dacl # Parses an access mask to extract the different values from a simple permission # https://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights # - fsr : the access mask to parse def parsePerms(self, fsr): _perms = [] for PERM in SIMPLE_PERMISSIONS: if (fsr & PERM.value) == PERM.value: _perms.append(PERM.name) fsr = fsr & (~ PERM.value) for PERM in ACCESS_MASK: if fsr & PERM.value: _perms.append(PERM.name) return _perms # Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType) # - ace : the ACE to parse def parseACE(self, ace): # For the moment, only the Allowed and Denied Access ACE are supported if ace['TypeName'] in [ "ACCESS_ALLOWED_ACE", "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_ACE", "ACCESS_DENIED_OBJECT_ACE" ]: parsed_ace = {} parsed_ace['ACE Type'] = ace['TypeName'] # Retrieves ACE's flags _ace_flags = [] for FLAG in ACE_FLAGS: if ace.hasFlag(FLAG.value): _ace_flags.append(FLAG.name) parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None" # For standard ACE # Extracts the access mask (by parsing the simple permissions) and the principal's SID if ace['TypeName'] in [ "ACCESS_ALLOWED_ACE", "ACCESS_DENIED_ACE" ]: parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(self.parsePerms(ace['Ace']['Mask']['Mask'])), ace['Ace']['Mask']['Mask']) parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) # For object-specific ACE elif ace['TypeName'] in [ "ACCESS_ALLOWED_OBJECT_ACE", "ACCESS_DENIED_OBJECT_ACE" ]: # Extracts the mask values. These values will indicate the ObjectType purpose _access_mask_flags = [] for FLAG in ALLOWED_OBJECT_ACE_MASK_FLAGS: if ace['Ace']['Mask'].hasPriv(FLAG.value): _access_mask_flags.append(FLAG.name) parsed_ace['Access mask'] = "%s (0x%x)" % (", ".join(_access_mask_flags), ace['Ace']['Mask']['Mask']) # Extracts the ACE flag values and the trusted SID _object_flags = [] for FLAG in OBJECT_ACE_FLAGS: if ace['Ace'].hasFlag(FLAG.value): _object_flags.append(FLAG.name) parsed_ace['Flags'] = ", ".join(_object_flags) or "None" # Extracts the ObjectType GUID values if ace['Ace']['ObjectTypeLen'] != 0: obj_type = bin_to_string(ace['Ace']['ObjectType']).lower() try: parsed_ace['Object type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[obj_type], obj_type) except KeyError: parsed_ace['Object type (GUID)'] = "UNKNOWN (%s)" % obj_type # Extracts the InheritedObjectType GUID values if ace['Ace']['InheritedObjectTypeLen'] != 0: inh_obj_type = bin_to_string(ace['Ace']['InheritedObjectType']).lower() try: parsed_ace['Inherited type (GUID)'] = "%s (%s)" % (OBJECT_TYPES_GUID[inh_obj_type], inh_obj_type) except KeyError: parsed_ace['Inherited type (GUID)'] = "UNKNOWN (%s)" % inh_obj_type # Extract the Trustee SID (the object that has the right over the DACL bearer) parsed_ace['Trustee (SID)'] = "%s (%s)" % (self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or "UNKNOWN", ace['Ace']['Sid'].formatCanonical()) else: # If the ACE is not an access allowed logging.debug("ACE Type (%s) unsupported for parsing yet, feel free to contribute" % ace['TypeName']) parsed_ace = {} parsed_ace['ACE type'] = ace['TypeName'] _ace_flags = [] for FLAG in ACE_FLAGS: if ace.hasFlag(FLAG.value): _ace_flags.append(FLAG.name) parsed_ace['ACE flags'] = ", ".join(_ace_flags) or "None" parsed_ace['DEBUG'] = "ACE type not supported for parsing by dacleditor.py, feel free to contribute" return parsed_ace # Prints a full DACL by printing each parsed ACE # - parsed_dacl : a parsed DACL from parseDACL() def printparsedDACL(self, parsed_dacl): # Attempts to retrieve the principal's SID if it's a write action if self.principal_SID is None and self.principal_sAMAccountName or self.principal_DN: if self.principal_sAMAccountName is not None: _lookedup_principal = self.principal_sAMAccountName self.ldap_session.search(self.domain_dumper.root, '(sAMAccountName=%s)' % escape_filter_chars(_lookedup_principal), attributes=['objectSid']) elif self.principal_DN is not None: _lookedup_principal = self.principal_DN self.ldap_session.search(_lookedup_principal, '(distinguishedName=%s)' % _lookedup_principal, attributes=['objectSid']) try: self.principal_SID = format_sid(self.ldap_session.entries[0]['objectSid'].raw_values[0]) except IndexError: logging.error('Principal not found in LDAP (%s)' % _lookedup_principal) return False logging.debug("Found principal SID to write in ACE(s): %s" % self.principal_SID) logging.info("Printing parsed DACL") i = 0 # If a principal has been specified, only the ACE where he is the trustee will be printed if self.principal_SID is not None: logging.info("Filtering results for SID (%s)" % self.principal_SID) for parsed_ace in parsed_dacl: print_ace = True if self.principal_SID is not None: try: if self.principal_SID not in parsed_ace['Trustee (SID)']: print_ace = False except Exception as e: logging.error("Error filtering ACE, probably because of ACE type unsupported for parsing yet (%s)" % e) if print_ace: logging.info(" %-28s" % "ACE[%d] info" % i) self.printparsedACE(parsed_ace) i += 1 # Prints properly a parsed ACE # - parsed_ace : a parsed ACE from parseACE() def printparsedACE(self, parsed_ace): elements_name = list(parsed_ace.keys()) for attribute in elements_name: logging.info(" %-26s: %s" % (attribute, parsed_ace[attribute])) # Retrieves the GUIDs for the specified rights def build_guids_for_rights(self): _rights_guids = [] if self.rights_guid is not None: _rights_guids = [self.rights_guid] elif self.rights == "WriteMembers": _rights_guids = [RIGHTS_GUID.WriteMembers.value] elif self.rights == "ResetPassword": _rights_guids = [RIGHTS_GUID.ResetPassword.value] elif self.rights == "DCSync": _rights_guids = [RIGHTS_GUID.DS_Replication_Get_Changes.value, RIGHTS_GUID.DS_Replication_Get_Changes_All.value] logging.debug('Built GUID: %s', _rights_guids) return _rights_guids # Attempts to push the locally built DACL to the remote server into the security descriptor of the specified principal # The target principal is specified with its Distinguished Name # - dn : the principal's Distinguished Name to modify # - secDesc : the Security Descriptor with the new DACL to push def modify_secDesc_for_dn(self, dn, secDesc): data = secDesc.getData() controls = security_descriptor_control(sdflags=0x04) logging.debug('Attempts to modify the Security Descriptor.') self.ldap_session.modify(dn, {'nTSecurityDescriptor': (ldap3.MODIFY_REPLACE, [data])}, controls=controls) if self.ldap_session.result['result'] == 0: logging.info('DACL modified successfully!') else: if self.ldap_session.result['result'] == 50: logging.error('Could not modify object, the server reports insufficient rights: %s', self.ldap_session.result['message']) elif self.ldap_session.result['result'] == 19: logging.error('Could not modify object, the server reports a constrained violation: %s', self.ldap_session.result['message']) else: logging.error('The server returned an error: %s', self.ldap_session.result['message']) # Builds a standard ACE for a specified access mask (rights) and a specified SID (the principal who obtains the right) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/72e7c7ea-bc02-4c74-a619-818a16bf6adb # - access_mask : the allowed access mask # - sid : the principal's SID # - ace_type : the ACE type (allowed or denied) def create_ace(self, access_mask, sid, ace_type): nace = ldaptypes.ACE() if ace_type == "allowed": nace['AceType'] = ldaptypes.ACCESS_ALLOWED_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_ALLOWED_ACE() else: nace['AceType'] = ldaptypes.ACCESS_DENIED_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_DENIED_ACE() if self.inheritance: nace['AceFlags'] = ldaptypes.ACE.OBJECT_INHERIT_ACE + ldaptypes.ACE.CONTAINER_INHERIT_ACE else: nace['AceFlags'] = 0x00 acedata['Mask'] = ldaptypes.ACCESS_MASK() acedata['Mask']['Mask'] = access_mask acedata['Sid'] = ldaptypes.LDAP_SID() acedata['Sid'].fromCanonical(sid) nace['Ace'] = acedata logging.debug('ACE created.') return nace # Builds an object-specific for a specified ObjectType (an extended right, a property, etc, to add) for a specified SID (the principal who obtains the right) # The Mask is "ADS_RIGHT_DS_CONTROL_ACCESS" (the ObjectType GUID will identify an extended access right) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe # - privguid : the ObjectType (an Extended Right here) # - sid : the principal's SID # - ace_type : the ACE type (allowed or denied) def create_object_ace(self, privguid, sid, ace_type, force_mask=None): nace = ldaptypes.ACE() if ace_type == "allowed": nace['AceType'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE() else: nace['AceType'] = ldaptypes.ACCESS_DENIED_OBJECT_ACE.ACE_TYPE acedata = ldaptypes.ACCESS_DENIED_OBJECT_ACE() if self.inheritance: nace['AceFlags'] = ldaptypes.ACE.OBJECT_INHERIT_ACE + ldaptypes.ACE.CONTAINER_INHERIT_ACE else: nace['AceFlags'] = 0x00 acedata['Mask'] = ldaptypes.ACCESS_MASK() # WriteMembers not an extended right, we need read and write mask on the attribute (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/c79a383c-2b3f-4655-abe7-dcbb7ce0cfbe) # force_mask in the case we give the -rights-guid option if force_mask is not None: acedata['Mask']['Mask'] = force_mask elif privguid == RIGHTS_GUID.WriteMembers.value: acedata['Mask']['Mask'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_READ_PROP + ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_WRITE_PROP # Other rights in this script are extended rights and need the DS_CONTROL_ACCESS mask else: 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() # This ACE flag verifes if the ObjectType is valid acedata['Flags'] = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ACE_OBJECT_TYPE_PRESENT nace['Ace'] = acedata logging.debug('Object-specific ACE created.') return nace def parse_args(): parser = argparse.ArgumentParser(add_help=True, description='Python editor for a principal\'s DACL.') parser.add_argument('identity', action='store', help='domain.local/username[:password]') parser.add_argument('-use-ldaps', action='store_true', help='Use LDAPS instead of LDAP') parser.add_argument('-ts', action='store_true', help='Adds timestamp to every logging output') parser.add_argument('-debug', action='store_true', help='Turn DEBUG output ON') auth_con = parser.add_argument_group('authentication & connection') auth_con.add_argument('-hashes', action="store", metavar="LMHASH:NTHASH", help='NTLM hashes, format is LMHASH:NTHASH') auth_con.add_argument('-no-pass', action="store_true", help='don\'t ask for password (useful for -k)') auth_con.add_argument('-k', action="store_true", help='Use Kerberos authentication. Grabs credentials from ccache file (KRB5CCNAME) based on target parameters. If valid credentials cannot be found, it will use the ones specified in the command line') auth_con.add_argument('-aesKey', action="store", metavar="hex key", help='AES key to use for Kerberos Authentication (128 or 256 bits)') auth_con.add_argument('-dc-ip', action='store', metavar="ip address", help='IP Address of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted it will use the domain part (FQDN) specified in the identity parameter') auth_con.add_argument('-dc-host', action='store', metavar="hostname", help='Hostname of the domain controller or KDC (Key Distribution Center) for Kerberos. If omitted, -dc-ip will be used') principal_parser = parser.add_argument_group("principal", description="Object, controlled by the attacker, to reference in the ACE to create or to filter when printing a DACL") principal_parser.add_argument("-principal", dest="principal_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") principal_parser.add_argument("-principal-sid", dest="principal_SID", metavar="SID", type=str, required=False, help="Security IDentifier") principal_parser.add_argument("-principal-dn", dest="principal_DN", metavar="DN", type=str, required=False, help="Distinguished Name") target_parser = parser.add_argument_group("target", description="Principal object to read/edit the DACL of") target_parser.add_argument("-target", dest="target_sAMAccountName", metavar="NAME", type=str, required=False, help="sAMAccountName") target_parser.add_argument("-target-sid", dest="target_SID", metavar="SID", type=str, required=False, help="Security IDentifier") target_parser.add_argument("-target-dn", dest="target_DN", metavar="DN", type=str, required=False, help="Distinguished Name") dacl_parser = parser.add_argument_group("dacl editor") dacl_parser.add_argument('-action', choices=['read', 'write', 'remove', 'backup', 'restore'], nargs='?', default='read', help='Action to operate on the DACL') dacl_parser.add_argument('-file', dest="filename", type=str, help='Filename/path (optional for -action backup, required for -restore))') dacl_parser.add_argument('-ace-type', choices=['allowed', 'denied'], nargs='?', default='allowed', help='The ACE Type (access allowed or denied) that must be added or removed (default: allowed)') dacl_parser.add_argument('-rights', choices=['FullControl', 'ResetPassword', 'WriteMembers', 'DCSync', 'Custom'], nargs='?', default='FullControl', help='Rights to write/remove in the target DACL (default: FullControl)') dacl_parser.add_argument('-rights-guid', type=str, help='Manual GUID representing the right to write/remove') dacl_parser.add_argument('-mask', nargs='?', default=None, help='Force access mask, possible values: readwrite, write, self, allext, 0xXXXXX. Useful with -rights Custom or --rights-guid where the mask is different of read+write.') dacl_parser.add_argument('-inheritance', action="store_true", help='Enable the inheritance in the ACE flag with CONTAINER_INHERIT_ACE and OBJECT_INHERIT_ACE. Useful when target is a Container or an OU, ' 'ACE will be inherited by objects within the container/OU (except objects with adminCount=1)') if len(sys.argv) == 1: parser.print_help() sys.exit(1) return parser.parse_args() def main(): print(version.BANNER) args = parse_args() logger.init(args.ts, args.debug) if args.action == 'write' and args.principal_sAMAccountName is None and args.principal_SID is None and args.principal_DN is None: logging.critical('-principal, -principal-sid, or -principal-dn should be specified when using -action write') sys.exit(1) if args.action == "restore" and not args.filename: logging.critical('-file is required when using -action restore') domain, username, password, lmhash, nthash, args.k = parse_identity(args.identity, args.hashes, args.no_pass, args.aesKey, args.k) try: ldap_server, ldap_session = init_ldap_session(domain, username, password, lmhash, nthash, args.k, args.dc_ip, args.dc_host, args.aesKey, args.use_ldaps) dacledit = DACLedit(ldap_server, ldap_session, args) if args.action == 'read': dacledit.read() elif args.action == 'write': dacledit.write() elif args.action == 'remove': dacledit.remove() elif args.action == 'flush': dacledit.flush() elif args.action == 'backup': dacledit.backup() elif args.action == 'restore': dacledit.restore() except Exception as e: if logging.getLogger().level == logging.DEBUG: traceback.print_exc() logging.error(str(e)) if __name__ == '__main__': main()