Skip to content

Commit 97838b4

Browse files
authored
Merge pull request Pennyw0rth#832 from tiagomanunes/improve-daclread-targetdn-from-file
Improve daclread: also allow passing a file for TARGET_DN, and refactor
2 parents b1faef4 + 4522f00 commit 97838b4

1 file changed

Lines changed: 78 additions & 114 deletions

File tree

nxc/modules/daclread.py

Lines changed: 78 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import binascii
2-
import codecs
32
import json
43
import datetime
54
from enum import Enum
@@ -189,6 +188,12 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
189188
Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF
190189

191190

191+
SEARCH_FILTERS = {
192+
"TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})",
193+
"TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})"
194+
}
195+
196+
192197
class NXCModule:
193198
"""Module to read and backup the Discretionary Access Control List of one or multiple objects.
194199
@@ -205,6 +210,14 @@ def __init__(self, context=None, module_options=None):
205210
self.context = context
206211
self.module_options = module_options
207212

213+
# Initialize module variables
214+
self.principal_sAMAccountName = None
215+
self.principal_sid = None
216+
self.action = "read"
217+
self.ace_type = "allowed"
218+
self.rights = None
219+
self.rights_guid = None
220+
208221
def options(self, context, module_options):
209222
"""
210223
Be careful, this module cannot read the DACLS recursively.
@@ -226,57 +239,48 @@ def options(self, context, module_options):
226239
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
227240
sys.exit(1)
228241

229-
if module_options and "TARGET" in module_options:
230-
context.log.debug("There is a target specified!")
231-
if isfile(module_options["TARGET"]):
232-
try:
233-
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
234-
self.target_sAMAccountName = None
235-
except Exception:
236-
context.log.fail("The file doesn't exist or cannot be opened.")
237-
else:
238-
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
239-
self.target_sAMAccountName = module_options["TARGET"]
240-
self.target_file = None
241-
self.target_DN = None
242+
self.targets = []
242243
self.target_SID = None
243-
if module_options and "TARGET_DN" in module_options:
244-
self.target_DN = module_options["TARGET_DN"]
245-
self.target_sAMAccountName = None
246-
self.target_file = None
247244

248-
if module_options and "PRINCIPAL" in module_options:
245+
for option in "TARGET", "TARGET_DN":
246+
if option in module_options:
247+
context.log.debug("There is a target specified!")
248+
if isfile(module_options[option]):
249+
try:
250+
target_file = open(module_options[option]) # noqa: SIM115
251+
for line in target_file:
252+
context.log.debug(f"Adding target from file: {line}")
253+
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
254+
except Exception:
255+
context.log.fail("The file doesn't exist or cannot be opened.")
256+
else:
257+
context.log.debug(f"Adding target: {module_options[option]}")
258+
self.targets.append((module_options[option].strip(), SEARCH_FILTERS[option]))
259+
260+
if not self.targets:
261+
context.log.fail("No target specified, please specify at least one target with the TARGET or TARGET_DN options.")
262+
sys.exit(1)
263+
264+
if "PRINCIPAL" in module_options:
249265
self.principal_sAMAccountName = module_options["PRINCIPAL"]
250-
else:
251-
self.principal_sAMAccountName = None
252-
self.principal_sid = None
253266

254-
if module_options and "ACTION" in module_options:
267+
if "ACTION" in module_options:
255268
self.action = module_options["ACTION"]
256-
else:
257-
self.action = "read"
258-
if module_options and "ACE_TYPE" in module_options:
269+
270+
if "ACE_TYPE" in module_options:
259271
self.ace_type = module_options["ACE_TYPE"]
260-
else:
261-
self.ace_type = "allowed"
262-
if module_options and "RIGHTS" in module_options:
272+
273+
if "RIGHTS" in module_options:
263274
self.rights = module_options["RIGHTS"]
264-
else:
265-
self.rights = None
266-
if module_options and "RIGHTS_GUID" in module_options:
275+
276+
if "RIGHTS_GUID" in module_options:
267277
self.rights_guid = module_options["RIGHTS_GUID"]
268-
else:
269-
self.rights_guid = None
270-
self.filename = None
271278

