From 0eee328ea044cd6b1456b92a2e97bd1b418e53bd Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Wed, 20 Mar 2024 21:59:38 -0400 Subject: [PATCH] feat(ldap): allow for specifying specific users to dump for ldap and print last password set date --- nxc/parsers/ldap_results.py | 13 ++++++ nxc/protocols/ldap.py | 76 +++++++++++++++++++------------- nxc/protocols/ldap/proto_args.py | 3 +- 3 files changed, 61 insertions(+), 31 deletions(-) create mode 100644 nxc/parsers/ldap_results.py diff --git a/nxc/parsers/ldap_results.py b/nxc/parsers/ldap_results.py new file mode 100644 index 00000000..206fad8b --- /dev/null +++ b/nxc/parsers/ldap_results.py @@ -0,0 +1,13 @@ +from impacket.ldap import ldapasn1 as ldapasn1_impacket + +def parse_result_attributes(ldap_response): + parsed_response = [] + for entry in ldap_response: + # SearchResultReferences may be returned + if not isinstance(entry, ldapasn1_impacket.SearchResultEntry): + continue + attribute_map = {} + for attribute in entry["attributes"]: + attribute_map[str(attribute["type"])] = str(attribute["vals"][0]) + parsed_response.append(attribute_map) + return parsed_response \ No newline at end of file diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index a21fe324..2d90d822 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -5,7 +5,7 @@ import hmac import os import socket from binascii import hexlify -from datetime import datetime +from datetime import datetime, timedelta from re import sub, I from zipfile import ZipFile from termcolor import colored @@ -38,6 +38,7 @@ from nxc.logger import NXCAdapter, nxc_logger from nxc.protocols.ldap.bloodhound import BloodHound from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.protocols.ldap.kerberos import KerberosAttacks +from nxc.parsers.ldap_results import parse_result_attributes ldap_error_status = { "1": "STATUS_NOT_SUPPORTED", @@ -753,37 +754,51 @@ class ldap(connection): return False def users(self): - # Building the search filter - search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)" - attributes = [ - "sAMAccountName", - "description", - "badPasswordTime", - "badPwdCount", - "pwdLastSet", - ] + """ + Retrieves user information from the LDAP server. - resp = self.search(search_filter, attributes, sizeLimit=0) + Args: + input_attributes (list): Optional. List of attributes to retrieve for each user. + TODO: allow users to pass this in + + Returns: + None + """ + if len(self.args.users) > 1: + self.logger.display(f"Trying to dumping users: {', '.join(self.args.users)}") + search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})" + else: + self.logger.info("Trying to dump all users") + search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)" + + # default to these attributes to mirror the SMB --users functionality + request_attributes = self.args.ldap_attributes + self.logger.debug(f"{request_attributes=}") + + resp = self.search(search_filter, request_attributes, sizeLimit=0) if resp: - self.logger.display(f"Total of records returned {len(resp):d}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - description = "" - try: - if self.username == "": - self.logger.highlight(f"{item['objectName']}") - else: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - elif str(attribute["type"]) == "description": - description = str(attribute["vals"][0]) - self.logger.highlight(f"{sAMAccountName:<30} {description}") - except Exception as e: - self.logger.debug(f"Skipping item, cannot process due to error {e}") - return + # I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it + if self.username == "": + self.logger.display(f"Total records returned: {len(resp):d}") + for item in resp: + if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: + continue + self.logger.highlight(f"{item['objectName']}") + return + + users = parse_result_attributes(resp) + # we print the total records after we parse the results since often SearchResultReferences are returned + self.logger.display(f"Total records returned: {len(users):d}") + self.logger.highlight(f"{'Username':<30} {'Last PW Set':<20}\t{'Description'}") # header + for user in users: + self.logger.debug(f"{user=}") + # we default attributes to blank strings if they don't exist in the dict + timestamp_seconds = int(user.get('pwdLastSet', '')) / 10**7 + start_date = datetime(1601, 1, 1) + parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S") + if parsed_pw_last_set == "1601-01-01 00:00:00": + parsed_pw_last_set = "" + self.logger.highlight(f"{user.get('sAMAccountName', ''):<30} {parsed_pw_last_set:<20}\t{user.get('description', '')}") def groups(self): # Building the search filter @@ -1390,3 +1405,4 @@ class ldap(connection): if each_file.startswith(self.output_filename.split("/")[-1]) and each_file.endswith("json"): z.write(each_file) os.remove(each_file) + diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index ff5866e0..5c46a2b3 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -20,11 +20,12 @@ def proto_args(parser, std_parser, module_parser): vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION") vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD") vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1") - vgroup.add_argument("--users", action="store_true", help="Enumerate enabled domain users") + vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users") vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups") vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers") vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid") vgroup.add_argument("--active-users", action="store_true", help="Get Active Domain Users Accounts") + vgroup.add_argument("--ldap-attributes", nargs="+", default=["sAMAccountName", "description", "pwdLastSet"], help="Attributes to search for") ggroup = ldap_parser.add_argument_group("Retrevie gmsa on the remote DC", "Options to play with gmsa") ggroup.add_argument("--gmsa", action="store_true", help="Enumerate GMSA passwords")