#!/usr/pkg/bin/python3.14
# 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()