272279
def on_login(self, context, connection):
273-
self.context = context
274280
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
275281
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
276-
self.baseDN = connection.ldap_connection._baseDN
277-
self.ldap_session = connection.ldap_connection
278-
self.connection = connection
279282
self.context = context
283+
self.connection = connection
280284

281285
# Searching for the principal SID
282286
if self.principal_sAMAccountName is not None:
@@ -294,86 +298,49 @@ def on_login(self, context, connection):
294298
return
295299

296300
# Searching for the targets SID and their Security Descriptors
297-
# If there is only one target
298-
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
301+
for target, search_filter in self.targets:
299302
try:
300303
# Searching for target account with its security descriptor
301-
if self.target_sAMAccountName: # noqa: SIM108
302-
search_filter = f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})"
303-
else:
304-
search_filter = f"(distinguishedName={escape_filter_chars(self.target_DN)})"
305-
306304
resp = connection.search(
307-
searchFilter=search_filter,
305+
searchFilter=search_filter(target),
308306
attributes=["distinguishedName", "nTSecurityDescriptor"],
309307
searchControls=security_descriptor_control(sdflags=0x04),
310308
)
311309
resp_parsed = parse_result_attributes(resp)[0]
312310

313311
# Extract security descriptor data
314-
self.target_principal_dn = resp_parsed["distinguishedName"]
315-
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
316-
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
317-
context.log.highlight(f"Target principal found in LDAP ({self.target_principal_dn})")
312+
target_principal_dn = resp_parsed["distinguishedName"]
313+
principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
314+
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=principal_raw_security_descriptor)
315+
context.log.highlight(f"Target principal found in LDAP ({target_principal_dn})")
318316
except Exception as e:
319-
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
317+
context.log.fail(f"Target SID not found in LDAP ({target})")
320318
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
321-
return
319+
continue
322320

323321
if self.action == "read":
324-
self.read(context)
322+
self.read(principal_security_descriptor)
325323
if self.action == "backup":
326-
self.backup(context)
327-
328-
# If there are multiple targets
329-
else:
330-
targets = self.target_file.readlines()
331-
for target in targets:
332-
try:
333-
self.target_sAMAccountName = target.strip()
334-
# Searching for target account with its security descriptor
335-
resp = connection.search(
336-
searchFilter=f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})",
337-
attributes=["distinguishedName", "nTSecurityDescriptor"],
338-
searchControls=security_descriptor_control(sdflags=0x04),
339-
)
340-
resp_parsed = parse_result_attributes(resp)[0]
341-
342-
# Extract security descriptor data
343-
self.target_principal_dn = resp_parsed["distinguishedName"]
344-
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
345-
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
346-
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
347-
except Exception:
348-
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
349-
continue
350-
351-
if self.action == "read":
352-
self.read(context)
353-
if self.action == "backup":
354-
self.backup(context)
324+
self.backup(target, target_principal_dn, principal_raw_security_descriptor)
355325

356326
# Main read funtion
357327
# Prints the parsed DACL
358-
def read(self, context):
359-
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
360-
self.print_parsed_dacl(context, parsed_dacl)
328+
def read(self, principal_security_descriptor):
329+
parsed_dacl = self.parse_dacl(principal_security_descriptor["Dacl"])
330+
self.print_parsed_dacl(parsed_dacl)
361331

362332
# Permits to export the DACL of the targets
363333
# This function is called before any writing action (write, remove or restore)
364-
def backup(self, context):
334+
def backup(self, target, target_principal_dn, principal_raw_security_descriptor):
365335
backup = {}
366-
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode("latin-1")
367-
backup["dn"] = str(self.target_principal_dn)
368-
if not self.filename:
369-
self.filename = "dacledit-{}-{}.bak".format(
370-
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
371-
self.target_sAMAccountName,
372-
)
373-
with codecs.open(self.filename, "w", "latin-1") as outfile:
336+
backup["sd"] = binascii.hexlify(principal_raw_security_descriptor).decode("latin-1")
337+
backup["dn"] = str(target_principal_dn)
338+
339+
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
340+
filename = f"dacledit-{timestamp}-{target}.bak"
341+
with open(filename, "w", encoding="latin-1") as outfile:
374342
json.dump(backup, outfile)
375-
context.log.highlight("DACL backed up to %s", self.filename)
376-
self.filename = None
343+
self.context.log.highlight(f"DACL backed up to {filename}")
377344

