From 8c37f34aced5294457f457ed3c3c6c0d1f22acd7 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:50:50 +0300 Subject: [PATCH 1/9] Create dump-computers.py ldap module --- nxc/modules/dump-computers.py | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 nxc/modules/dump-computers.py diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py new file mode 100644 index 00000000..5254784b --- /dev/null +++ b/nxc/modules/dump-computers.py @@ -0,0 +1,87 @@ +from nxc.logger import nxc_logger +from impacket.ldap.ldap import LDAPSearchError +from impacket.ldap.ldapasn1 import SearchResultEntry + +class NXCModule: + + name = "dump-computers" + description = "Dumps all computers in the domain" + supported_protocols = ["ldap"] + opsec_safe = True + multiple_hosts = False + + def options(self, context, module_options): + """ + dump-computers: Specify dump-computers to call the module + Usage: + + >prints fqdn and version + nxc ldap $DC-IP -u Username -p Password -M dump-computers + + >prints only netbios name + nxc ldap $DC-IP -u Username -p Password -M dump-computers -o NETBIOS=True + + >prints fqdn and version, output to file + nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= + + >prints only netbios name, output to file + nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= -o NETBIOS=True + + """ + self.output_file = None + self.netbios_only = False + + if "OUTPUT" in module_options: + self.output_file = module_options["OUTPUT"] + if "NETBIOS" in module_options and module_options["NETBIOS"].lower() == "true": + self.netbios_only = True + + def on_login(self, context, connection): + search_filter = "(objectCategory=computer)" + + try: + context.log.debug(f"Search Filter={search_filter}") + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0) + except LDAPSearchError as e: + if e.getErrorString().find("sizeLimitExceeded") >= 0: + context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") + resp = e.getAnswers() + else: + nxc_logger.debug(e) + return False + + answers = [] + context.log.debug(f"Total no. of records returned: {len(resp)}") + for item in resp: + if isinstance(item, SearchResultEntry) is not True: + continue + dns_host_name = "" + operating_system = "" + try: + for attribute in item["attributes"]: + if str(attribute["type"]) == "dNSHostName": + dns_host_name = str(attribute["vals"][0]) + elif str(attribute["type"]) == "operatingSystem": + operating_system = attribute["vals"][0] + if dns_host_name: + netbios_name = dns_host_name.split(".")[0] + answer = netbios_name if self.netbios_only else f"{dns_host_name} ({operating_system})" + answers.append(answer) + except Exception as e: + context.log.debug("Exception:", exc_info=True) + context.log.debug(f"Skipping item, cannot process due to error {e}") + + if len(answers) > 0: + context.log.success("Found the following computers: ") + for answer in answers: + context.log.highlight(answer) + + if self.output_file: + try: + with open(self.output_file, "w") as f: + f.write("\n".join(answers) + "\n") + context.log.success(f"Results saved to {self.output_file}") + except Exception as e: + context.log.error(f"Failed to write to file {self.output_file}: {e}") + else: + context.log.success("No computers found in the domain.") From 25ac2e2a2a8de16df6958078925c755c79486614 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Fri, 7 Feb 2025 16:56:13 +0300 Subject: [PATCH 2/9] Update dump-computers.py added fqdn only --- nxc/modules/dump-computers.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index 5254784b..2ee2ba1e 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -1,6 +1,8 @@ +import socket from nxc.logger import nxc_logger from impacket.ldap.ldap import LDAPSearchError from impacket.ldap.ldapasn1 import SearchResultEntry +import sys class NXCModule: @@ -13,28 +15,34 @@ class NXCModule: def options(self, context, module_options): """ dump-computers: Specify dump-computers to call the module - Usage: - + Usage: >prints fqdn and version nxc ldap $DC-IP -u Username -p Password -M dump-computers >prints only netbios name nxc ldap $DC-IP -u Username -p Password -M dump-computers -o NETBIOS=True + >prints only fqdn + nxc ldap $DC-IP -u Username -p Password -M dump-computers -o FQDN=True + >prints fqdn and version, output to file nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= >prints only netbios name, output to file nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= -o NETBIOS=True + nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= -o FQDN=True """ self.output_file = None self.netbios_only = False + self.fqdn_only = False if "OUTPUT" in module_options: self.output_file = module_options["OUTPUT"] if "NETBIOS" in module_options and module_options["NETBIOS"].lower() == "true": self.netbios_only = True + if "FQDN" in module_options and module_options["FQDN"].lower() == "true": + self.fqdn_only = True def on_login(self, context, connection): search_filter = "(objectCategory=computer)" @@ -65,7 +73,12 @@ class NXCModule: operating_system = attribute["vals"][0] if dns_host_name: netbios_name = dns_host_name.split(".")[0] - answer = netbios_name if self.netbios_only else f"{dns_host_name} ({operating_system})" + if self.netbios_only: + answer = netbios_name + elif self.fqdn_only: + answer = dns_host_name + else: + answer = f"{dns_host_name} ({operating_system})" answers.append(answer) except Exception as e: context.log.debug("Exception:", exc_info=True) From fa3eba48dfa113362b51f6a0a3adc6848d3fe779 Mon Sep 17 00:00:00 2001 From: emre <73831924+crosscutsaw@users.noreply.github.com> Date: Sat, 8 Feb 2025 14:34:04 +0300 Subject: [PATCH 3/9] Update dump-computers.py made some adjustments for better experience --- nxc/modules/dump-computers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index 2ee2ba1e..a08b106c 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -17,20 +17,20 @@ class NXCModule: dump-computers: Specify dump-computers to call the module Usage: >prints fqdn and version - nxc ldap $DC-IP -u Username -p Password -M dump-computers + netexec ldap $DC-IP -u $username -p $password -M dump-computers >prints only netbios name - nxc ldap $DC-IP -u Username -p Password -M dump-computers -o NETBIOS=True + netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios >prints only fqdn - nxc ldap $DC-IP -u Username -p Password -M dump-computers -o FQDN=True + netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn >prints fqdn and version, output to file - nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= + netexec ldap $DC-IP -u $username -p $password -M dump-computers -o OUTPUT= >prints only netbios name, output to file - nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= -o NETBIOS=True - nxc ldap $DC-IP -u Username -p Password -M dump-computers -o OUTPUT= -o FQDN=True + netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT= + netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn OUTPUT= """ self.output_file = None @@ -39,9 +39,9 @@ class NXCModule: if "OUTPUT" in module_options: self.output_file = module_options["OUTPUT"] - if "NETBIOS" in module_options and module_options["NETBIOS"].lower() == "true": + if "TYPE" in module_options and module_options["TYPE"].lower() == "netbios": self.netbios_only = True - if "FQDN" in module_options and module_options["FQDN"].lower() == "true": + if "TYPE" in module_options and module_options["TYPE"].lower() == "fqdn": self.fqdn_only = True def on_login(self, context, connection): From 16a65d4c261e6b07e2a59bff0d3f3584adaa741c Mon Sep 17 00:00:00 2001 From: crosscutsaw <73831924+crosscutsaw@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:59:07 +0300 Subject: [PATCH 4/9] Update dump-computers.py Signed-off-by: crosscutsaw <73831924+crosscutsaw@users.noreply.github.com> --- nxc/modules/dump-computers.py | 86 ++++++++++++++++------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index a08b106c..ed174a72 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -1,11 +1,7 @@ -import socket -from nxc.logger import nxc_logger -from impacket.ldap.ldap import LDAPSearchError +from nxc.parsers.ldap_results import parse_result_attributes from impacket.ldap.ldapasn1 import SearchResultEntry -import sys class NXCModule: - name = "dump-computers" description = "Dumps all computers in the domain" supported_protocols = ["ldap"] @@ -16,61 +12,56 @@ class NXCModule: """ dump-computers: Specify dump-computers to call the module Usage: - >prints fqdn and version + > prints fqdn and machine version netexec ldap $DC-IP -u $username -p $password -M dump-computers - - >prints only netbios name + + > prints only netbios name (no machine version) netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios - - >prints only fqdn + + > prints only fqdn (no machine version) netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn - - >prints fqdn and version, output to file + + > prints fqdn and machine version, output to file netexec ldap $DC-IP -u $username -p $password -M dump-computers -o OUTPUT= - - >prints only netbios name, output to file + + > prints netbios or fqdn (no machine version), output to file netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT= netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn OUTPUT= - """ self.output_file = None self.netbios_only = False self.fqdn_only = False - + if "OUTPUT" in module_options: self.output_file = module_options["OUTPUT"] - if "TYPE" in module_options and module_options["TYPE"].lower() == "netbios": - self.netbios_only = True - if "TYPE" in module_options and module_options["TYPE"].lower() == "fqdn": - self.fqdn_only = True + if "TYPE" in module_options: + t = module_options["TYPE"].lower() + if t == "netbios": + self.netbios_only = True + elif t == "fqdn": + self.fqdn_only = True def on_login(self, context, connection): search_filter = "(objectCategory=computer)" - - try: - context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0) - except LDAPSearchError as e: - if e.getErrorString().find("sizeLimitExceeded") >= 0: - context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") - resp = e.getAnswers() - else: - nxc_logger.debug(e) - return False + context.log.debug(f"Search Filter = {search_filter}") + + entries = connection.search( + searchFilter=search_filter, + attributes=["dNSHostName", "operatingSystem"] + ) answers = [] - context.log.debug(f"Total no. of records returned: {len(resp)}") - for item in resp: - if isinstance(item, SearchResultEntry) is not True: + context.log.debug(f"Total number of records returned: {len(entries)}") + + for item in entries: + if not isinstance(item, SearchResultEntry): continue - dns_host_name = "" - operating_system = "" + try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "dNSHostName": - dns_host_name = str(attribute["vals"][0]) - elif str(attribute["type"]) == "operatingSystem": - operating_system = attribute["vals"][0] + parsed = parse_result_attributes([item])[0] + dns_host_name = parsed.get("dNSHostName", "") + operating_system = parsed.get("operatingSystem", "") + if dns_host_name: netbios_name = dns_host_name.split(".")[0] if self.netbios_only: @@ -80,15 +71,14 @@ class NXCModule: else: answer = f"{dns_host_name} ({operating_system})" answers.append(answer) - except Exception as e: - context.log.debug("Exception:", exc_info=True) - context.log.debug(f"Skipping item, cannot process due to error {e}") - - if len(answers) > 0: - context.log.success("Found the following computers: ") + except Exception: + context.log.debug("Failed to parse entry", exc_info=True) + + if answers: + context.log.success("Found the following computers:") for answer in answers: context.log.highlight(answer) - + if self.output_file: try: with open(self.output_file, "w") as f: From 8b3bc6b89aa575b9d7432d4abed71e314ad52f23 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 14 Jul 2025 07:40:57 -0400 Subject: [PATCH 5/9] Fix description --- nxc/modules/dump-computers.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index ed174a72..a1978a86 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -1,6 +1,7 @@ from nxc.parsers.ldap_results import parse_result_attributes from impacket.ldap.ldapasn1 import SearchResultEntry + class NXCModule: name = "dump-computers" description = "Dumps all computers in the domain" @@ -10,23 +11,15 @@ class NXCModule: def options(self, context, module_options): """ - dump-computers: Specify dump-computers to call the module - Usage: - > prints fqdn and machine version + TYPE Only dump NETBIOS or FQDN instead of 'FQDN (OS Version)' + OUTPUT Output to file in addition to printing to console + + Examples + -------- netexec ldap $DC-IP -u $username -p $password -M dump-computers - - > prints only netbios name (no machine version) netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios - - > prints only fqdn (no machine version) netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn - - > prints fqdn and machine version, output to file - netexec ldap $DC-IP -u $username -p $password -M dump-computers -o OUTPUT= - - > prints netbios or fqdn (no machine version), output to file netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=netbios OUTPUT= - netexec ldap $DC-IP -u $username -p $password -M dump-computers -o TYPE=fqdn OUTPUT= """ self.output_file = None self.netbios_only = False @@ -35,10 +28,9 @@ class NXCModule: if "OUTPUT" in module_options: self.output_file = module_options["OUTPUT"] if "TYPE" in module_options: - t = module_options["TYPE"].lower() - if t == "netbios": + if module_options["TYPE"].lower() == "netbios": self.netbios_only = True - elif t == "fqdn": + elif module_options["TYPE"].lower() == "fqdn": self.fqdn_only = True def on_login(self, context, connection): From 234b41efa595f48722cdfaf7d2ea85527e3ce010 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 14 Jul 2025 07:45:02 -0400 Subject: [PATCH 6/9] Simplify logic --- nxc/modules/dump-computers.py | 40 +++++++++++++---------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index a1978a86..d1300b4c 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -34,37 +34,27 @@ class NXCModule: self.fqdn_only = True def on_login(self, context, connection): - search_filter = "(objectCategory=computer)" - context.log.debug(f"Search Filter = {search_filter}") - - entries = connection.search( - searchFilter=search_filter, + resp = connection.search( + searchFilter="(objectCategory=computer)", attributes=["dNSHostName", "operatingSystem"] ) + resp_parsed = parse_result_attributes(resp) answers = [] - context.log.debug(f"Total number of records returned: {len(entries)}") + context.log.debug(f"Total number of records returned: {len(resp_parsed)}") - for item in entries: - if not isinstance(item, SearchResultEntry): - continue + for item in resp_parsed: + dns_host_name = item["dNSHostName"] + operating_system = item.get("operatingSystem", "Unknown OS") - try: - parsed = parse_result_attributes([item])[0] - dns_host_name = parsed.get("dNSHostName", "") - operating_system = parsed.get("operatingSystem", "") - - if dns_host_name: - netbios_name = dns_host_name.split(".")[0] - if self.netbios_only: - answer = netbios_name - elif self.fqdn_only: - answer = dns_host_name - else: - answer = f"{dns_host_name} ({operating_system})" - answers.append(answer) - except Exception: - context.log.debug("Failed to parse entry", exc_info=True) + if self.netbios_only: + netbios_name = dns_host_name.split(".")[0] + answer = netbios_name + elif self.fqdn_only: + answer = dns_host_name + else: + answer = f"{dns_host_name} ({operating_system})" + answers.append(answer) if answers: context.log.success("Found the following computers:") From 239cc805164dc9291f9784aa558192f5fa560bd6 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 14 Jul 2025 07:48:28 -0400 Subject: [PATCH 7/9] As there have to be computers in a domain (at least the DC) don't check if there are anwsers --- nxc/modules/dump-computers.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index d1300b4c..a29f95df 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -56,17 +56,14 @@ class NXCModule: answer = f"{dns_host_name} ({operating_system})" answers.append(answer) - if answers: - context.log.success("Found the following computers:") - for answer in answers: - context.log.highlight(answer) + context.log.success("Found the following computers:") + for answer in answers: + context.log.highlight(answer) - if self.output_file: - try: - with open(self.output_file, "w") as f: - f.write("\n".join(answers) + "\n") - context.log.success(f"Results saved to {self.output_file}") - except Exception as e: - context.log.error(f"Failed to write to file {self.output_file}: {e}") - else: - context.log.success("No computers found in the domain.") + if self.output_file: + try: + with open(self.output_file, "w") as f: + f.write("\n".join(answers) + "\n") + context.log.success(f"Results saved to {self.output_file}") + except Exception as e: + context.log.error(f"Failed to write to file {self.output_file}: {e}") From 0eb65ed3478f35dbf5d724d7aae4e1a02e4e0fb2 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 14 Jul 2025 07:49:02 -0400 Subject: [PATCH 8/9] Add e2e test --- tests/e2e_commands.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 7d03207e..3bd17d09 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -210,6 +210,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M subnets netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M user-desc netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M whoami netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pso +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dump-computers ##### WINRM netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig From e259128eadbb6407172ec4f6095310f145999ad3 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Mon, 14 Jul 2025 07:50:46 -0400 Subject: [PATCH 9/9] Linting --- nxc/modules/dump-computers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/modules/dump-computers.py b/nxc/modules/dump-computers.py index a29f95df..cf11e858 100644 --- a/nxc/modules/dump-computers.py +++ b/nxc/modules/dump-computers.py @@ -1,5 +1,4 @@ from nxc.parsers.ldap_results import parse_result_attributes -from impacket.ldap.ldapasn1 import SearchResultEntry class NXCModule: