From 9cbd847fd6711f3176cf7a498efc50f08e62c7b6 Mon Sep 17 00:00:00 2001 From: romanrii Date: Sun, 24 Sep 2023 12:36:14 +0000 Subject: [PATCH 1/7] Modified nxc/protocols/ftp/proto_args.py and added (--get and --put). Modified --ls Modified nxc/protocols/ftp.py and added --get and --put functionality. Modified --ls functionality --- nxc/protocols/ftp.py | 85 ++++++++++++++++++++++++++++++--- nxc/protocols/ftp/proto_args.py | 4 +- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 98dfc7d9..5c776a79 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- +import os from nxc.config import process_secret from nxc.connection import * from nxc.logger import NXCAdapter @@ -80,16 +81,42 @@ class ftp(connection): host_id = self.db.get_hosts(self.host)[0].id self.db.add_loggedin_relation(cred_id, host_id) - if username in ["anonymous", ""] and password in ["", "-"]: + if username in ["anonymous", ""]: self.logger.success(f"{username}:{process_secret(password)} {highlight('- Anonymous Login!')}") else: self.logger.success(f"{username}:{process_secret(password)}") - + if self.args.ls: - files = self.list_directory_full() - self.logger.display(f"Directory Listing") - for file in files: - self.logger.highlight(file) + # If the default directory is specified, then we will list the current directory + if self.args.ls == ".": + files = self.list_directory_full() + # If files is false, then we encountered an exception + if not files: + return False + # If there are files, then we can list the files + self.logger.display(f"Directory Listing") + for file in files: + self.logger.highlight(file) + else: + # If the default directory is not specified, then we will list the specified directory + self.logger.display(f"Directory Listing for {self.args.ls}") + # Change to the specified directory + try: + self.conn.cwd(self.args.ls) + except error_perm as error_message: + self.logger.fail(f"Failed to change directory. Response: ({error_message})") + self.conn.close() + return False + # List the files in the specified directory + files = self.list_directory_full() + for file in files: + self.logger.highlight(file) + + if self.args.get: + self.get_file(f"{self.args.get}") + + if self.args.put: + self.put_file(self.args.put[0], self.args.put[1]) if not self.args.continue_on_success: self.conn.close() @@ -101,9 +128,53 @@ class ftp(connection): # in the future we can use mlsd/nlst if we want, but this gives a full output like `ls -la` # ftplib's "dir" prints directly to stdout, and "nlst" only returns the folder name, not full details files = [] - self.conn.retrlines("LIST", callback=files.append) + try: + self.conn.retrlines("LIST", callback=files.append) + except error_perm as error_message: + self.logger.fail(f"Failed to list directory. Response: ({error_message})") + self.conn.close() + return False return files + def get_file(self, filename): + # Extract the filename from the path + downloaded_file = filename.split("/")[-1] + try: + # Check if the current connection is ASCII (ASCII does not support .size()) + if self.conn.encoding == "utf-8": + # Switch the connection to binary + self.conn.sendcmd("TYPE I") + # Check if the file exists + self.conn.size(filename) + # Attempt to download the file + self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) + except error_perm as error_message: + self.logger.fail(f"Failed to download the file. Response: ({error_message})") + self.conn.close() + return False + except FileNotFoundError: + self.logger.fail(f"Failed to download the file. Response: (No such file or directory.)") + self.conn.close() + return False + # Check if the file was downloaded + if os.path.isfile(downloaded_file): + self.logger.success(f"Downloaded: {filename}") + else: + self.logger.fail(f"Failed to download: {filename}") + + def put_file(self, local_file, remote_file): + try: + # Attempt to upload the file + self.conn.storbinary(f"STOR {remote_file}", open(local_file, "rb")) + except error_perm as error_message: + self.logger.fail(f"Failed to upload file. Response: ({error_message})") + return False + # Check if the file was uploaded + if self.conn.size(remote_file) > 0: + self.logger.success(f"Uploaded: {local_file} to {remote_file}") + else: + self.logger.fail(f"Failed to upload: {local_file} to {remote_file}") + def supported_commands(self): raw_supported_commands = self.conn.sendcmd("HELP") supported_commands = [item for sublist in (x.split() for x in raw_supported_commands.split("\n")[1:-1]) for item in sublist] diff --git a/nxc/protocols/ftp/proto_args.py b/nxc/protocols/ftp/proto_args.py index 0e9e94d4..14f4f33f 100644 --- a/nxc/protocols/ftp/proto_args.py +++ b/nxc/protocols/ftp/proto_args.py @@ -3,5 +3,7 @@ def proto_args(parser, std_parser, module_parser): ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)") cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access") - cgroup.add_argument("--ls", action="store_true", help="List files in the directory") + cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory") + cgroup.add_argument("--get", metavar="FILE", help="Download a file") + cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file") return parser From a306cfaf5c9d27420c82b276ff1413fc13150755 Mon Sep 17 00:00:00 2001 From: romanrii Date: Sun, 24 Sep 2023 12:54:48 +0000 Subject: [PATCH 2/7] Added exception handler if a local file does not exist when attempting to upload it to a ftp server using --put --- nxc/protocols/ftp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 5c776a79..821ff347 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -169,6 +169,9 @@ class ftp(connection): except error_perm as error_message: self.logger.fail(f"Failed to upload file. Response: ({error_message})") return False + except FileNotFoundError: + self.logger.fail(f"Failed to upload file. {local_file} does not exist locally.") + return False # Check if the file was uploaded if self.conn.size(remote_file) > 0: self.logger.success(f"Uploaded: {local_file} to {remote_file}") From f2d01785291697cbc57c6e75c879e5a7c68998e9 Mon Sep 17 00:00:00 2001 From: romanrii Date: Tue, 3 Oct 2023 04:09:13 +0000 Subject: [PATCH 3/7] Added function tests + test file for FTP upload/download testing --- tests/data/test_file.txt | 1 + tests/e2e_commands.txt | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 tests/data/test_file.txt diff --git a/tests/data/test_file.txt b/tests/data/test_file.txt new file mode 100644 index 00000000..62053472 --- /dev/null +++ b/tests/data/test_file.txt @@ -0,0 +1 @@ +Test file used to test FTP upload and download \ No newline at end of file diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 951a5ad8..75d8b3e6 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -205,6 +205,8 @@ netexec ssh TARGET_HOST -u USERNAME -p '' --key-file data/test_key.priv ##### FTP- Default test passwords and random key; switch these out if you want correct authentication netexec ftp TARGET_HOST -u USERNAME -p PASSWORD netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --ls +netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --put data/test_file.txt +netexec ftp TARGET_HOST -u USERNAME -p PASSWORD --get test_file.txt netexec ftp TARGET_HOST -u data/test_users.txt -p test_passwords.txt --no-bruteforce netexec ftp TARGET_HOST -u data/test_users.txt -p test_passwords.txt --no-bruteforce --continue-on-success netexec ftp TARGET_HOST -u data/test_users.txt -p test_passwords.txt \ No newline at end of file From f6fed70894a5d0ea1119802cc544b11ae50d443a Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 12 Oct 2023 16:28:36 -0400 Subject: [PATCH 4/7] Revert "[nanodump] fix error with temporary path" --- nxc/modules/nanodump.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nxc/modules/nanodump.py b/nxc/modules/nanodump.py index 158b96cb..4b3d7461 100644 --- a/nxc/modules/nanodump.py +++ b/nxc/modules/nanodump.py @@ -4,10 +4,9 @@ # author of the module : github.com/mpgn # nanodump: https://github.com/helpsystems/nanodump -import os import base64 import sys -from pypykatz.pypykatz import pypykatz +import pypykatz import tempfile from datetime import datetime from nxc.helpers.bloodhound import add_user_bh @@ -60,6 +59,7 @@ class NXCModule: self.useembeded = False else: self.nano_path = f"{tempfile.gettempdir()}" + self.dir_result = self.nano_path if "NANO_EXE_NAME" in module_options: @@ -76,7 +76,7 @@ class NXCModule: self.connection = connection self.context = context if self.useembeded: - with open(os.path.join(self.nano_path, self.nano), "wb") as nano: + with open(self.nano_path + self.nano, "wb") as nano: if self.connection.os_arch == 32 and self.context.protocol == "smb": self.context.log.display("32-bit Windows detected.") nano.write(self.nano_embedded32) @@ -90,14 +90,14 @@ class NXCModule: sys.exit(1) if self.context.protocol == "smb": - with open(os.path.join(self.nano_path, self.nano), "rb") as nano: + with open(self.nano_path + self.nano, "rb") as nano: try: self.connection.conn.putFile(self.share, self.tmp_share + self.nano, nano.read) self.context.log.success(f"Created file {self.nano} on the \\\\{self.share}{self.tmp_share}") except Exception as e: self.context.log.fail(f"Error writing file to share {self.share}: {e}") else: - with open(os.path.join(self.nano_path, self.nano), "rb") as nano: + with open(self.nano_path + self.nano, "rb") as nano: try: self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}") exec_method = MSSQLEXEC(self.connection.conn) @@ -154,7 +154,7 @@ class NXCModule: if dump: self.context.log.display(f"Copying {nano_log_name} to host") - filename = os.path.join(self.dir_result,f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log") + filename = f"{self.dir_result}{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log" if self.context.protocol == "smb": with open(filename, "wb+") as dump_file: try: From dfafcb2975e364c73f19616c2c7728f30f9c5836 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 15 Oct 2023 10:59:22 -0400 Subject: [PATCH 5/7] Add module sorting when listing modules by privileges needed for execution --- nxc/loaders/moduleloader.py | 1 + nxc/netexec.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nxc/loaders/moduleloader.py b/nxc/loaders/moduleloader.py index 9337e9d6..0ba352f7 100755 --- a/nxc/loaders/moduleloader.py +++ b/nxc/loaders/moduleloader.py @@ -114,6 +114,7 @@ class ModuleLoader: "supported_protocols": module_spec.supported_protocols, "opsec_safe": module_spec.opsec_safe, "multiple_hosts": module_spec.multiple_hosts, + "requires_admin": True if hasattr(module_spec, 'on_admin_login') and callable(module_spec.on_admin_login) else False, } } if self.module_is_sane(module_spec, module_path): diff --git a/nxc/netexec.py b/nxc/netexec.py index ea5514f0..2b22734d 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -47,6 +47,7 @@ except: print("Incompatible python version, try with another python version or another binary 3.8 / 3.9 / 3.10 / 3.11 that match your python version (python -V)") exit(1) + def create_db_engine(db_path): db_engine = sqlalchemy.create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True) return db_engine @@ -179,8 +180,13 @@ def main(): modules = loader.list_modules() if args.list_modules: + nxc_logger.highlight("LOW PRIVILEGE MODULES") for name, props in sorted(modules.items()): - if args.protocol in props["supported_protocols"]: + if args.protocol in props["supported_protocols"] and not props["requires_admin"]: + nxc_logger.display(f"{name:<25} {props['description']}") + nxc_logger.highlight("\nHIGH PRIVILEGE MODULES (requires admin privs)") + for name, props in sorted(modules.items()): + if args.protocol in props["supported_protocols"] and props["requires_admin"]: nxc_logger.display(f"{name:<25} {props['description']}") exit(0) elif args.module and args.show_module_options: From 901b8dee80fc1109414c688f491f5ad4fdec9049 Mon Sep 17 00:00:00 2001 From: Roman Rivas II <74742067+RomanRII@users.noreply.github.com> Date: Sun, 15 Oct 2023 08:12:21 -0700 Subject: [PATCH 6/7] Update proto_args.py Signed-off-by: Roman Rivas II <74742067+RomanRII@users.noreply.github.com> --- nxc/protocols/ftp/proto_args.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ftp/proto_args.py b/nxc/protocols/ftp/proto_args.py index 14f4f33f..adf1e69a 100644 --- a/nxc/protocols/ftp/proto_args.py +++ b/nxc/protocols/ftp/proto_args.py @@ -3,7 +3,7 @@ def proto_args(parser, std_parser, module_parser): ftp_parser.add_argument("--port", type=int, default=21, help="FTP port (default: 21)") cgroup = ftp_parser.add_argument_group("FTP Access", "Options for enumerating your access") - cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory") - cgroup.add_argument("--get", metavar="FILE", help="Download a file") - cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file") + cgroup.add_argument("--ls", metavar="DIRECTORY", nargs="?", const=".", help="List files in the directory, ex: --ls or --ls Directory") + cgroup.add_argument("--get", metavar="FILE", help="Download a file, ex: --get fileName.txt") + cgroup.add_argument("--put", metavar=("LOCAL_FILE", "REMOTE_FILE"), nargs=2, help="Upload a file, ex: --put inputFileName.txt outputFileName.txt") return parser From f26b676c8ee94992006352737663352301ed201b Mon Sep 17 00:00:00 2001 From: Roman Rivas II <74742067+RomanRII@users.noreply.github.com> Date: Sun, 15 Oct 2023 08:14:15 -0700 Subject: [PATCH 7/7] Update ftp.py Signed-off-by: Roman Rivas II <74742067+RomanRII@users.noreply.github.com> --- nxc/protocols/ftp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 821ff347..c52b1ae7 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -85,7 +85,7 @@ class ftp(connection): self.logger.success(f"{username}:{process_secret(password)} {highlight('- Anonymous Login!')}") else: self.logger.success(f"{username}:{process_secret(password)}") - + if self.args.ls: # If the default directory is specified, then we will list the current directory if self.args.ls == ".":