378345
def resolveSID(self, sid):
379346
"""Resolves a SID to its corresponding sAMAccountName."""
@@ -394,11 +361,11 @@ def resolveSID(self, sid):
394361

395362
# Parses a full DACL
396363
# - dacl : the DACL to parse, submitted in a Security Desciptor format
397-
def parse_dacl(self, context, dacl):
364+
def parse_dacl(self, dacl):
398365
parsed_dacl = []
399-
context.log.debug("Parsing DACL")
366+
self.context.log.debug("Parsing DACL")
400367
for ace in dacl["Data"]:
401-
parsed_ace = self.parse_ace(context, ace)
368+
parsed_ace = self.parse_ace(ace)
402369
parsed_dacl.append(parsed_ace)
403370
return parsed_dacl
404371

@@ -413,7 +380,7 @@ def parse_perms(self, access_mask):
413380

414381
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
415382
# - ace : the ACE to parse
416-
def parse_ace(self, context, ace):
383+
def parse_ace(self, ace):
417384
# For the moment, only the Allowed and Denied Access ACE are supported
418385
if ace["TypeName"] in [
419386
"ACCESS_ALLOWED_ACE",
@@ -455,12 +422,9 @@ def parse_ace(self, context, ace):
455422
except KeyError:
456423
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
457424
# Extract the Trustee SID (the object that has the right over the DACL bearer)
458-
parsed_ace["Trustee (SID)"] = "{} ({})".format(
459-
self.resolveSID(ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
460-
ace["Ace"]["Sid"].formatCanonical(),
461-
)
425+
parsed_ace["Trustee (SID)"] = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
462426
else: # if the ACE is not an access allowed
463-
context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
427+
self.context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
464428
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
465429
parsed_ace = {
466430
"ACE type": ace["TypeName"],
@@ -469,18 +433,18 @@ def parse_ace(self, context, ace):
469433
}
470434
return parsed_ace
471435

472-
def print_parsed_dacl(self, context, parsed_dacl):
436+
def print_parsed_dacl(self, parsed_dacl):
473437
"""Prints a full DACL by printing each parsed ACE
474438
475439
parsed_dacl : a parsed DACL from parse_dacl()
476440
"""
477-
context.log.debug("Printing parsed DACL")
441+
self.context.log.debug("Printing parsed DACL")
478442
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
479443
# If an ACE type has been specified, only the ACE with this type will be specified
480444
# If a principal has been specified, only the ACE where he is the trustee will be printed
481445
for i, parsed_ace in enumerate(parsed_dacl):
482446
print_ace = True
483-
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
447+
self.context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
484448

485449
# Filter on specific rights
486450
if self.rights is not None:
@@ -494,37 +458,37 @@ def print_parsed_dacl(self, context, parsed_dacl):
494458
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
495459
print_ace = False
496460
except Exception as e:
497-
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
461+
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
498462

499463
# Filter on specific right GUID
500464
if self.rights_guid is not None:
501465
try:
502466
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
503467
print_ace = False
504468
except Exception as e:
505-
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
469+
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
506470

507471
# Filter on ACE type
508472
if self.ace_type == "allowed":
509473
try:
510474
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
511475
print_ace = False
512476
except Exception as e:
513-
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
477+
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
514478
else:
515479
try:
516480
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
517481
print_ace = False
518482
except Exception as e:
519-
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
483+
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
520484

521485
# Filter on trusted principal
522486
if self.principal_sid is not None:
523487
try:
524488
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
525489
print_ace = False
526490
except Exception as e:
527-
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
491+
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
528492
if print_ace:
529493
self.context.log.highlight(f"ACE[{i}] info")
530494
self.print_parsed_ace(parsed_ace)

0 commit comments

Comments
 (0)