mirror of
https://github.com/fortra/impacket
synced 2026-06-08 14:15:13 +00:00
794 lines
41 KiB
Python
Executable File
794 lines
41 KiB
Python
Executable File
#!/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()
|