From ad295ea7ac71da9e6a91f32d31dde35d44851f43 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Mon, 18 Sep 2023 16:26:23 +0800 Subject: [PATCH 01/28] [ssh] improvement Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 296 +++++++++++++++++++++----------- nxc/protocols/ssh/proto_args.py | 7 +- poetry.lock | 11 +- pyproject.toml | 2 +- 4 files changed, 211 insertions(+), 105 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index d602fdf5..b43b1425 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -1,11 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import logging +import paramiko, re, uuid, logging, time, sys from io import StringIO - -import paramiko - from nxc.config import process_secret from nxc.connection import * from nxc.logger import NXCAdapter @@ -15,13 +12,28 @@ from paramiko.ssh_exception import ( SSHException, ) - class ssh(connection): def __init__(self, args, db, host): self.protocol = "SSH" - self.remote_version = None - self.server_os = None + self.remote_version = "" + self.server_os_platform = "Linux" + self.user_principal = "root" super().__init__(args, db, host) + + def proto_flow(self): + self.proto_logger() + if self.create_conn_obj(): + self.enum_host_info() + self.print_host_info() + if not self.remote_version: + self.conn.close() + return + if self.login(): + if hasattr(self.args, "module") and self.args.module: + self.call_modules() + else: + self.call_cmd_args() + self.conn.close() def proto_logger(self): self.logger = NXCAdapter( @@ -32,28 +44,23 @@ class ssh(connection): "hostname": self.hostname, } ) - logging.getLogger("paramiko").setLevel(logging.WARNING) def print_host_info(self): - self.logger.display(self.remote_version) + self.logger.display(self.remote_version if self.remote_version else "Unknown SSH version, skipping...") return True def enum_host_info(self): self.remote_version = self.conn._transport.remote_version - self.logger.debug(f"Remote version: {self.remote_version}") - self.server_os = "" - if self.args.remote_enum: - stdin, stdout, stderr = self.conn.exec_command("uname -r") - self.server_os = stdout.read().decode("utf-8") - self.logger.debug(f"OS retrieved: {self.server_os}") - self.db.add_host(self.host, self.args.port, self.remote_version, os=self.server_os) + self.logger.debug(f'Remote version: {self.remote_version if self.remote_version else "Unknown SSH Version"}') + self.db.add_host(self.host, self.args.port, self.remote_version if self.remote_version else "Unknown SSH Version") def create_conn_obj(self): + logging.getLogger("paramiko").disabled = True + logging.getLogger("paramiko.transport").disabled = True self.conn = paramiko.SSHClient() self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - self.conn.connect(self.host, port=self.args.port) + self.conn.connect(self.host, port=self.args.port, timeout=self.args.ssh_timeout) except AuthenticationException: return True except SSHException: @@ -63,46 +70,137 @@ class ssh(connection): except socket.error: return False - def client_close(self): - self.conn.close() - def check_if_admin(self): + self.admin_privs = False + + if self.args.sudo_check: + self.check_if_admin_sudo() + return + # we could add in another method to check by piping in the password to sudo # but that might be too much of an opsec concern - maybe add in a flag to do more checks? - stdin, stdout, stderr = self.conn.exec_command("id") - if stdout.read().decode("utf-8").find("uid=0(root)") != -1: - self.logger.info(f"Determined user is root via `id` command") - self.admin_privs = True - return True - stdin, stdout, stderr = self.conn.exec_command("sudo -ln | grep 'NOPASSWD: ALL'") - if stdout.read().decode("utf-8").find("NOPASSWD: ALL") != -1: - self.logger.info(f"Determined user is root via `sudo -ln` command") - self.admin_privs = True - return True + self.logger.info(f"Determined user is root via `id && sudo -ln` command") + stdin, stdout, stderr = self.conn.exec_command("id && sudo -ln 2>&1") + stdout = stdout.read().decode("utf-8", errors="ignore") + admin_Flag = { + "(root)": [True, None], + "NOPASSWD: ALL": [True, None], + "(ALL : ALL) ALL": [True, None], + "(sudo)": [False, f'Current user: "{self.username}" was in "sudo" group, please try "--sudo-check" to check if user can run sudo shell'], + } + for keyword in admin_Flag.keys(): + match = re.findall(re.escape(keyword), stdout) + if match: + self.logger.info(f'User: "{self.username}" matched keyword: {match[0]}') + self.admin_privs = admin_Flag[match[0]][0] + if self.admin_privs: + #break + break + else: + # Continue find admin flag + tips = admin_Flag[match[0]][1] + continue + if not self.admin_privs and "tips" in locals(): + self.logger.display(tips) + return + + def check_if_admin_sudo(self): + if not self.password: + self.logger.error("Check admin with sudo not support private key.") + return + + if self.args.sudo_check_method: + method = self.args.sudo_check_method + self.logger.info(f"Doing sudo check with method: {method}") + + if method == "sudo-stdin": + stdin, stdout, stderr = self.conn.exec_command("sudo --help") + stdout = stdout.read().decode("utf-8", errors="ignore") + if "stdin" in stdout: + shadow_Backup = f'/tmp/{uuid.uuid4()}' + # sudo support stdin password + stdin, stdout, stderr = self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_Backup} >/dev/null 2>&1 &") + stdin, stdout, stderr = self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_Backup} >/dev/null 2>&1 &") + tries = 1 + while True: + self.logger.info(f"Checking {shadow_Backup} if it existed") + stdin, stdout, stderr = self.conn.exec_command(f'ls {shadow_Backup}') + if tries >= self.args.get_output_tries: + self.logger.info(f'{shadow_Backup} not existed, maybe the pipe has been hanged over, please increase the number of tries with the option "--get-output-tries" or change other method with "--sudo-check-method". If it\'s still failing maybe sudo shell is not working with current user') + break + if stderr.read().decode('utf-8'): + time.sleep(2) + tries +=1 + else: + self.logger.info(f"{shadow_Backup} existed") + self.admin_privs = True + break + self.logger.info(f"Remove up temporary files") + stdin, stdout, stderr = self.conn.exec_command(f"rm -rf {shadow_Backup}") + else: + self.logger.error("Command: 'sudo' not support stdin mode, running command with 'sudo' failed") + return + else: + stdin, stdout, stderr = self.conn.exec_command("mkfifo --help") + stdout = stdout.read().decode("utf-8", errors="ignore") + # check if user can execute mkfifo + if "Create named pipes" in stdout: + self.logger.info("Command: 'mkfifo' available") + pipe_stdin = f'/tmp/systemd-{uuid.uuid4()}' + pipe_stdout = f'/tmp/systemd-{uuid.uuid4()}' + shadow_Backup = f'/tmp/{uuid.uuid4()}' + stdin, stdout, stderr = self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") + # 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat + stdin, stdout, stderr = self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") + stdin, stdout, stderr = self.conn.exec_command(f"echo 'sudo -s' > {pipe_stdin} && echo '{self.password}' > {pipe_stdin}") + # Sometime the pipe will hanging(only happen with paramiko) + # Can't get "whoami" or "id" result in pipe_stdout, maybe something wrong using pipe with paramiko + # But one thing I can confirm, is the command was executed even can't get result from pipe_stdout + tries = 1 + self.logger.info(f"Copy /etc/shadow to {shadow_Backup} if pass the sudo auth") + while True: + self.logger.info(f"Checking {shadow_Backup} if it existed") + stdin, stdout, stderr = self.conn.exec_command(f'ls {shadow_Backup}') + if tries >= self.args.get_output_tries: + self.logger.info(f'{shadow_Backup} not existed, maybe the pipe has been hanged over, please increase the number of tries with the option "--get-output-tries" or change other method with "--sudo-check-method". If it\'s still failing maybe sudo shell is not working with current user') + break + + if stderr.read().decode('utf-8'): + time.sleep(2) + stdin, stdout, stderr = self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_Backup} && chmod 777 {shadow_Backup}' > {pipe_stdin}") + tries += 1 + else: + self.logger.info(f"{shadow_Backup} existed") + self.admin_privs = True + break + self.logger.info(f"Remove up temporary files") + stdin, stdout, stderr = self.conn.exec_command(f"rm -rf {shadow_Backup} {pipe_stdin} {pipe_stdout}") + else: + self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed") + return def plaintext_login(self, username, password, private_key=None): + self.username = username + self.password = password + pkey = "" + stdout = None + stderr = None + cred_id = self.db.add_credential("plaintext", username, password) try: if self.args.key_file or private_key: + self.logger.debug(f"Logging in with key") if private_key: pkey = paramiko.RSAKey.from_private_key(StringIO(private_key)) else: pkey = paramiko.RSAKey.from_private_key_file(self.args.key_file) - self.logger.debug(f"Logging in with key") - self.conn.connect( - self.host, - port=self.args.port, - username=username, - passphrase=password if password != "" else None, - pkey=pkey, - look_for_keys=False, - allow_agent=False, - ) + password = f"(keydata: {private_key})" if private_key else f"(keyfile: {self.args.key_file})" + self.conn._transport.auth_publickey(username, pkey) if private_key: cred_id = self.db.add_credential( "key", username, - password if password != "" else "", + "", key=private_key, ) else: @@ -111,76 +209,82 @@ class ssh(connection): cred_id = self.db.add_credential( "key", username, - password if password != "" else "", + "", key=key_data, ) else: - self.logger.debug(f"Logging in with password") - self.conn.connect( - self.host, - port=self.args.port, - username=username, - password=password, - look_for_keys=False, - allow_agent=False, - ) - cred_id = self.db.add_credential("plaintext", username, password) + self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}") + self.conn._transport.auth_password(username, password, fallback=True) + # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey + stdin, stdout, stderr = self.conn.exec_command("id") + stdout = stdout.read().decode("utf-8", errors="ignore") + except Exception as e: + self.logger.fail(f"{username}:{process_secret(password) if not pkey else password} {e}") + self.conn.close() + return False + else: shell_access = False host_id = self.db.get_hosts(self.host)[0].id - if self.check_if_admin(): - shell_access = True - self.logger.debug(f"User {username} logged in successfully and is root!") - if self.args.key_file: - self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id) + if not stdout: + stdin, stdout, stderr = self.conn.exec_command("whoami /priv") + stdout = stdout.read().decode("utf-8", errors="ignore") + self.server_os_platform = "Windows" + self.user_principal = "admin" + if "SeDebugPrivilege" in stdout: + self.admin_privs = True + elif "SeUndockPrivilege" in stdout: + self.admin_privs = True + self.user_principal = "admin (UAC)" else: - self.db.add_admin_user( - "plaintext", - username, - password, - host_id=host_id, - cred_id=cred_id, - ) + # non admin (low priv) + self.user_principal = "admin (low priv)" + + if not stdout: + self.logger.debug(f"User: {self.username} can't get a basic shell") + self.server_os_platform = "Network Devices" + shell_access = False else: - stdin, stdout, stderr = self.conn.exec_command("id") - output = stdout.read().decode("utf-8") - if not output: - self.logger.debug(f"User cannot get a shell") - shell_access = False - else: - shell_access = True + shell_access = True + + if shell_access and self.server_os_platform == "Linux": + self.check_if_admin() + if self.admin_privs: + self.logger.debug(f"User {username} logged in successfully and is root!") + if self.args.key_file: + self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id) + else: + self.db.add_admin_user( + "plaintext", + username, + password, + host_id=host_id, + cred_id=cred_id, + ) self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access) if self.args.key_file: - password = f"{password} (keyfile: {self.args.key_file})" + password = f"(keyfile: {self.args.key_file})" - display_shell_access = f" - shell access!" if shell_access else "" - - self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()}{highlight(display_shell_access)}") + display_shell_access = f'Shell access! {f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})"}' if shell_access else "" + self.logger.success(f"{username}:{process_secret(password)} {highlight(display_shell_access)} {highlight(self.server_os_platform)} {self.mark_pwned()}") + return True - except ( - AuthenticationException, - NoValidConnectionsError, - ConnectionResetError, - ) as e: - self.logger.fail(f"{username}:{process_secret(password)} {e}") - self.client_close() - return False - except Exception as e: - self.logger.exception(e) - self.client_close() - return False - - def execute(self, payload=None, output=False): + + def execute(self, payload=None, get_output=False): + if not payload and self.args.execute: + payload = self.args.execute + if not self.args.no_output: + get_output = True try: - command = payload if payload is not None else self.args.execute - stdin, stdout, stderr = self.conn.exec_command(command) + stdin, stdout, stderr = self.conn.exec_command(f"{payload} 2>&1") except AttributeError: return "" - if output: + if get_output: self.logger.success("Executed command") - for line in stdout: - self.logger.highlight(line.strip()) - return stdout + if get_output: + for line in stdout: + self.logger.highlight(line.strip()) + return stdout diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index 7d5473d1..6c8936f6 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -2,10 +2,13 @@ def proto_args(parser, std_parser, module_parser): ssh_parser = parser.add_parser("ssh", help="own stuff using SSH", parents=[std_parser, module_parser]) ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase.") ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") + ssh_parser.add_argument("--ssh-timeout", help="SSH connection timeout, default is %(default)s secondes", type=int, default=15) + ssh_parser.add_argument("--sudo-check", action="store_true", help="Check user privilege with sudo") + ssh_parser.add_argument("--sudo-check-method", choices={"sudo-stdin", "mkfifo"}, default="sudo-stdin", help="method to do with sudo check, default is '%(default)s (mkfifo is non-stable, probably you need to execute once again if it failed)'") + ssh_parser.add_argument("--get-output-tries", help="Number of times with sudo command tries to get results, default is %(default)s", type=int, default=5) cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command") - cgroup.add_argument("--remote-enum", action="store_true", help="executes remote commands for enumeration") - return parser + return parser \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 86113290..df3ef56b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1546,14 +1546,13 @@ files = [ [[package]] name = "paramiko" -version = "2.12.0" +version = "3.3.1" description = "SSH2 protocol library" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "paramiko-2.12.0-py2.py3-none-any.whl", hash = "sha256:b2df1a6325f6996ef55a8789d0462f5b502ea83b3c990cbb5bbe57345c6812c4"}, - {file = "paramiko-2.12.0.tar.gz", hash = "sha256:376885c05c5d6aa6e1f4608aac2a6b5b0548b1add40274477324605903d9cd49"}, + {file = "paramiko-3.3.1-py3-none-any.whl", hash = "sha256:b7bc5340a43de4287bbe22fe6de728aa2c22468b2a849615498dd944c2f275eb"}, + {file = "paramiko-3.3.1.tar.gz", hash = "sha256:6a3777a961ac86dbef375c5f5b8d50014a1a96d0fd7f054a43bc880134b0ff77"}, ] [package.dependencies] @@ -2828,4 +2827,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.7.0" -content-hash = "9dc5181178139fe742c1b9d18de9613e544a11e221b30299aabc8ab04b68cc09" \ No newline at end of file +content-hash = "9dc5181178139fe742c1b9d18de9613e544a11e221b30299aabc8ab04b68cc09" diff --git a/pyproject.toml b/pyproject.toml index 4f8b753b..60c52f91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ msgpack = "^1.0.0" neo4j = "^4.1.1" pylnk3 = "^0.4.2" pypsrp = "^0.7.0" -paramiko = "^2.7.2" +paramiko = "=3.3.1" impacket = { git = "https://github.com/mpgn/impacket.git", branch = "gkdi" } dsinternals = "^1.2.4" xmltodict = "^0.12.0" From 9cbd847fd6711f3176cf7a498efc50f08e62c7b6 Mon Sep 17 00:00:00 2001 From: romanrii Date: Sun, 24 Sep 2023 12:36:14 +0000 Subject: [PATCH 02/28] 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 03/28] 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 04/28] 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 d21bf5e46356acc3efa2c005bb42f4db71ecf150 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Sat, 7 Oct 2023 15:36:00 +0800 Subject: [PATCH 05/28] [ssh] Marshall review I Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 25 +++++++++++++------------ tests/e2e_commands.txt | 5 +++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index b43b1425..fd3a54b9 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -15,7 +15,7 @@ from paramiko.ssh_exception import ( class ssh(connection): def __init__(self, args, db, host): self.protocol = "SSH" - self.remote_version = "" + self.remote_version = "Unknown SSH Version" self.server_os_platform = "Linux" self.user_principal = "root" super().__init__(args, db, host) @@ -25,7 +25,7 @@ class ssh(connection): if self.create_conn_obj(): self.enum_host_info() self.print_host_info() - if not self.remote_version: + if self.remote_version == "Unknown SSH Version": self.conn.close() return if self.login(): @@ -46,13 +46,14 @@ class ssh(connection): ) def print_host_info(self): - self.logger.display(self.remote_version if self.remote_version else "Unknown SSH version, skipping...") + self.logger.display(self.remote_version if self.remote_version != "Unknown SSH Version" else f"{self.remote_version}, skipping...") return True def enum_host_info(self): - self.remote_version = self.conn._transport.remote_version - self.logger.debug(f'Remote version: {self.remote_version if self.remote_version else "Unknown SSH Version"}') - self.db.add_host(self.host, self.args.port, self.remote_version if self.remote_version else "Unknown SSH Version") + if self.conn._transport.remote_version: + self.remote_version = self.conn._transport.remote_version + self.logger.debug(f'Remote version: {self.remote_version}') + self.db.add_host(self.host, self.args.port, self.remote_version) def create_conn_obj(self): logging.getLogger("paramiko").disabled = True @@ -79,26 +80,26 @@ class ssh(connection): # we could add in another method to check by piping in the password to sudo # but that might be too much of an opsec concern - maybe add in a flag to do more checks? - self.logger.info(f"Determined user is root via `id && sudo -ln` command") - stdin, stdout, stderr = self.conn.exec_command("id && sudo -ln 2>&1") + self.logger.info(f"Determined user is root via `id ; sudo -ln` command") + stdin, stdout, stderr = self.conn.exec_command("id ; sudo -ln 2>&1") stdout = stdout.read().decode("utf-8", errors="ignore") - admin_Flag = { + admin_flag = { "(root)": [True, None], "NOPASSWD: ALL": [True, None], "(ALL : ALL) ALL": [True, None], "(sudo)": [False, f'Current user: "{self.username}" was in "sudo" group, please try "--sudo-check" to check if user can run sudo shell'], } - for keyword in admin_Flag.keys(): + for keyword in admin_flag.keys(): match = re.findall(re.escape(keyword), stdout) if match: self.logger.info(f'User: "{self.username}" matched keyword: {match[0]}') - self.admin_privs = admin_Flag[match[0]][0] + self.admin_privs = admin_flag[match[0]][0] if self.admin_privs: #break break else: # Continue find admin flag - tips = admin_Flag[match[0]][1] + tips = admin_flag[match[0]][1] continue if not self.admin_privs and "tips" in locals(): self.logger.display(tips) diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 951a5ad8..900bb139 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -202,6 +202,11 @@ netexec ssh TARGET_HOST -u data/test_users.txt -p test_passwords.txt --no-brutef netexec ssh TARGET_HOST -u data/test_users.txt -p test_passwords.txt netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --key-file data/test_key.priv netexec ssh TARGET_HOST -u USERNAME -p '' --key-file data/test_key.priv +netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check +netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method sudo-stdin +netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method sudo-stdin --get-output-tries 10 +netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method mkfifo +netexec ssh TARGET_HOST -u USERNAME -p PASSWORD --sudo-check --sudo-check-method mkfifo --get-output-tries 10 ##### 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 From 78de09be8d1c75e945d2705b20634023039d2769 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Sat, 7 Oct 2023 16:37:45 +0800 Subject: [PATCH 06/28] [ssh] Neff review I Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index fd3a54b9..3d6414f1 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -1,6 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import paramiko, re, uuid, logging, time, sys + +import paramiko +import re +import uuid +import logging +import time from io import StringIO from nxc.config import process_secret @@ -21,6 +26,7 @@ class ssh(connection): super().__init__(args, db, host) def proto_flow(self): + self.logger.debug(f"Kicking off proto_flow") self.proto_logger() if self.create_conn_obj(): self.enum_host_info() @@ -36,6 +42,8 @@ class ssh(connection): self.conn.close() def proto_logger(self): + logging.getLogger("paramiko").disabled = True + logging.getLogger("paramiko.transport").disabled = True self.logger = NXCAdapter( extra={ "protocol": "SSH", @@ -56,8 +64,6 @@ class ssh(connection): self.db.add_host(self.host, self.args.port, self.remote_version) def create_conn_obj(self): - logging.getLogger("paramiko").disabled = True - logging.getLogger("paramiko.transport").disabled = True self.conn = paramiko.SSHClient() self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: @@ -87,20 +93,18 @@ class ssh(connection): "(root)": [True, None], "NOPASSWD: ALL": [True, None], "(ALL : ALL) ALL": [True, None], - "(sudo)": [False, f'Current user: "{self.username}" was in "sudo" group, please try "--sudo-check" to check if user can run sudo shell'], + "(sudo)": [False, f'Current user: "{self.username}" was in "sudo" group, please try "--sudo-check" to check if user can run sudo shell'] } for keyword in admin_flag.keys(): match = re.findall(re.escape(keyword), stdout) if match: self.logger.info(f'User: "{self.username}" matched keyword: {match[0]}') self.admin_privs = admin_flag[match[0]][0] - if self.admin_privs: - #break - break - else: - # Continue find admin flag + if not self.admin_privs: tips = admin_flag[match[0]][1] continue + else: + break if not self.admin_privs and "tips" in locals(): self.logger.display(tips) return @@ -186,7 +190,6 @@ class ssh(connection): pkey = "" stdout = None stderr = None - cred_id = self.db.add_credential("plaintext", username, password) try: if self.args.key_file or private_key: self.logger.debug(f"Logging in with key") @@ -216,6 +219,7 @@ class ssh(connection): else: self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}") self.conn._transport.auth_password(username, password, fallback=True) + cred_id = self.db.add_credential("plaintext", username, password) # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey stdin, stdout, stderr = self.conn.exec_command("id") @@ -249,6 +253,8 @@ class ssh(connection): else: shell_access = True + self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access) + if shell_access and self.server_os_platform == "Linux": self.check_if_admin() if self.admin_privs: @@ -264,13 +270,17 @@ class ssh(connection): cred_id=cred_id, ) - self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access) - if self.args.key_file: password = f"(keyfile: {self.args.key_file})" - display_shell_access = f'Shell access! {f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})"}' if shell_access else "" - self.logger.success(f"{username}:{process_secret(password)} {highlight(display_shell_access)} {highlight(self.server_os_platform)} {self.mark_pwned()}") + display_shell_access = "- {} {} {}".format( + f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})", + self.server_os_platform, + '- Shell access!' if shell_access else '' + ) + # Force show pwn3d label + self.admin_privs = True + self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}") return True From cec042d8eed3d574c8961a31dfb0f6b8cee1dde4 Mon Sep 17 00:00:00 2001 From: Xiaoli Chan <2209553467@qq.com> Date: Sat, 7 Oct 2023 22:15:08 +0800 Subject: [PATCH 07/28] [SSH] fix private key with passphrase login Signed-off-by: Xiaoli Chan <2209553467@qq.com> --- nxc/protocols/ssh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 3d6414f1..341fecfd 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -199,12 +199,12 @@ class ssh(connection): pkey = paramiko.RSAKey.from_private_key_file(self.args.key_file) password = f"(keydata: {private_key})" if private_key else f"(keyfile: {self.args.key_file})" - self.conn._transport.auth_publickey(username, pkey) + self.conn._transport.auth_publickey(username, password, pkey) if private_key: cred_id = self.db.add_credential( "key", username, - "", + password if password != "" else "", key=private_key, ) else: @@ -213,7 +213,7 @@ class ssh(connection): cred_id = self.db.add_credential( "key", username, - "", + password if password != "" else "", key=key_data, ) else: From c9af18df67a806c541234c34001a4ac1909ad2ce Mon Sep 17 00:00:00 2001 From: Xiaoli Chan <2209553467@qq.com> Date: Sat, 7 Oct 2023 23:05:51 +0800 Subject: [PATCH 08/28] [SSH] fix private key with passphrase login Signed-off-by: Xiaoli Chan <2209553467@qq.com> --- nxc/protocols/ssh.py | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 341fecfd..ac14e273 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -6,6 +6,7 @@ import re import uuid import logging import time +import base64 from io import StringIO from nxc.config import process_secret @@ -193,29 +194,22 @@ class ssh(connection): try: if self.args.key_file or private_key: self.logger.debug(f"Logging in with key") - if private_key: - pkey = paramiko.RSAKey.from_private_key(StringIO(private_key)) - else: - pkey = paramiko.RSAKey.from_private_key_file(self.args.key_file) + + if self.args.key_file: + with open(self.args.key_file, 'r') as f: + private_key = f.read() + + pkey = paramiko.RSAKey.from_private_key(StringIO(private_key), password) + + self.conn._transport.auth_publickey(username, pkey) + + cred_id = self.db.add_credential( + "key", + username, + password if password != "" else "", + key=private_key, + ) - password = f"(keydata: {private_key})" if private_key else f"(keyfile: {self.args.key_file})" - self.conn._transport.auth_publickey(username, password, pkey) - if private_key: - cred_id = self.db.add_credential( - "key", - username, - password if password != "" else "", - key=private_key, - ) - else: - with open(self.args.key_file, "r") as f: - key_data = f.read() - cred_id = self.db.add_credential( - "key", - username, - password if password != "" else "", - key=key_data, - ) else: self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}") self.conn._transport.auth_password(username, password, fallback=True) @@ -271,7 +265,7 @@ class ssh(connection): ) if self.args.key_file: - password = f"(keyfile: {self.args.key_file})" + password = f"{password} (keyfile: {self.args.key_file})" display_shell_access = "- {} {} {}".format( f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})", @@ -298,4 +292,4 @@ class ssh(connection): if get_output: for line in stdout: self.logger.highlight(line.strip()) - return stdout + return stdout \ No newline at end of file From ea62e4252607717d5260746f16dfc3dc2e357da5 Mon Sep 17 00:00:00 2001 From: Xiaoli Chan <2209553467@qq.com> Date: Sat, 7 Oct 2023 23:35:36 +0800 Subject: [PATCH 09/28] [SSH] improve login failed output Signed-off-by: Xiaoli Chan <2209553467@qq.com> --- nxc/protocols/ssh.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index ac14e273..96ec4072 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -188,7 +188,7 @@ class ssh(connection): def plaintext_login(self, username, password, private_key=None): self.username = username self.password = password - pkey = "" + private_key = "" stdout = None stderr = None try: @@ -219,7 +219,9 @@ class ssh(connection): stdin, stdout, stderr = self.conn.exec_command("id") stdout = stdout.read().decode("utf-8", errors="ignore") except Exception as e: - self.logger.fail(f"{username}:{process_secret(password) if not pkey else password} {e}") + if self.args.key_file: + password = f"{process_secret(password)} (keyfile: {self.args.key_file})" + self.logger.fail(f"{username}:{password} {e}") self.conn.close() return False else: @@ -265,7 +267,7 @@ class ssh(connection): ) if self.args.key_file: - password = f"{password} (keyfile: {self.args.key_file})" + password = f"{process_secret(password)} (keyfile: {self.args.key_file})" display_shell_access = "- {} {} {}".format( f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})", @@ -274,7 +276,7 @@ class ssh(connection): ) # Force show pwn3d label self.admin_privs = True - self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}") + self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}") return True From 95ccfca992d10523c42b938785197009e32e7bf7 Mon Sep 17 00:00:00 2001 From: Xiaoli Chan <2209553467@qq.com> Date: Sun, 8 Oct 2023 00:02:08 +0800 Subject: [PATCH 10/28] [SSH] Logic fix Signed-off-by: Xiaoli Chan <2209553467@qq.com> --- nxc/protocols/ssh.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 96ec4072..3e1de711 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -269,13 +269,12 @@ class ssh(connection): if self.args.key_file: password = f"{process_secret(password)} (keyfile: {self.args.key_file})" - display_shell_access = "- {} {} {}".format( + display_shell_access = "{} {} {}".format( f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})", self.server_os_platform, '- Shell access!' if shell_access else '' ) - # Force show pwn3d label - self.admin_privs = True + self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}") return True From 84a41b72a23ab3452801cb336cc18405114bd44c Mon Sep 17 00:00:00 2001 From: Xiaoli Chan <2209553467@qq.com> Date: Sun, 8 Oct 2023 00:03:21 +0800 Subject: [PATCH 11/28] [SSH] Remove useless package Signed-off-by: Xiaoli Chan <2209553467@qq.com> --- nxc/protocols/ssh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 3e1de711..0c694114 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -6,7 +6,6 @@ import re import uuid import logging import time -import base64 from io import StringIO from nxc.config import process_secret From f6fed70894a5d0ea1119802cc544b11ae50d443a Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Thu, 12 Oct 2023 16:28:36 -0400 Subject: [PATCH 12/28] 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 0ef92f0a049f87663af5838ba5c67cbee19042e9 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Fri, 13 Oct 2023 20:56:05 +0800 Subject: [PATCH 13/28] [ssh] update pyproject.toml Signed-off-by: XiaoliChan <2209553467@qq.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 60c52f91..2f255a6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,7 @@ msgpack = "^1.0.0" neo4j = "^4.1.1" pylnk3 = "^0.4.2" pypsrp = "^0.7.0" -paramiko = "=3.3.1" +paramiko = "^3.3.1" impacket = { git = "https://github.com/mpgn/impacket.git", branch = "gkdi" } dsinternals = "^1.2.4" xmltodict = "^0.12.0" From 0e4c545bc0b02b9dbc04e6cae373c587f8d5f976 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 15 Oct 2023 09:09:26 -0400 Subject: [PATCH 14/28] Formating --- nxc/protocols/ssh.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 0c694114..349e3975 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -17,6 +17,7 @@ from paramiko.ssh_exception import ( SSHException, ) + class ssh(connection): def __init__(self, args, db, host): self.protocol = "SSH" @@ -24,7 +25,7 @@ class ssh(connection): self.server_os_platform = "Linux" self.user_principal = "root" super().__init__(args, db, host) - + def proto_flow(self): self.logger.debug(f"Kicking off proto_flow") self.proto_logger() @@ -86,11 +87,11 @@ class ssh(connection): # we could add in another method to check by piping in the password to sudo # but that might be too much of an opsec concern - maybe add in a flag to do more checks? - self.logger.info(f"Determined user is root via `id ; sudo -ln` command") - stdin, stdout, stderr = self.conn.exec_command("id ; sudo -ln 2>&1") + self.logger.info(f"Determined user is root via `id; sudo -ln` command") + stdin, stdout, stderr = self.conn.exec_command("id; sudo -ln 2>&1") stdout = stdout.read().decode("utf-8", errors="ignore") admin_flag = { - "(root)": [True, None], + "(root)": [True, None], "NOPASSWD: ALL": [True, None], "(ALL : ALL) ALL": [True, None], "(sudo)": [False, f'Current user: "{self.username}" was in "sudo" group, please try "--sudo-check" to check if user can run sudo shell'] @@ -113,11 +114,11 @@ class ssh(connection): if not self.password: self.logger.error("Check admin with sudo not support private key.") return - + if self.args.sudo_check_method: method = self.args.sudo_check_method self.logger.info(f"Doing sudo check with method: {method}") - + if method == "sudo-stdin": stdin, stdout, stderr = self.conn.exec_command("sudo --help") stdout = stdout.read().decode("utf-8", errors="ignore") @@ -135,7 +136,7 @@ class ssh(connection): break if stderr.read().decode('utf-8'): time.sleep(2) - tries +=1 + tries += 1 else: self.logger.info(f"{shadow_Backup} existed") self.admin_privs = True @@ -193,7 +194,7 @@ class ssh(connection): try: if self.args.key_file or private_key: self.logger.debug(f"Logging in with key") - + if self.args.key_file: with open(self.args.key_file, 'r') as f: private_key = f.read() @@ -275,9 +276,9 @@ class ssh(connection): ) self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}") - + return True - + def execute(self, payload=None, get_output=False): if not payload and self.args.execute: payload = self.args.execute @@ -292,4 +293,4 @@ class ssh(connection): if get_output: for line in stdout: self.logger.highlight(line.strip()) - return stdout \ No newline at end of file + return stdout From dfafcb2975e364c73f19616c2c7728f30f9c5836 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 15 Oct 2023 10:59:22 -0400 Subject: [PATCH 15/28] 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 16/28] 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 17/28] 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 == ".": From d2ed2aaf9e62758d643b608c64fda731e02446bf Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Wed, 18 Oct 2023 20:13:43 +0800 Subject: [PATCH 18/28] [ssh] conflict fix Signed-off-by: XiaoliChan <2209553467@qq.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ef1d5b30..03b2016f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ termcolor = "^2.3.0" msgpack = "^1.0.0" neo4j = "^4.1.1" # do not upgrade this until performance regression issues in 5 are fixed (as of 9/23) pylnk3 = "^0.4.2" -pypsrp = "^0.7.0" +pypsrp = "^0.8.1" paramiko = "^3.3.1" impacket = { git = "https://github.com/Pennyw0rth/impacket.git", branch = "gkdi" } dsinternals = "^1.2.4" From feac7f07bfb9878f3c6b3c480728c1d918437f01 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Wed, 18 Oct 2023 22:15:38 +0800 Subject: [PATCH 19/28] [ssh] Neff review I Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 57 +++++++++++++++++---------------- nxc/protocols/ssh/proto_args.py | 6 ++++ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 349e3975..0a9cda09 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -88,8 +88,8 @@ class ssh(connection): # we could add in another method to check by piping in the password to sudo # but that might be too much of an opsec concern - maybe add in a flag to do more checks? self.logger.info(f"Determined user is root via `id; sudo -ln` command") - stdin, stdout, stderr = self.conn.exec_command("id; sudo -ln 2>&1") - stdout = stdout.read().decode("utf-8", errors="ignore") + _, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1") + stdout = stdout.read().decode(self.args.codec, errors="ignore") admin_flag = { "(root)": [True, None], "NOPASSWD: ALL": [True, None], @@ -120,17 +120,18 @@ class ssh(connection): self.logger.info(f"Doing sudo check with method: {method}") if method == "sudo-stdin": - stdin, stdout, stderr = self.conn.exec_command("sudo --help") - stdout = stdout.read().decode("utf-8", errors="ignore") + _, stdout, _ = self.conn.exec_command("sudo --help") + stdout = stdout.read().decode(self.args.codec, errors="ignore") + # Read sudo help docs and find "stdin" if "stdin" in stdout: shadow_Backup = f'/tmp/{uuid.uuid4()}' # sudo support stdin password - stdin, stdout, stderr = self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_Backup} >/dev/null 2>&1 &") - stdin, stdout, stderr = self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_Backup} >/dev/null 2>&1 &") + _, _, _ = self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_Backup} >/dev/null 2>&1 &") + _, _, _ = self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_Backup} >/dev/null 2>&1 &") tries = 1 while True: self.logger.info(f"Checking {shadow_Backup} if it existed") - stdin, stdout, stderr = self.conn.exec_command(f'ls {shadow_Backup}') + _, _, stderr = self.conn.exec_command(f'ls {shadow_Backup}') if tries >= self.args.get_output_tries: self.logger.info(f'{shadow_Backup} not existed, maybe the pipe has been hanged over, please increase the number of tries with the option "--get-output-tries" or change other method with "--sudo-check-method". If it\'s still failing maybe sudo shell is not working with current user') break @@ -142,23 +143,23 @@ class ssh(connection): self.admin_privs = True break self.logger.info(f"Remove up temporary files") - stdin, stdout, stderr = self.conn.exec_command(f"rm -rf {shadow_Backup}") + _, _, _ = self.conn.exec_command(f"echo '' > {shadow_Backup}") else: self.logger.error("Command: 'sudo' not support stdin mode, running command with 'sudo' failed") return else: - stdin, stdout, stderr = self.conn.exec_command("mkfifo --help") - stdout = stdout.read().decode("utf-8", errors="ignore") + _, stdout, _ = self.conn.exec_command("mkfifo --help") + stdout = stdout.read().decode(self.args.codec, errors="ignore") # check if user can execute mkfifo if "Create named pipes" in stdout: self.logger.info("Command: 'mkfifo' available") pipe_stdin = f'/tmp/systemd-{uuid.uuid4()}' pipe_stdout = f'/tmp/systemd-{uuid.uuid4()}' shadow_Backup = f'/tmp/{uuid.uuid4()}' - stdin, stdout, stderr = self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") + _, _, _ = self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") # 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat - stdin, stdout, stderr = self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") - stdin, stdout, stderr = self.conn.exec_command(f"echo 'sudo -s' > {pipe_stdin} && echo '{self.password}' > {pipe_stdin}") + _, _, _ = self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") + _, _, _ = self.conn.exec_command(f"echo 'sudo -s' > {pipe_stdin} && echo '{self.password}' > {pipe_stdin}") # Sometime the pipe will hanging(only happen with paramiko) # Can't get "whoami" or "id" result in pipe_stdout, maybe something wrong using pipe with paramiko # But one thing I can confirm, is the command was executed even can't get result from pipe_stdout @@ -166,21 +167,21 @@ class ssh(connection): self.logger.info(f"Copy /etc/shadow to {shadow_Backup} if pass the sudo auth") while True: self.logger.info(f"Checking {shadow_Backup} if it existed") - stdin, stdout, stderr = self.conn.exec_command(f'ls {shadow_Backup}') + _, _, stderr = self.conn.exec_command(f'ls {shadow_Backup}') if tries >= self.args.get_output_tries: self.logger.info(f'{shadow_Backup} not existed, maybe the pipe has been hanged over, please increase the number of tries with the option "--get-output-tries" or change other method with "--sudo-check-method". If it\'s still failing maybe sudo shell is not working with current user') break if stderr.read().decode('utf-8'): time.sleep(2) - stdin, stdout, stderr = self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_Backup} && chmod 777 {shadow_Backup}' > {pipe_stdin}") + _, _, _ = self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_Backup} && chmod 777 {shadow_Backup}' > {pipe_stdin}") tries += 1 else: self.logger.info(f"{shadow_Backup} existed") self.admin_privs = True break self.logger.info(f"Remove up temporary files") - stdin, stdout, stderr = self.conn.exec_command(f"rm -rf {shadow_Backup} {pipe_stdin} {pipe_stdout}") + _, _, _ = self.conn.exec_command(f"echo '' > {shadow_Backup} && rm -rf {pipe_stdin} {pipe_stdout}") else: self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed") return @@ -196,7 +197,7 @@ class ssh(connection): self.logger.debug(f"Logging in with key") if self.args.key_file: - with open(self.args.key_file, 'r') as f: + with open(self.args.key_file, "r") as f: private_key = f.read() pkey = paramiko.RSAKey.from_private_key(StringIO(private_key), password) @@ -216,8 +217,8 @@ class ssh(connection): cred_id = self.db.add_credential("plaintext", username, password) # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey - stdin, stdout, stderr = self.conn.exec_command("id") - stdout = stdout.read().decode("utf-8", errors="ignore") + _, stdout, _ = self.conn.exec_command("id") + stdout = stdout.read().decode(self.args.codec, errors="ignore") except Exception as e: if self.args.key_file: password = f"{process_secret(password)} (keyfile: {self.args.key_file})" @@ -229,8 +230,8 @@ class ssh(connection): host_id = self.db.get_hosts(self.host)[0].id if not stdout: - stdin, stdout, stderr = self.conn.exec_command("whoami /priv") - stdout = stdout.read().decode("utf-8", errors="ignore") + _, stdout, _ = self.conn.exec_command("whoami /priv") + stdout = stdout.read().decode(self.args.codec, errors="ignore") self.server_os_platform = "Windows" self.user_principal = "admin" if "SeDebugPrivilege" in stdout: @@ -285,12 +286,14 @@ class ssh(connection): if not self.args.no_output: get_output = True try: - stdin, stdout, stderr = self.conn.exec_command(f"{payload} 2>&1") - except AttributeError: - return "" - if get_output: + _, stdout, _ = self.conn.exec_command(f"{payload} 2>&1") + stdout = stdout.read().decode(self.args.codec, errors="ignore") + except Exception as e: + self.logger.fail(f"Execute command failed, error: {str(e)}") + return False + else: self.logger.success("Executed command") if get_output: - for line in stdout: + for line in stdout.split('\n'): self.logger.highlight(line.strip()) - return stdout + return stdout \ No newline at end of file diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index 6c8936f6..af00d860 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -8,6 +8,12 @@ def proto_args(parser, std_parser, module_parser): ssh_parser.add_argument("--get-output-tries", help="Number of times with sudo command tries to get results, default is %(default)s", type=int, default=5) cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") + cgroup.add_argument("--codec", default="utf-8", + help="Set encoding used (codec) from the target's output (default " + "\"utf-8\"). If errors are detected, run chcp.com at the target, " + "map the result with " + "https://docs.python.org/3/library/codecs.html#standard-encodings and then execute " + "again with --codec and the corresponding codec") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command") From 113b98060c3a498223adb1cee825e201fc5dc066 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Wed, 18 Oct 2023 22:40:37 +0800 Subject: [PATCH 20/28] [ssh] Neff review: args condition Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh/proto_args.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index af00d860..ca7e95aa 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -1,11 +1,14 @@ +from argparse import _StoreAction + def proto_args(parser, std_parser, module_parser): ssh_parser = parser.add_parser("ssh", help="own stuff using SSH", parents=[std_parser, module_parser]) ssh_parser.add_argument("--key-file", type=str, help="Authenticate using the specified private key. Treats the password parameter as the key's passphrase.") ssh_parser.add_argument("--port", type=int, default=22, help="SSH port (default: 22)") ssh_parser.add_argument("--ssh-timeout", help="SSH connection timeout, default is %(default)s secondes", type=int, default=15) - ssh_parser.add_argument("--sudo-check", action="store_true", help="Check user privilege with sudo") - ssh_parser.add_argument("--sudo-check-method", choices={"sudo-stdin", "mkfifo"}, default="sudo-stdin", help="method to do with sudo check, default is '%(default)s (mkfifo is non-stable, probably you need to execute once again if it failed)'") + sudo_check_arg = ssh_parser.add_argument("--sudo-check", action="store_true", help="Check user privilege with sudo") + sudo_check_method_arg = ssh_parser.add_argument("--sudo-check-method", action=get_conditional_action(_StoreAction), make_required=[], choices={"sudo-stdin", "mkfifo"}, default="sudo-stdin", help="method to do with sudo check, default is '%(default)s (mkfifo is non-stable, probably you need to execute once again if it failed)'") ssh_parser.add_argument("--get-output-tries", help="Number of times with sudo command tries to get results, default is %(default)s", type=int, default=5) + sudo_check_method_arg.make_required.append(sudo_check_arg) cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument("--codec", default="utf-8", @@ -17,4 +20,18 @@ def proto_args(parser, std_parser, module_parser): cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command") - return parser \ No newline at end of file + return parser + +def get_conditional_action(baseAction): + class ConditionalAction(baseAction): + def __init__(self, option_strings, dest, **kwargs): + x = kwargs.pop('make_required', []) + super(ConditionalAction, self).__init__(option_strings, dest, **kwargs) + self.make_required = x + + def __call__(self, parser, namespace, values, option_string=None): + for x in self.make_required: + x.required = True + super(ConditionalAction, self).__call__(parser, namespace, values, option_string) + + return ConditionalAction \ No newline at end of file From 6385cce49a92d53f1f3107cfe04d3053b5a541be Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Wed, 18 Oct 2023 22:56:23 +0200 Subject: [PATCH 21/28] Formating, ruff linting and removal of unused code --- nxc/protocols/ssh.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 0a9cda09..22608367 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -6,10 +6,11 @@ import re import uuid import logging import time +import socket from io import StringIO from nxc.config import process_secret -from nxc.connection import * +from nxc.connection import connection, highlight from nxc.logger import NXCAdapter from paramiko.ssh_exception import ( AuthenticationException, @@ -27,7 +28,7 @@ class ssh(connection): super().__init__(args, db, host) def proto_flow(self): - self.logger.debug(f"Kicking off proto_flow") + self.logger.debug("Kicking off proto_flow") self.proto_logger() if self.create_conn_obj(): self.enum_host_info() @@ -87,7 +88,7 @@ class ssh(connection): # we could add in another method to check by piping in the password to sudo # but that might be too much of an opsec concern - maybe add in a flag to do more checks? - self.logger.info(f"Determined user is root via `id; sudo -ln` command") + self.logger.info("Determined user is root via `id; sudo -ln` command") _, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1") stdout = stdout.read().decode(self.args.codec, errors="ignore") admin_flag = { @@ -103,7 +104,6 @@ class ssh(connection): self.admin_privs = admin_flag[match[0]][0] if not self.admin_privs: tips = admin_flag[match[0]][1] - continue else: break if not self.admin_privs and "tips" in locals(): @@ -126,8 +126,8 @@ class ssh(connection): if "stdin" in stdout: shadow_Backup = f'/tmp/{uuid.uuid4()}' # sudo support stdin password - _, _, _ = self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_Backup} >/dev/null 2>&1 &") - _, _, _ = self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_Backup} >/dev/null 2>&1 &") + self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_Backup} >/dev/null 2>&1 &") + self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_Backup} >/dev/null 2>&1 &") tries = 1 while True: self.logger.info(f"Checking {shadow_Backup} if it existed") @@ -142,8 +142,8 @@ class ssh(connection): self.logger.info(f"{shadow_Backup} existed") self.admin_privs = True break - self.logger.info(f"Remove up temporary files") - _, _, _ = self.conn.exec_command(f"echo '' > {shadow_Backup}") + self.logger.info("Remove up temporary files") + self.conn.exec_command(f"echo '' > {shadow_Backup}") else: self.logger.error("Command: 'sudo' not support stdin mode, running command with 'sudo' failed") return @@ -156,10 +156,10 @@ class ssh(connection): pipe_stdin = f'/tmp/systemd-{uuid.uuid4()}' pipe_stdout = f'/tmp/systemd-{uuid.uuid4()}' shadow_Backup = f'/tmp/{uuid.uuid4()}' - _, _, _ = self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") + self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") # 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat - _, _, _ = self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") - _, _, _ = self.conn.exec_command(f"echo 'sudo -s' > {pipe_stdin} && echo '{self.password}' > {pipe_stdin}") + self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") + self.conn.exec_command(f"echo 'sudo -s' > {pipe_stdin} && echo '{self.password}' > {pipe_stdin}") # Sometime the pipe will hanging(only happen with paramiko) # Can't get "whoami" or "id" result in pipe_stdout, maybe something wrong using pipe with paramiko # But one thing I can confirm, is the command was executed even can't get result from pipe_stdout @@ -174,14 +174,14 @@ class ssh(connection): if stderr.read().decode('utf-8'): time.sleep(2) - _, _, _ = self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_Backup} && chmod 777 {shadow_Backup}' > {pipe_stdin}") + self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_Backup} && chmod 777 {shadow_Backup}' > {pipe_stdin}") tries += 1 else: self.logger.info(f"{shadow_Backup} existed") self.admin_privs = True break - self.logger.info(f"Remove up temporary files") - _, _, _ = self.conn.exec_command(f"echo '' > {shadow_Backup} && rm -rf {pipe_stdin} {pipe_stdout}") + self.logger.info("Remove up temporary files") + self.conn.exec_command(f"echo '' > {shadow_Backup} && rm -rf {pipe_stdin} {pipe_stdout}") else: self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed") return @@ -191,10 +191,9 @@ class ssh(connection): self.password = password private_key = "" stdout = None - stderr = None try: if self.args.key_file or private_key: - self.logger.debug(f"Logging in with key") + self.logger.debug("Logging in with key") if self.args.key_file: with open(self.args.key_file, "r") as f: From 78ed50806621a2d80fc0b52bafeb183d355a47c1 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Fri, 20 Oct 2023 15:56:39 +0800 Subject: [PATCH 22/28] [ssh] Marshall review I Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 38 +++++++++++++++---------------- nxc/protocols/winrm/proto_args.py | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 22608367..ad4c0abd 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -112,7 +112,7 @@ class ssh(connection): def check_if_admin_sudo(self): if not self.password: - self.logger.error("Check admin with sudo not support private key.") + self.logger.error("Check admin with sudo does not support using a private key") return if self.args.sudo_check_method: @@ -124,26 +124,26 @@ class ssh(connection): stdout = stdout.read().decode(self.args.codec, errors="ignore") # Read sudo help docs and find "stdin" if "stdin" in stdout: - shadow_Backup = f'/tmp/{uuid.uuid4()}' + shadow_backup = f'/tmp/{uuid.uuid4()}' # sudo support stdin password - self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_Backup} >/dev/null 2>&1 &") - self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_Backup} >/dev/null 2>&1 &") + self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_backup} >/dev/null 2>&1 &") + self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_backup} >/dev/null 2>&1 &") tries = 1 while True: - self.logger.info(f"Checking {shadow_Backup} if it existed") - _, _, stderr = self.conn.exec_command(f'ls {shadow_Backup}') + self.logger.info(f"Checking {shadow_backup} if it existed") + _, _, stderr = self.conn.exec_command(f'ls {shadow_backup}') if tries >= self.args.get_output_tries: - self.logger.info(f'{shadow_Backup} not existed, maybe the pipe has been hanged over, please increase the number of tries with the option "--get-output-tries" or change other method with "--sudo-check-method". If it\'s still failing maybe sudo shell is not working with current user') + self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option '--get-output-tries' or change other method with '--sudo-check-method'. If it's still failing, maybe sudo shell does not work with the current user") break - if stderr.read().decode('utf-8'): + if stderr.read().decode("utf-8"): time.sleep(2) tries += 1 else: - self.logger.info(f"{shadow_Backup} existed") + self.logger.info(f"{shadow_backup} existed") self.admin_privs = True break self.logger.info("Remove up temporary files") - self.conn.exec_command(f"echo '' > {shadow_Backup}") + self.conn.exec_command(f"echo '' > {shadow_backup}") else: self.logger.error("Command: 'sudo' not support stdin mode, running command with 'sudo' failed") return @@ -155,7 +155,7 @@ class ssh(connection): self.logger.info("Command: 'mkfifo' available") pipe_stdin = f'/tmp/systemd-{uuid.uuid4()}' pipe_stdout = f'/tmp/systemd-{uuid.uuid4()}' - shadow_Backup = f'/tmp/{uuid.uuid4()}' + shadow_backup = f'/tmp/{uuid.uuid4()}' self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") # 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") @@ -164,24 +164,24 @@ class ssh(connection): # Can't get "whoami" or "id" result in pipe_stdout, maybe something wrong using pipe with paramiko # But one thing I can confirm, is the command was executed even can't get result from pipe_stdout tries = 1 - self.logger.info(f"Copy /etc/shadow to {shadow_Backup} if pass the sudo auth") + self.logger.info(f"Copy /etc/shadow to {shadow_backup} if pass the sudo auth") while True: - self.logger.info(f"Checking {shadow_Backup} if it existed") - _, _, stderr = self.conn.exec_command(f'ls {shadow_Backup}') + self.logger.info(f"Checking {shadow_backup} if it existed") + _, _, stderr = self.conn.exec_command(f"ls {shadow_backup}") if tries >= self.args.get_output_tries: - self.logger.info(f'{shadow_Backup} not existed, maybe the pipe has been hanged over, please increase the number of tries with the option "--get-output-tries" or change other method with "--sudo-check-method". If it\'s still failing maybe sudo shell is not working with current user') + self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option \"--get-output-tries\" or change other method with \"--sudo-check-method\". If it's still failing, maybe sudo shell does not work with the current user") break - if stderr.read().decode('utf-8'): + if stderr.read().decode("utf-8"): time.sleep(2) - self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_Backup} && chmod 777 {shadow_Backup}' > {pipe_stdin}") + self.conn.exec_command(f"echo 'cp /etc/shadow {shadow_backup} && chmod 777 {shadow_backup}' > {pipe_stdin}") tries += 1 else: - self.logger.info(f"{shadow_Backup} existed") + self.logger.info(f"{shadow_backup} existed") self.admin_privs = True break self.logger.info("Remove up temporary files") - self.conn.exec_command(f"echo '' > {shadow_Backup} && rm -rf {pipe_stdin} {pipe_stdout}") + self.conn.exec_command(f"echo '' > {shadow_backup} && rm -rf {pipe_stdin} {pipe_stdout}") else: self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed") return diff --git a/nxc/protocols/winrm/proto_args.py b/nxc/protocols/winrm/proto_args.py index 991cfc83..004bb076 100644 --- a/nxc/protocols/winrm/proto_args.py +++ b/nxc/protocols/winrm/proto_args.py @@ -23,7 +23,7 @@ def proto_args(parser, std_parser, module_parser): cgroup = winrm_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default " - "\"utf-8\"). If errors are detected, run chcp.com at the target, " + "'utf-8'). If errors are detected, run chcp.com at the target, " "map the result with " "https://docs.python.org/3/library/codecs.html#standard-encodings and then execute " "again with --codec and the corresponding codec") From fa7c5945efe964700861186907542d12564f3cc9 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Fri, 20 Oct 2023 15:59:58 +0800 Subject: [PATCH 23/28] [ssh] Marshall review I Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh/proto_args.py | 2 +- nxc/protocols/winrm/proto_args.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index ca7e95aa..51b6c88b 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -13,7 +13,7 @@ def proto_args(parser, std_parser, module_parser): cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default " - "\"utf-8\"). If errors are detected, run chcp.com at the target, " + "'utf-8'). If errors are detected, run chcp.com at the target, " "map the result with " "https://docs.python.org/3/library/codecs.html#standard-encodings and then execute " "again with --codec and the corresponding codec") diff --git a/nxc/protocols/winrm/proto_args.py b/nxc/protocols/winrm/proto_args.py index 004bb076..991cfc83 100644 --- a/nxc/protocols/winrm/proto_args.py +++ b/nxc/protocols/winrm/proto_args.py @@ -23,7 +23,7 @@ def proto_args(parser, std_parser, module_parser): cgroup = winrm_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output (default " - "'utf-8'). If errors are detected, run chcp.com at the target, " + "\"utf-8\"). If errors are detected, run chcp.com at the target, " "map the result with " "https://docs.python.org/3/library/codecs.html#standard-encodings and then execute " "again with --codec and the corresponding codec") From 6a5f76b939ebffce72527b175dee932915d3e52f Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sat, 21 Oct 2023 07:08:36 -0400 Subject: [PATCH 24/28] Remove all files created on the target --- nxc/protocols/ssh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index ad4c0abd..93f0cf6d 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -142,8 +142,8 @@ class ssh(connection): self.logger.info(f"{shadow_backup} existed") self.admin_privs = True break - self.logger.info("Remove up temporary files") - self.conn.exec_command(f"echo '' > {shadow_backup}") + self.logger.info(f"Remove up temporary files {shadow_backup}") + self.conn.exec_command(f"echo {self.password} | sudo -S rm -rf {shadow_backup}") else: self.logger.error("Command: 'sudo' not support stdin mode, running command with 'sudo' failed") return @@ -180,8 +180,8 @@ class ssh(connection): self.logger.info(f"{shadow_backup} existed") self.admin_privs = True break - self.logger.info("Remove up temporary files") - self.conn.exec_command(f"echo '' > {shadow_backup} && rm -rf {pipe_stdin} {pipe_stdout}") + self.logger.info(f"Remove up temporary files {shadow_backup} {pipe_stdin} {pipe_stdout}") + self.conn.exec_command(f"echo 'rm -rf {shadow_backup}' > {pipe_stdin} && rm -rf {pipe_stdin} {pipe_stdout}") else: self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed") return From 35cdf414b762d5bb2194acf97523bad5d351996b Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Sat, 21 Oct 2023 23:13:47 +0800 Subject: [PATCH 25/28] [ssh] Neff review II Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 93f0cf6d..539d1c56 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -62,7 +62,7 @@ class ssh(connection): def enum_host_info(self): if self.conn._transport.remote_version: self.remote_version = self.conn._transport.remote_version - self.logger.debug(f'Remote version: {self.remote_version}') + self.logger.debug(f"Remote version: {self.remote_version}") self.db.add_host(self.host, self.args.port, self.remote_version) def create_conn_obj(self): @@ -95,12 +95,12 @@ class ssh(connection): "(root)": [True, None], "NOPASSWD: ALL": [True, None], "(ALL : ALL) ALL": [True, None], - "(sudo)": [False, f'Current user: "{self.username}" was in "sudo" group, please try "--sudo-check" to check if user can run sudo shell'] + "(sudo)": [False, f"Current user: '{self.username}' was in 'sudo' group, please try '--sudo-check' to check if user can run sudo shell"] } for keyword in admin_flag.keys(): match = re.findall(re.escape(keyword), stdout) if match: - self.logger.info(f'User: "{self.username}" matched keyword: {match[0]}') + self.logger.info(f"User: '{self.username}' matched keyword: {match[0]}") self.admin_privs = admin_flag[match[0]][0] if not self.admin_privs: tips = admin_flag[match[0]][1] @@ -124,14 +124,14 @@ class ssh(connection): stdout = stdout.read().decode(self.args.codec, errors="ignore") # Read sudo help docs and find "stdin" if "stdin" in stdout: - shadow_backup = f'/tmp/{uuid.uuid4()}' + shadow_backup = f"/tmp/{uuid.uuid4()}" # sudo support stdin password self.conn.exec_command(f"echo {self.password} | sudo -S cp /etc/shadow {shadow_backup} >/dev/null 2>&1 &") self.conn.exec_command(f"echo {self.password} | sudo -S chmod 777 {shadow_backup} >/dev/null 2>&1 &") tries = 1 while True: self.logger.info(f"Checking {shadow_backup} if it existed") - _, _, stderr = self.conn.exec_command(f'ls {shadow_backup}') + _, _, stderr = self.conn.exec_command(f"ls {shadow_backup}") if tries >= self.args.get_output_tries: self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option '--get-output-tries' or change other method with '--sudo-check-method'. If it's still failing, maybe sudo shell does not work with the current user") break @@ -153,9 +153,9 @@ class ssh(connection): # check if user can execute mkfifo if "Create named pipes" in stdout: self.logger.info("Command: 'mkfifo' available") - pipe_stdin = f'/tmp/systemd-{uuid.uuid4()}' - pipe_stdout = f'/tmp/systemd-{uuid.uuid4()}' - shadow_backup = f'/tmp/{uuid.uuid4()}' + pipe_stdin = f"/tmp/systemd-{uuid.uuid4()}" + pipe_stdout = f"/tmp/systemd-{uuid.uuid4()}" + shadow_backup = f"/tmp/{uuid.uuid4()}" self.conn.exec_command(f"mkfifo {pipe_stdin}; tail -f {pipe_stdin} | /bin/sh 2>&1 > {pipe_stdout} >/dev/null 2>&1 &") # 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat self.conn.exec_command(f"echo 'script -qc /bin/sh /dev/null' > {pipe_stdin}") @@ -169,7 +169,7 @@ class ssh(connection): self.logger.info(f"Checking {shadow_backup} if it existed") _, _, stderr = self.conn.exec_command(f"ls {shadow_backup}") if tries >= self.args.get_output_tries: - self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option \"--get-output-tries\" or change other method with \"--sudo-check-method\". If it's still failing, maybe sudo shell does not work with the current user") + self.logger.info(f"The file {shadow_backup} does not exist, the pipe may be hanging. Increase the number of tries with the option '--get-output-tries' or change other method with '--sudo-check-method'. If it's still failing, maybe sudo shell does not work with the current user") break if stderr.read().decode("utf-8"): @@ -272,7 +272,7 @@ class ssh(connection): display_shell_access = "{} {} {}".format( f"({self.user_principal})" if self.admin_privs else f"(non {self.user_principal})", self.server_os_platform, - '- Shell access!' if shell_access else '' + "- Shell access!" if shell_access else "" ) self.logger.success(f"{username}:{password} {self.mark_pwned()} {highlight(display_shell_access)}") @@ -293,6 +293,6 @@ class ssh(connection): else: self.logger.success("Executed command") if get_output: - for line in stdout.split('\n'): - self.logger.highlight(line.strip()) + for line in stdout.split("\n"): + self.logger.highlight(line.strip("\n")) return stdout \ No newline at end of file From 425e83c4c4c30ae1aca545985645cf7a71331775 Mon Sep 17 00:00:00 2001 From: XiaoliChan <2209553467@qq.com> Date: Sat, 21 Oct 2023 23:38:06 +0800 Subject: [PATCH 26/28] [ssh] disable look_for_keys in paramiko Signed-off-by: XiaoliChan <2209553467@qq.com> --- nxc/protocols/ssh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 539d1c56..0a61c8f1 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -69,7 +69,7 @@ class ssh(connection): self.conn = paramiko.SSHClient() self.conn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) try: - self.conn.connect(self.host, port=self.args.port, timeout=self.args.ssh_timeout) + self.conn.connect(self.host, port=self.args.port, timeout=self.args.ssh_timeout, look_for_keys=False) except AuthenticationException: return True except SSHException: From 2681244c3ba8ea2eb520daa19a1275dfd179bbf1 Mon Sep 17 00:00:00 2001 From: Marshall Hallenbeck Date: Sat, 21 Oct 2023 23:15:22 -0400 Subject: [PATCH 27/28] fix(dependencies): add bloodhound to netexec.spec, fixes #79 --- netexec.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/netexec.spec b/netexec.spec index 184be698..28bb2485 100644 --- a/netexec.spec +++ b/netexec.spec @@ -48,6 +48,7 @@ a = Analysis( 'lsassy.parser', 'lsassy.session', 'lsassy.impacketfile', + 'bloodhound', 'dns', 'dns.name', 'dns.resolver', From 92f9c8c51e2889088e2887b45a48c1a03b68aa98 Mon Sep 17 00:00:00 2001 From: Alexander Neff Date: Sun, 22 Oct 2023 07:32:11 -0400 Subject: [PATCH 28/28] Improve ssh key file output --- nxc/protocols/ssh.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index 0a61c8f1..b1919fb0 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -221,7 +221,10 @@ class ssh(connection): except Exception as e: if self.args.key_file: password = f"{process_secret(password)} (keyfile: {self.args.key_file})" - self.logger.fail(f"{username}:{password} {e}") + if "OpenSSH private key file checkints do not match" in str(e): + self.logger.fail(f"{username}:{password} - Could not decrypt key file, wrong password") + else: + self.logger.fail(f"{username}:{password} {e}") self.conn.close() return False else: