diff --git a/nxc/modules/get-unixUserPassword.py b/nxc/modules/get-unixUserPassword.py index fbf88a99..1ab24eaa 100644 --- a/nxc/modules/get-unixUserPassword.py +++ b/nxc/modules/get-unixUserPassword.py @@ -1,6 +1,6 @@ -from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap import ldap as ldap_impacket from nxc.logger import nxc_logger +from nxc.parsers.ldap_results import parse_result_attributes class NXCModule: @@ -20,7 +20,7 @@ class NXCModule: """ def on_login(self, context, connection): - searchFilter = "(objectclass=user)" + searchFilter = "(unixUserPassword=*)" try: context.log.debug(f"Search Filter={searchFilter}") @@ -37,27 +37,10 @@ class NXCModule: nxc_logger.debug(e) return False - answers = [] - context.log.debug(f"Total of records returned {len(resp)}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - unixUserPassword = [] - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - elif str(attribute["type"]) == "unixUserPassword": - unixUserPassword = [str(i) for i in attribute["vals"]] - if sAMAccountName != "" and len(unixUserPassword) > 0: - answers.append([sAMAccountName, unixUserPassword]) - except Exception as e: - context.log.debug("Exception:", exc_info=True) - context.log.debug(f"Skipping item, cannot process due to error {e!s}") - if len(answers) > 0: + if resp: + resp_parsed = parse_result_attributes(resp) context.log.success("Found following users: ") - for answer in answers: - context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}") + for user in resp_parsed: + context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}") else: context.log.fail("No unixUserPassword Found") diff --git a/nxc/modules/get-userPassword.py b/nxc/modules/get-userPassword.py index 2888941e..638beee1 100644 --- a/nxc/modules/get-userPassword.py +++ b/nxc/modules/get-userPassword.py @@ -1,6 +1,6 @@ -from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap import ldap as ldap_impacket from nxc.logger import nxc_logger +from nxc.parsers.ldap_results import parse_result_attributes class NXCModule: @@ -20,7 +20,7 @@ class NXCModule: """ def on_login(self, context, connection): - searchFilter = "(objectclass=user)" + searchFilter = "(userPassword=*)" try: context.log.debug(f"Search Filter={searchFilter}") @@ -37,27 +37,10 @@ class NXCModule: nxc_logger.debug(e) return False - answers = [] - context.log.debug(f"Total of records returned {len(resp)}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - userPassword = [] - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - elif str(attribute["type"]) == "userPassword": - userPassword = [str(i) for i in attribute["vals"]] - if sAMAccountName != "" and len(userPassword) > 0: - answers.append([sAMAccountName, userPassword]) - except Exception as e: - context.log.debug("Exception:", exc_info=True) - context.log.debug(f"Skipping item, cannot process due to error {e!s}") - if len(answers) > 0: + if resp: + resp_parsed = parse_result_attributes(resp) context.log.success("Found following users: ") - for answer in answers: - context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}") + for user in resp_parsed: + context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}") else: - context.log.fail("No userPassword Found") + context.log.fail("No unixUserPassword Found") diff --git a/nxc/modules/wcc.py b/nxc/modules/wcc.py index fd2f7d1c..5dac6022 100644 --- a/nxc/modules/wcc.py +++ b/nxc/modules/wcc.py @@ -174,7 +174,7 @@ class HostChecker: ConfigCheck("IPv4 preferred over IPv6", "Checks if IPv4 is preferred over IPv6", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Services\\Tcpip6\\Parameters", "DisabledComponents", (32, 255), in_)]]), ConfigCheck("Spooler service disabled", "Checks if the spooler service is disabled", checkers=[self.check_spooler_service]), ConfigCheck("WDigest authentication disabled", "Checks if WDigest authentication is disabled", checker_args=[[self, ("HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest", "UseLogonCredential", 0)]]), - ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]), + ConfigCheck("WSUS configuration", "Checks if WSUS configuration uses HTTPS", checkers=[self.check_wsus_running, None], checker_args=[[], [self, ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate", "WUServer", "https://", startswith), ("HKLM\\Software\\Policies\\Microsoft\\Windows\\WindowsUpdate\\AU", "UseWUServer", 0, operator.eq)]], checker_kwargs=[{}, {"options": {"lastWins": True}}]), ConfigCheck("Small LSA cache", "Checks how many logons are kept in the LSA cache", checker_args=[[self, ("HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", "CachedLogonsCount", 2, le)]]), ConfigCheck("AppLocker rules defined", "Checks if there are AppLocker rules defined", checkers=[self.check_applocker]), ConfigCheck("RDP expiration time", "Checks RDP session timeout", checker_args=[[self, ("HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt), ("HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows NT\\Terminal Services", "MaxDisconnectionTime", 0, operator.gt)]]), diff --git a/nxc/parsers/ldap_results.py b/nxc/parsers/ldap_results.py index c12be0e1..da7f90d1 100644 --- a/nxc/parsers/ldap_results.py +++ b/nxc/parsers/ldap_results.py @@ -18,6 +18,9 @@ def parse_result_attributes(ldap_response): # If we can't decode the value, we'll just return the bytes val_decoded = val.__bytes__() val_list.append(val_decoded) - attribute_map[str(attribute["type"])] = val_list if len(val_list) > 1 else val_list[0] + if len(val_list) == 1: + attribute_map[str(attribute["type"])] = val_list[0] + else: + attribute_map[str(attribute["type"])] = val_list parsed_response.append(attribute_map) return parsed_response diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 413185c8..a6a99fe0 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -1002,19 +1002,20 @@ class ldap(connection): self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}") try: resp = self.search(search_filter, attributes, 0) + resp_parsed = parse_result_attributes(resp) except LDAPFilterSyntaxError as e: self.logger.fail(f"LDAP Filter Syntax Error: {e}") return - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - self.logger.success(f"Response for object: {item['objectName']}") - for attribute in item["attributes"]: - attr = f"{attribute['type']}:" - vals = str(attribute["vals"]).replace("\n", "") - if "SetOf: " in vals: - vals = vals.replace("SetOf: ", "") - self.logger.highlight(f"{attr:<20} {vals}") + for idx, entry in enumerate(resp_parsed): + self.logger.success(f"Response for object: {resp[idx]['objectName']}") + for attribute in entry: + if isinstance(entry[attribute], list) and entry[attribute]: + # Display first item in the same line as attribute + self.logger.highlight(f"{attribute:<20} {entry[attribute].pop(0)}") + for item in entry[attribute]: + self.logger.highlight(f"{'':<20} {item}") + else: + self.logger.highlight(f"{attribute:<20} {entry[attribute]}") def find_delegation(self): def printTable(items, header): diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 04d534cc..f8a5b6c8 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -246,22 +246,23 @@ class nfs(connection): mnt_info = self.mount.mnt(share, self.auth) self.logger.debug(f"Mounted {share} - {mnt_info}") if mnt_info["status"] != 0: - self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}") - continue - file_handle = mnt_info["mountinfo"]["fhandle"] + self.logger.debug(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}") + self.logger.highlight(f"{'-':<11}{'---':<9}{'---'}/{'---':<12} {share:<30} {', '.join(network) if network else 'No network':<15}") + else: + file_handle = mnt_info["mountinfo"]["fhandle"] - info = self.nfs3.fsstat(file_handle, self.auth) - free_space = info["resok"]["fbytes"] - total_space = info["resok"]["tbytes"] - used_space = total_space - free_space + info = self.nfs3.fsstat(file_handle, self.auth) + free_space = info["resok"]["fbytes"] + total_space = info["resok"]["tbytes"] + used_space = total_space - free_space - # Autodetectting the uid needed for the share - attrs = self.nfs3.getattr(file_handle, auth=self.auth) - self.auth["uid"] = attrs["attributes"]["uid"] + # Autodetectting the uid needed for the share + attrs = self.nfs3.getattr(file_handle, auth=self.auth) + self.auth["uid"] = attrs["attributes"]["uid"] - read_perm, write_perm, exec_perm = self.get_permissions(file_handle) - self.mount.umnt(self.auth) - self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30} {', '.join(network) if network else 'No network':<15}") + read_perm, write_perm, exec_perm = self.get_permissions(file_handle) + self.mount.umnt(self.auth) + self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + "/" + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}") except Exception as e: self.logger.fail(f"Failed to list share: {share} - {e}") diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index abdba2fd..744de3ce 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -1,7 +1,7 @@ def proto_args(parser, parents): nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents) nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)") - nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)") + nfs_parser.add_argument("--nfs-timeout", type=int, default=5, help="NFS connection timeout (default: %(default)ss)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") dgroup.add_argument("--share", help="Specify a share, e.g. for --ls, --get-file, --put-file") diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index b3b4b217..d61efa31 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -13,6 +13,11 @@ from impacket.examples.secretsdump import ( LSASecrets, NTDSHashes, ) +from impacket.examples.regsecrets import ( + RemoteOperations as RegSecretsRemoteOperations, + SAMHashes as RegSecretsSAMHashes, + LSASecrets as RegSecretsLSASecrets +) from impacket.nmb import NetBIOSError, NetBIOSTimeout from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp, srvs, wkst from impacket.dcerpc.v5.rpcrt import DCERPCException @@ -1079,6 +1084,7 @@ class smb(connection): try: self.conn.createDirectory(share_name, temp_dir) write_dir = True + self.logger.debug(f"WRITE access with DIR creation on share: {share_name}") try: self.conn.deleteDirectory(share_name, temp_dir) except SessionError as e: @@ -1089,13 +1095,14 @@ class smb(connection): self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}") except SessionError as e: error = get_error_string(e) - self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}") + self.logger.debug(f"Error checking WRITE access with DIR creation on share {share_name}: {error}") try: tid = self.conn.connectTree(share_name) fid = self.conn.createFile(tid, temp_file, desiredAccess=FILE_SHARE_WRITE, shareMode=FILE_SHARE_DELETE) self.conn.closeFile(tid, fid) write_file = True + self.logger.debug(f"WRITE access with FILE creation on share: {share_name}") try: self.conn.deleteFile(share_name, temp_file) except SessionError as e: @@ -1106,7 +1113,7 @@ class smb(connection): self.logger.debug(f"Error DELETING created temp file {temp_file} on share {share_name}") except SessionError as e: error = get_error_string(e) - self.logger.debug(f"Error checking WRITE access with file on share {share_name}: {error}") + self.logger.debug(f"Error checking WRITE access with FILE creation on share {share_name}: {error}") # If we either can create a file or a directory we add the write privs to the output. Agreed on in https://github.com/Pennyw0rth/NetExec/pull/404 if write_dir or write_file: @@ -1528,9 +1535,12 @@ class smb(connection): for src, dest in self.args.get_file: self.get_file_single(src, dest) - def enable_remoteops(self): + def enable_remoteops(self, regsecret=False): try: - self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost) + if regsecret: + self.remote_ops = RegSecretsRemoteOperations(self.conn, self.kerberos, self.kdcHost) + else: + self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost) self.remote_ops.enableRegistry() if self.bootkey is None: self.bootkey = self.remote_ops.getBootKey() @@ -1540,7 +1550,7 @@ class smb(connection): @requires_admin def sam(self): try: - self.enable_remoteops() + self.enable_remoteops(regsecret=(self.args.sam == "regdump")) host_id = self.db.get_hosts(filter_term=self.host)[0][0] def add_sam_hash(sam_hash, host_id): @@ -1558,13 +1568,20 @@ class smb(connection): add_sam_hash.sam_hashes = 0 if self.remote_ops and self.bootkey: - SAM_file_name = self.remote_ops.saveSAM() - SAM = SAMHashes( - SAM_file_name, - self.bootkey, - isRemote=True, - perSecretCallback=lambda secret: add_sam_hash(secret, host_id), - ) + if self.args.sam == "regdump": + SAM = RegSecretsSAMHashes( + self.bootkey, + remoteOps=self.remote_ops, + perSecretCallback=lambda secret: add_sam_hash(secret, host_id), + ) + else: + SAM_file_name = self.remote_ops.saveSAM() + SAM = SAMHashes( + SAM_file_name, + self.bootkey, + isRemote=True, + perSecretCallback=lambda secret: add_sam_hash(secret, host_id), + ) self.logger.display("Dumping SAM hashes") SAM.dump() @@ -1575,7 +1592,9 @@ class smb(connection): self.remote_ops.finish() except Exception as e: self.logger.debug(f"Error calling remote_ops.finish(): {e}") - SAM.finish() + + if self.args.sam == "secdump": + SAM.finish() except SessionError as e: if "STATUS_ACCESS_DENIED" in e.getErrorString(): self.logger.fail('Error "STATUS_ACCESS_DENIED" while dumping SAM. This is likely due to an endpoint protection.') @@ -1792,7 +1811,7 @@ class smb(connection): @requires_admin def lsa(self): try: - self.enable_remoteops() + self.enable_remoteops(regsecret=(self.args.lsa == "regdump")) def add_lsa_secret(secret): add_lsa_secret.secrets += 1 @@ -1811,14 +1830,21 @@ class smb(connection): add_lsa_secret.secrets = 0 if self.remote_ops and self.bootkey: - SECURITYFileName = self.remote_ops.saveSECURITY() - LSA = LSASecrets( - SECURITYFileName, - self.bootkey, - self.remote_ops, - isRemote=True, - perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), - ) + if self.args.lsa == "regdump": + LSA = RegSecretsLSASecrets( + self.bootkey, + self.remote_ops, + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), + ) + else: + SECURITYFileName = self.remote_ops.saveSECURITY() + LSA = LSASecrets( + SECURITYFileName, + self.bootkey, + self.remote_ops, + isRemote=True, + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), + ) self.logger.success("Dumping LSA secrets") LSA.dumpCachedHashes() LSA.exportCached(self.output_filename) @@ -1829,7 +1855,8 @@ class smb(connection): self.remote_ops.finish() except Exception as e: self.logger.debug(f"Error calling remote_ops.finish(): {e}") - LSA.finish() + if self.args.lsa == "secdump": + LSA.finish() except SessionError as e: if "STATUS_ACCESS_DENIED" in e.getErrorString(): self.logger.fail('Error "STATUS_ACCESS_DENIED" while dumping LSA. This is likely due to an endpoint protection.') diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 6799d670..af81d321 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -25,8 +25,8 @@ def proto_args(parser, parents): self_delegate_arg.make_required = [delegate_arg] cred_gathering_group = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials") - cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") - cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") + cred_gathering_group.add_argument("--sam", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump SAM hashes from target systems") + cred_gathering_group.add_argument("--lsa", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump LSA secrets from target systems") cred_gathering_group.add_argument("--ntds", choices={"vss", "drsuapi"}, nargs="?", const="drsuapi", help="dump the NTDS.dit from target DCs using the specifed method") cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") diff --git a/poetry.lock b/poetry.lock index 97d036cd..36383111 100644 --- a/poetry.lock +++ b/poetry.lock @@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "impacket" -version = "0.13.0.dev0+20250220.93348.6315ebd5" +version = "0.13.0.dev0+20250314.172046.8b4566b1" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" @@ -890,7 +890,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket.git" reference = "HEAD" -resolved_reference = "6315ebd5388cf5bf52a809b8101f18d49c6a0ef7" +resolved_reference = "8b4566b12fc79acb520d045dbae8f13446a9d4d7" [[package]] name = "iniconfig"