diff --git a/README.md b/README.md index 6fa60c9a..d0171dd8 100755 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Going forward, our intent is to maintain a community-driven and maintained proje You are on the **latest up-to-date** repository of the project NetExec (nxc) ! 🎉 -- 🚧 If you want to report a problem, open un [Issue](https://github.com/Pennyw0rth/NetExec/issues) +- 🚧 If you want to report a problem, open an [Issue](https://github.com/Pennyw0rth/NetExec/issues) - 🔀 If you want to contribute, open a [Pull Request](https://github.com/Pennyw0rth/NetExec/pulls) - 💬 If you want to discuss, open a [Discussion](https://github.com/Pennyw0rth/NetExec/discussions) @@ -37,6 +37,12 @@ See the project's [wiki](https://netexec.wiki/) (in development) for documentati # Installation Please see the installation instructions on the [wiki](https://netexec.wiki/getting-started/installation) (in development) +## Linux +``` +sudo apt install pipx git +pipx ensurepath +pipx install git+https://github.com/Pennyw0rth/NetExec +``` # Development Development guidelines and recommendations in development diff --git a/nxc/config.py b/nxc/config.py index ce9c688f..07f2e55b 100644 --- a/nxc/config.py +++ b/nxc/config.py @@ -43,5 +43,5 @@ if len(host_info_colors) != 4: # this should probably be put somewhere else, but if it's in the config helpers, there is a circular import def process_secret(text): - hidden = text[:reveal_chars_of_pwd] - return text if not audit_mode else hidden + audit_mode * 8 + reveal = text[:reveal_chars_of_pwd] + return text if not audit_mode else reveal + (audit_mode if len(audit_mode) > 1 else audit_mode * 8) diff --git a/nxc/connection.py b/nxc/connection.py index cc2d68bb..7a70dea8 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -87,8 +87,8 @@ class connection: self.port = self.args.port self.conn = None self.admin_privs = False - self.password = "" - self.username = "" + self.password = None + self.username = None self.kerberos = bool(self.args.kerberos or self.args.use_kcache or self.args.aesKey) self.aesKey = None if not self.args.aesKey else self.args.aesKey[0] self.kdcHost = None if not self.args.kdcHost else self.args.kdcHost diff --git a/nxc/database.py b/nxc/database.py new file mode 100644 index 00000000..3f93d7db --- /dev/null +++ b/nxc/database.py @@ -0,0 +1,97 @@ +import sys +import configparser +import shutil +from sqlalchemy import create_engine +from sqlite3 import connect +from os import mkdir +from os.path import exists +from os.path import join as path_join + +from nxc.loaders.protocolloader import ProtocolLoader +from nxc.paths import WORKSPACE_DIR + + +def create_db_engine(db_path): + return create_engine(f"sqlite:///{db_path}", isolation_level="AUTOCOMMIT", future=True) + + +def open_config(config_path): + try: + config = configparser.ConfigParser() + config.read(config_path) + except Exception as e: + print(f"[-] Error reading nxc.conf: {e}") + sys.exit(1) + return config + + +def get_workspace(config): + return config.get("nxc", "workspace") + + +def set_workspace(config_path, workspace_name): + config = open_config(config_path) + config.set("nxc", "workspace", workspace_name) + write_configfile(config, config_path) + print(f"[*] Workspace set to {workspace_name}") + + +def get_db(config): + return config.get("nxc", "last_used_db") + + +def write_configfile(config, config_path): + with open(config_path, "w") as configfile: + config.write(configfile) + + +def create_workspace(workspace_name, p_loader=None): + """ + Create a new workspace with the given name. + + Args: + ---- + workspace_name (str): The name of the workspace. + + Returns: + ------- + None + """ + if exists(path_join(WORKSPACE_DIR, workspace_name)): + print(f"[-] Workspace {workspace_name} already exists") + else: + print(f"[*] Creating {workspace_name} workspace") + mkdir(path_join(WORKSPACE_DIR, workspace_name)) + + if p_loader is None: + p_loader = ProtocolLoader() + protocols = p_loader.get_protocols() + + for protocol in protocols: + protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"]) + proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db") + + if not exists(proto_db_path): + print(f"[*] Initializing {protocol.upper()} protocol database") + conn = connect(proto_db_path) + c = conn.cursor() + + # try to prevent some weird sqlite I/O errors + c.execute("PRAGMA journal_mode = OFF") + c.execute("PRAGMA foreign_keys = 1") + + protocol_object.database.db_schema(c) + + # commit the changes and close everything off + conn.commit() + conn.close() + + +def delete_workspace(workspace_name): + shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name)) + print(f"[*] Workspace {workspace_name} deleted") + + +def initialize_db(): + if not exists(path_join(WORKSPACE_DIR, "default")): + create_workspace("default") \ No newline at end of file diff --git a/nxc/first_run.py b/nxc/first_run.py index e60979bc..c3b55f14 100755 --- a/nxc/first_run.py +++ b/nxc/first_run.py @@ -3,7 +3,7 @@ from os.path import exists from os.path import join as path_join import shutil from nxc.paths import NXC_PATH, CONFIG_PATH, TMP_PATH, DATA_PATH -from nxc.nxcdb import initialize_db +from nxc.database import initialize_db from nxc.logger import nxc_logger @@ -29,7 +29,7 @@ def first_run_setup(logger=nxc_logger): logger.display(f"Creating missing folder {folder}") mkdir(path_join(NXC_PATH, folder)) - initialize_db(logger) + initialize_db() if not exists(CONFIG_PATH): logger.display("Copying default configuration file") diff --git a/nxc/helpers/ntlm_parser.py b/nxc/helpers/ntlm_parser.py new file mode 100644 index 00000000..dd5913f8 --- /dev/null +++ b/nxc/helpers/ntlm_parser.py @@ -0,0 +1,34 @@ +# Original from here: https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py#L568 + +import struct + +from impacket import ntlm +from impacket.smb3 import WIN_VERSIONS +import contextlib + + +def parse_challenge(challange): + target_info = { + "hostname": None, + "domain": None, + "os_version": None + } + challange = ntlm.NTLMAuthChallenge(challange) + av_pairs = ntlm.AV_PAIRS(challange["TargetInfoFields"][:challange["TargetInfoFields_len"]]) + if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME] is not None: + with contextlib.suppress(Exception): + target_info["hostname"] = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le") + if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME] is not None: + with contextlib.suppress(Exception): + target_info["domain"] = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le") + if "Version" in challange.fields: + version = challange["Version"] + if len(version) >= 4: + major_version = version[0] + minor_version = version[1] + product_build = struct.unpack(" " + workspace) + print(f" * {colored(workspace, 'green')}") else: - print(workspace) + print(f" {workspace}") elif exists(path_join(WORKSPACE_DIR, line)): self.config.set("nxc", "workspace", line) - self.write_configfile() + write_configfile(self.config, self.config_path) self.workspace = line self.prompt = f"nxcdb ({line}) > " @@ -538,65 +518,49 @@ class NXCDBMenu(cmd.Cmd): Exits """ print_help(help_string) - - @staticmethod - def create_workspace(workspace_name, p_loader, protocols): - os.mkdir(path_join(WORKSPACE_DIR, workspace_name)) - - for protocol in protocols: - protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"]) - proto_db_path = path_join(WORKSPACE_DIR, workspace_name, f"{protocol}.db") - - if not exists(proto_db_path): - print(f"[*] Initializing {protocol.upper()} protocol database") - conn = connect(proto_db_path) - c = conn.cursor() - - # try to prevent some weird sqlite I/O errors - c.execute("PRAGMA journal_mode = OFF") - c.execute("PRAGMA foreign_keys = 1") - - protocol_object.database.db_schema(c) - - # commit the changes and close everything off - conn.commit() - conn.close() - - -def delete_workspace(workspace_name): - shutil.rmtree(path_join(WORKSPACE_DIR, workspace_name)) - - -def initialize_db(logger): - if not exists(path_join(WS_PATH, "default")): - logger.debug("Creating default workspace") - os.mkdir(path_join(WS_PATH, "default")) - - p_loader = ProtocolLoader() - protocols = p_loader.get_protocols() - for protocol in protocols: - protocol_object = p_loader.load_protocol(protocols[protocol]["dbpath"]) - proto_db_path = path_join(WS_PATH, "default", f"{protocol}.db") - - if not exists(proto_db_path): - logger.debug(f"Initializing {protocol.upper()} protocol database") - conn = connect(proto_db_path) - c = conn.cursor() - # try to prevent some weird sqlite I/O errors - c.execute("PRAGMA journal_mode = OFF") # could try setting to PERSIST if DB corruption starts occurring - c.execute("PRAGMA foreign_keys = 1") - # set a small timeout (5s) so if another thread is writing to the database, the entire program doesn't crash - c.execute("PRAGMA busy_timeout = 5000") - protocol_object.database.db_schema(c) - # commit the changes and close everything off - conn.commit() - conn.close() - + def main(): if not exists(CONFIG_PATH): print("[-] Unable to find config file") sys.exit(1) + + parser = argparse.ArgumentParser( + description="NXCDB is a database navigator for NXC", + ) + parser.add_argument( + "-gw", + "--get-workspace", + action="store_true", + help="get the current workspace", + ) + parser.add_argument( + "-cw", + "--create-workspace", + help="create a new workspace", + ) + parser.add_argument( + "-sw", + "--set-workspace", + help="set the current workspace", + ) + args = parser.parse_args() + + if args.create_workspace: + create_workspace(args.create_workspace) + sys.exit() + if args.set_workspace: + set_workspace(CONFIG_PATH, args.set_workspace) + sys.exit() + if args.get_workspace: + current_workspace = get_workspace(open_config(CONFIG_PATH)) + for workspace in listdir(path_join(WORKSPACE_DIR)): + if workspace == current_workspace: + print(f" * {colored(workspace, 'green')}") + else: + print(f" {workspace}") + sys.exit() + try: nxcdbnav = NXCDBMenu(CONFIG_PATH) nxcdbnav.cmdloop() diff --git a/nxc/paths.py b/nxc/paths.py index 5b16c191..5ebed0e8 100644 --- a/nxc/paths.py +++ b/nxc/paths.py @@ -8,7 +8,7 @@ if os.name == "nt": TMP_PATH = os.getenv("LOCALAPPDATA") + "\\Temp\\nxc_hosted" if hasattr(sys, "getandroidapilevel"): TMP_PATH = os.path.join("/data", "data", "com.termux", "files", "usr", "tmp", "nxc_hosted") -WS_PATH = os.path.join(NXC_PATH, "workspaces") + CERT_PATH = os.path.join(NXC_PATH, "nxc.pem") CONFIG_PATH = os.path.join(NXC_PATH, "nxc.conf") WORKSPACE_DIR = os.path.join(NXC_PATH, "workspaces") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7766f87d..586b9f48 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -399,7 +399,7 @@ class ldap(connection): return False except (KeyError, KerberosException, OSError) as e: self.logger.fail( - f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {e!s}", + f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {e!s}", color="red", ) return False @@ -442,21 +442,21 @@ class ldap(connection): except SessionError as e: error, desc = e.getErrorString() self.logger.fail( - f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error!s}", + f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error!s}", color="magenta" if error in ldap_error_status else "red", ) return False except Exception as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if error_code in ldap_error_status else "red", ) return False else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (kerb_pass if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8)} {error_code!s}", + f"{self.domain}\\{self.username}{' from ccache' if useCache else ':%s' % (process_secret(kerb_pass))} {error_code!s}", color="magenta" if error_code in ldap_error_status else "red", ) return False @@ -526,18 +526,18 @@ class ldap(connection): except Exception as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) return False except OSError as e: - self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") + self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") return False def hash_login(self, domain, username, ntlm_hash): @@ -619,18 +619,18 @@ class ldap(connection): except ldap_impacket.LDAPSessionError as e: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) else: error_code = str(e).split()[-2][:-1] self.logger.fail( - f"{self.domain}\\{self.username}:{nthash if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", + f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) return False except OSError as e: - self.logger.fail(f"{self.domain}\\{self.username}:{self.password if not self.config.get('nxc', 'audit_mode') else self.config.get('nxc', 'audit_mode') * 8} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") + self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") return False def create_smbv1_conn(self): diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 97daf0fc..1fc00f19 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -1,15 +1,19 @@ import os +import random +import socket +import contextlib from nxc.config import process_secret from nxc.connection import connection from nxc.connection import requires_admin from nxc.logger import NXCAdapter -from nxc.protocols.mssql.mssqlexec import MSSQLEXEC from nxc.helpers.bloodhound import add_user_bh +from nxc.helpers.ntlm_parser import parse_challenge from nxc.helpers.powershell import create_ps_command -from impacket import tds +from nxc.protocols.mssql.mssqlexec import MSSQLEXEC + +from impacket import tds, ntlm from impacket.krb5.ccache import CCache -from impacket.smbconnection import SMBConnection, SessionError from impacket.tds import ( SQLErrorException, TDS_LOGINACK_TOKEN, @@ -22,31 +26,20 @@ from impacket.tds import ( TDS_ENVCHANGE_CHARSET, TDS_ENVCHANGE_PACKETSIZE, ) -import contextlib class mssql(connection): def __init__(self, args, db, host): - self.mssql_instances = None + self.mssql_instances = [] self.domain = None self.server_os = None self.hash = None self.os_arch = None self.nthash = "" + self.is_mssql = False connection.__init__(self, args, db, host) - def proto_flow(self): - self.proto_logger() - if self.create_conn_obj(): - self.enum_host_info() - self.print_host_info() - if self.login(): - if hasattr(self.args, "module") and self.args.module: - self.call_modules() - else: - self.call_cmd_args() - def proto_logger(self): self.logger = NXCAdapter( extra={ @@ -57,83 +50,98 @@ class mssql(connection): } ) - def enum_host_info(self): - # this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363 - try: # noqa: SIM105 - # Probably a better way of doing this, grab our IP from the socket - self.local_ip = str(self.conn.socket).split()[2].split("=")[1].split(":")[0] - except Exception: - pass - - if self.args.no_smb: - self.domain = self.args.domain - else: - try: - smb_conn = SMBConnection(self.host, self.host, None) - try: - smb_conn.login("", "") - except SessionError as e: - if "STATUS_ACCESS_DENIED" in e.getErrorString(): - pass - - self.domain = smb_conn.getServerDNSDomainName() - self.hostname = smb_conn.getServerName() - self.server_os = smb_conn.getServerOS() - self.logger.extra["hostname"] = self.hostname - - with contextlib.suppress(Exception): - smb_conn.logoff() - - if self.args.domain: - self.domain = self.args.domain - - if self.args.local_auth: - self.domain = self.hostname - except Exception as e: - self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag") - - self.mssql_instances = self.conn.getInstances(0) - self.db.add_host( - self.host, - self.hostname, - self.domain, - self.server_os, - len(self.mssql_instances), - ) - - with contextlib.suppress(Exception): - self.conn.disconnect() - - def print_host_info(self): - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") - # if len(self.mssql_instances) > 0: - # for i, instance in enumerate(self.mssql_instances): - # for key in instance.keys(): - def create_conn_obj(self): try: self.conn = tds.MSSQL(self.host, self.port) - self.conn.connect() - except OSError as e: - self.logger.debug(f"Error connecting to MSSQL: {e}") + # Default has not timeout option in tds.MSSQL.connect() function, let rewrite it. + af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0] + sock = socket.socket(af, socktype, proto) + sock.settimeout(self.args.mssql_timeout) + sock.connect(sa) + self.conn.socket = sock + if not self.is_mssql: + self.conn.preLogin() + except Exception as e: + self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}") return False - return True + else: + self.is_mssql = True + return True + + def reconnect_mssql(func): + def wrapper(self, *args, **kwargs): + with contextlib.suppress(Exception): + self.conn.disconnect() + # When using ccache file, we must need to set target host to hostname when creating connection object. + if self.kerberos: + self.host = self.hostname + self.create_conn_obj() + return func(self, *args, **kwargs) + return wrapper def check_if_admin(self): + self.admin_privs = False try: results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')") is_admin = int(results[0][""]) except Exception as e: self.logger.fail(f"Error querying for sysadmin role: {e}") - return False - - if is_admin: - self.admin_privs = True - self.logger.debug("User is admin") else: + if is_admin: + self.admin_privs = True + + @reconnect_mssql + def enum_host_info(self): + challenge = None + try: + login = tds.TDS_LOGIN() + login["HostName"] = "" + login["AppName"] = "" + login["ServerName"] = self.conn.server.encode("utf-16le") + login["CltIntName"] = login["AppName"] + login["ClientPID"] = random.randint(0, 1024) + login["PacketSize"] = self.conn.packetSize + login["OptionFlags2"] = tds.TDS_INIT_LANG_FATAL | tds.TDS_ODBC_ON | tds.TDS_INTEGRATED_SECURITY_ON + + # NTLMSSP Negotiate + auth = ntlm.getNTLMSSPType1("", "") + login["SSPI"] = auth.getData() + login["Length"] = len(login.getData()) + + # Get number of mssql instance + self.mssql_instances = self.conn.getInstances(0) + + # Send the NTLMSSP Negotiate or SQL Auth Packet + self.conn.sendTDS(tds.TDS_LOGIN7, login.getData()) + + tdsx = self.conn.recvTDS() + challenge = tdsx["Data"][3:] + self.logger.info(f"NTLM challenge: {challenge!s}") + except Exception as e: + self.logger.info(f"Failed to receive NTLM challenge, reason: {e!s}") return False + else: + ntlm_info = parse_challenge(challenge) + self.domain = ntlm_info["domain"] + self.hostname = ntlm_info["hostname"] + self.server_os = ntlm_info["os_version"] + self.logger.extra["hostname"] = self.hostname + self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances),) + + if self.args.domain: + self.domain = self.args.domain + + if self.args.local_auth: + self.domain = self.hostname + + if self.domain is None: + self.domain = "" + + def print_host_info(self): + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") return True + @reconnect_mssql def kerberos_login( self, domain, @@ -144,146 +152,126 @@ class mssql(connection): kdcHost="", useCache=False, ): - with contextlib.suppress(Exception): - self.conn.disconnect() - self.create_conn_obj() - + self.username = username + self.password = password + self.domain = domain + self.nthash = "" hashes = None - if ntlm_hash != "": + if ntlm_hash: if ntlm_hash.find(":") != -1: - hashes = ntlm_hash - ntlm_hash.split(":")[1] + self.nthash = ntlm_hash.split(":")[1] + hashes = f":{self.nthash}" else: - # only nt hash - hashes = f":{ntlm_hash}" + self.nthash = ntlm_hash + hashes = f":{self.nthash}" kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else "" + + if useCache and kerb_pass == "": + ccache = CCache.loadFile(os.getenv("KRB5CCNAME")) + username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0] + self.username = username + + used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" + try: res = self.conn.kerberosLogin( None, - username, - password, - domain, + self.username, + self.password, + self.domain, hashes, aesKey, kdcHost=kdcHost, useCache=useCache, ) if res is not True: - self.conn.printReplies() - return False - - self.password = password - if username == "" and useCache: - ccache = CCache.loadFile(os.getenv("KRB5CCNAME")) - principal = ccache.principal.toPrincipal() - self.username = principal.components[0] - username = principal.components[0] - else: - self.username = username - self.domain = domain + raise self.check_if_admin() - - used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" - domain = f"{domain}\\" if not self.args.local_auth else "" - - self.logger.success(f"{domain}{username}{used_ccache} {self.mark_pwned()}") + self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}") if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) if self.admin_privs: - add_user_bh(f"{self.hostname}$", domain, self.logger, self.config) - return True - except Exception as e: - used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" - domain = f"{domain}\\" if not self.args.local_auth else "" - self.logger.fail(f"{domain}\\{username}{used_ccache} {e}") - return False - - def plaintext_login(self, domain, username, password): - with contextlib.suppress(Exception): - self.conn.disconnect() - self.create_conn_obj() - - try: - # this is to prevent a decoding issue in impacket/ntlm.py:617 where it attempts to decode the domain - if not domain: - domain = "" - res = self.conn.login(None, username, password, domain, None, not self.args.local_auth) - if res is not True: - self.handle_mssql_reply() - return False - - self.password = password - self.username = username - self.domain = domain - self.check_if_admin() - self.db.add_credential("plaintext", domain, username, password) - - if self.admin_privs: - self.db.add_admin_user("plaintext", domain, username, password, self.host) - add_user_bh(f"{self.hostname}$", domain, self.logger, self.config) - - domain = f"{domain}\\" if not self.args.local_auth else "" - out = f"{domain}{username}:{process_secret(password)} {self.mark_pwned()}" - self.logger.success(out) - if not self.args.local_auth: - add_user_bh(self.username, self.domain, self.logger, self.config) + add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config) return True except BrokenPipeError: self.logger.fail("Broken Pipe Error while attempting to login") return False - except Exception as e: - self.logger.fail(f"{domain}\\{username}:{process_secret(password)}") - self.logger.exception(e) + except Exception: + error_msg = self.handle_mssql_reply() + self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, kerb_pass, error_msg if error_msg else "")) return False + @reconnect_mssql + def plaintext_login(self, domain, username, password): + self.password = password + self.username = username + self.domain = domain + + try: + res = self.conn.login( + None, + self.username, + self.password, + self.domain, + None, + not self.args.local_auth, + ) + if res is not True: + raise + self.check_if_admin() + out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}" + self.logger.success(out) + if not self.args.local_auth: + add_user_bh(self.username, self.domain, self.logger, self.config) + if self.admin_privs: + add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config) + return True + except BrokenPipeError: + self.logger.fail("Broken Pipe Error while attempting to login") + return False + except Exception: + error_msg = self.handle_mssql_reply() + self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.password), error_msg if error_msg else "")) + return False + + @reconnect_mssql def hash_login(self, domain, username, ntlm_hash): - lmhash = "" - nthash = "" - - # This checks to see if we didn't provide the LM Hash + self.username = username + self.domain = domain + self.lmhash = "" + self.nthash = "" + if ntlm_hash.find(":") != -1: - lmhash, nthash = ntlm_hash.split(":") + self.lmhash, self.nthash = ntlm_hash.split(":") else: - nthash = ntlm_hash - - with contextlib.suppress(Exception): - self.conn.disconnect() - self.create_conn_obj() + self.nthash = ntlm_hash try: res = self.conn.login( None, - username, + self.username, "", - domain, - ":" + nthash if not lmhash else ntlm_hash, + self.domain, + f"{self.lmhash}:{self.nthash}", not self.args.local_auth, ) if res is not True: - self.conn.printReplies() - return False - - self.hash = ntlm_hash - self.username = username - self.domain = domain + raise self.check_if_admin() - self.db.add_credential("hash", domain, username, ntlm_hash) - - if self.admin_privs: - self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host) - add_user_bh(f"{self.hostname}$", domain, self.logger, self.config) - - out = f"{domain}\\{username} {process_secret(ntlm_hash)} {self.mark_pwned()}" + out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}" self.logger.success(out) if not self.args.local_auth: add_user_bh(self.username, self.domain, self.logger, self.config) + if self.admin_privs: + add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config) return True except BrokenPipeError: self.logger.fail("Broken Pipe Error while attempting to login") return False - except Exception as e: - self.logger.fail(f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}") + except Exception: + error_msg = self.handle_mssql_reply() + self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.nthash), error_msg if error_msg else "")) return False def mssql_query(self): @@ -306,47 +294,39 @@ class mssql(connection): else: self.logger.fail("Unexpected output") except Exception as e: - self.logger.exception(e) + self.logger.exception(f"Failed to excuted MSSQL query, reason: {e}") return None - return raw_output @requires_admin - def execute(self, payload=None, print_output=False): + def execute(self, payload=None, get_output=False): if not payload and self.args.execute: payload = self.args.execute - self.logger.info(f"Command to execute:\n{payload}") + if not self.args.no_output: + get_output = True + + self.logger.info(f"Command to execute: {payload}") try: - exec_method = MSSQLEXEC(self.conn) - raw_output = exec_method.execute(payload, print_output) - self.logger.info("Executed command via mssqlexec") - self.logger.debug(f"Raw output: {raw_output}") + exec_method = MSSQLEXEC(self.conn, self.logger) + raw_output = exec_method.execute(payload, get_output) except Exception as e: - self.logger.exception(e) - return None - - if hasattr(self, "server"): - self.server.track_host(self.host) - - if self.args.execute or self.args.ps_execute: + self.logger.fail(f"Execute command failed, error: {e!s}") + return False + else: self.logger.success("Executed command via mssqlexec") - if self.args.no_output: - self.logger.debug("Output set to disabled") - else: + if raw_output: for line in raw_output: self.logger.highlight(line) - - return raw_output + return raw_output @requires_admin def ps_execute( self, payload=None, get_output=False, - methods=None, force_ps32=False, - dont_obfs=True, + dont_obfs=False, ): if not payload and self.args.ps_execute: payload = self.args.ps_execute @@ -364,7 +344,7 @@ class mssql(connection): try: data = f.read() self.logger.display(f"Size is {len(data)} bytes") - exec_method = MSSQLEXEC(self.conn) + exec_method = MSSQLEXEC(self.conn, self.logger) exec_method.put_file(data, self.args.put_file[1]) if exec_method.file_exists(self.args.put_file[1]): self.logger.success("File has been uploaded on the remote machine") @@ -374,13 +354,13 @@ class mssql(connection): self.logger.fail(f"Error during upload : {e}") @requires_admin - def get_file(self): + def get_file(self): remote_path = self.args.get_file[0] download_path = self.args.get_file[1] self.logger.display(f'Copying "{remote_path}" to "{download_path}"') try: - exec_method = MSSQLEXEC(self.conn) + exec_method = MSSQLEXEC(self.conn, self.logger) exec_method.get_file(self.args.get_file[0], self.args.get_file[1]) self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') except Exception as e: @@ -394,13 +374,13 @@ class mssql(connection): for keys in self.conn.replies: for _i, key in enumerate(self.conn.replies[keys]): if key["TokenType"] == TDS_ERROR_TOKEN: - error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}" + error_msg = f"({key['MsgText'].decode('utf-16le')} Please try again with or without '--local-auth')" self.conn.lastError = SQLErrorException(f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}") - self.logger.fail(error) + return error_msg elif key["TokenType"] == TDS_INFO_TOKEN: - self.logger.display(f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}") + return f"({key['MsgText'].decode('utf-16le')})" elif key["TokenType"] == TDS_LOGINACK_TOKEN: - self.logger.display(f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) ") + return f"(ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) )" elif key["TokenType"] == TDS_ENVCHANGE_TOKEN and key["Type"] in ( TDS_ENVCHANGE_DATABASE, TDS_ENVCHANGE_LANGUAGE, @@ -422,4 +402,4 @@ class mssql(connection): _type = "PACKETSIZE" else: _type = f"{key['Type']:d}" - self.logger.display(f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}") + return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})" diff --git a/nxc/protocols/mssql/mssqlexec.py b/nxc/protocols/mssql/mssqlexec.py index a2081a99..53255ecc 100755 --- a/nxc/protocols/mssql/mssqlexec.py +++ b/nxc/protocols/mssql/mssqlexec.py @@ -1,38 +1,38 @@ import binascii -from nxc.logger import nxc_logger class MSSQLEXEC: - def __init__(self, connection): + def __init__(self, connection, logger): self.mssql_conn = connection - self.outputBuffer = "" + self.logger = logger + self.outputBuffer = [] def execute(self, command, output=False): - command_output = [] try: self.enable_xp_cmdshell() except Exception as e: - nxc_logger.error(f"Error when attempting to enable x_cmdshell: {e}") + self.logger.error(f"Error when attempting to enable x_cmdshell: {e}") + try: result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'") - nxc_logger.debug(f"SQL Query Result: {result}") - for row in result: - if row["output"] == "NULL": - continue - command_output.append(row["output"]) except Exception as e: - nxc_logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}") + self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}") - if output: - nxc_logger.debug("Output is enabled") - for row in command_output: - nxc_logger.debug(row) - # if len(self.outputBuffer): try: self.disable_xp_cmdshell() except Exception as e: - nxc_logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}") - return command_output + self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}") + + if output: + self.logger.debug(f"SQL Query Result: {result}") + for row in result: + if row["output"] == "NULL": + continue + self.outputBuffer.append(row["output"]) + else: + self.logger.info("Output set to disabled") + + return self.outputBuffer def enable_xp_cmdshell(self): self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;") @@ -53,7 +53,7 @@ class MSSQLEXEC: self.mssql_conn.sql_query(f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;") self.disable_ole() except Exception as e: - nxc_logger.debug(f"Error uploading via mssqlexec: {e}") + self.logger.debug(f"Error uploading via mssqlexec: {e}") def file_exists(self, remote): try: @@ -71,4 +71,4 @@ class MSSQLEXEC: f.write(binascii.unhexlify(data)) except Exception as e: - nxc_logger.debug(f"Error downloading via mssqlexec: {e}") + self.logger.debug(f"Error downloading via mssqlexec: {e}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 4ee7fcc8..8a832bc2 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -1,17 +1,13 @@ -from argparse import _StoreTrueAction - - def proto_args(parser, std_parser, module_parser): mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=[std_parser, module_parser]) mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port (default: 1433)") + mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout, default is %(default)s seconds", type=int, default=5) mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB") - no_smb_arg = mssql_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection") dgroup = mssql_parser.add_mutually_exclusive_group() - domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name") + dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name") dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target") - no_smb_arg.make_required = [domain_arg] cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands") cgroup.add_argument("--force-ps32", action="store_true", help="force the PowerShell command to run in a 32-bit process") @@ -25,22 +21,7 @@ def proto_args(parser, std_parser, module_parser): psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts") tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files") - tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt") - tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt") + tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt") + tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt") - return parser - - -def get_conditional_action(baseAction): - class ConditionalAction(baseAction): - def __init__(self, option_strings, dest, **kwargs): - x = kwargs.pop("make_required", []) - super().__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().__call__(parser, namespace, values, option_string) - - return ConditionalAction + return parser \ No newline at end of file diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 386468ec..730accb3 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -736,17 +736,23 @@ class smb(connection): share_info["access"].append("READ") except SessionError as e: error = get_error_string(e) - self.logger.debug(f"Error checking READ access on share: {error}") + self.logger.debug(f"Error checking READ access on share {share_name}: {error}") if not self.args.no_write_check: try: self.conn.createDirectory(share_name, temp_dir) - self.conn.deleteDirectory(share_name, temp_dir) write = True share_info["access"].append("WRITE") except SessionError as e: error = get_error_string(e) - self.logger.debug(f"Error checking WRITE access on share: {error}") + self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}") + + if write: + try: + self.conn.deleteDirectory(share_name, temp_dir) + except SessionError as e: + error = get_error_string(e) + self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}") permissions.append(share_info) @@ -1258,12 +1264,11 @@ class smb(connection): os.remove(download_path) def enable_remoteops(self): - if self.remote_ops is not None and self.bootkey is not None: - return try: self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost) self.remote_ops.enableRegistry() - self.bootkey = self.remote_ops.getBootKey() + if self.bootkey is None: + self.bootkey = self.remote_ops.getBootKey() except Exception as e: self.logger.fail(f"RemoteOperations failed: {e}") diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 2b37b802..fc832482 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -1,7 +1,7 @@ import os +import base64 import requests import urllib3 -import contextlib import logging import xml.etree.ElementTree as ET @@ -10,13 +10,13 @@ from datetime import datetime from pypsrp.wsman import NAMESPACES from pypsrp.client import Client -from impacket.smbconnection import SMBConnection from impacket.examples.secretsdump import LocalOperations, LSASecrets, SAMHashes from nxc.config import process_secret from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.misc import gen_random_string +from nxc.helpers.ntlm_parser import parse_challenge from nxc.logger import NXCAdapter @@ -33,58 +33,33 @@ class winrm(connection): self.lmhash = "" self.nthash = "" self.ssl = False - self.auth_type = None + self.challenge_header = None connection.__init__(self, args, db, host) def proto_logger(self): - # Reason why default is SMB/445, because default is enumerate over SMB. - # For more details, please check the function "print_host_info" + # For more details, please check the function "print_host_info" logging.getLogger("pypsrp").disabled = True logging.getLogger("pypsrp.wsman").disabled = True self.logger = NXCAdapter( extra={ - "protocol": "SMB", + "protocol": "WINRM", "host": self.host, - "port": "445", + "port": "5985", "hostname": self.hostname, } ) def enum_host_info(self): - # smb no open, specify the domain - if self.args.no_smb: - self.domain = self.args.domain - else: - try: - smb_conn = SMBConnection(self.host, self.host, None, timeout=5) - no_ntlm = False - except Exception as e: - self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag") - else: - try: - smb_conn.login("", "") - except BrokenPipeError: - self.logger.fail("Broken Pipe Error while attempting to login") - except Exception as e: - if "STATUS_NOT_SUPPORTED" in str(e): - # no ntlm supported - no_ntlm = True + ntlm_info = parse_challenge(base64.b64decode(self.challenge_header.split(" ")[1].replace(",", ""))) + self.domain = ntlm_info["domain"] + self.hostname = ntlm_info["hostname"] + self.server_os = ntlm_info["os_version"] + self.logger.extra["hostname"] = self.hostname - self.domain = smb_conn.getServerDNSDomainName() if not no_ntlm else self.args.domain - self.hostname = smb_conn.getServerName() if not no_ntlm else self.host - self.server_os = smb_conn.getServerOS() - if isinstance(self.server_os.lower(), bytes): - self.server_os = self.server_os.decode("utf-8") + self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}") - self.logger.extra["hostname"] = self.hostname - - self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}") - - with contextlib.suppress(Exception): - smb_conn.logoff() - - self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os) + self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os) if self.args.domain: self.domain = self.args.domain @@ -98,16 +73,10 @@ class winrm(connection): self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")) def print_host_info(self): - if self.args.no_smb: - self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM" - self.logger.extra["port"] = self.port - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") - else: - self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") - self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM" - self.logger.extra["port"] = self.port - - self.logger.info(f"Connection information: {self.endpoint} (auth type:{self.auth_type}) (domain:{self.domain if self.args.domain else ''})") + self.logger.extra["protocol"] = "WINRM-SSL" if self.ssl else "WINRM" + self.logger.extra["port"] = self.port + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") + return True def create_conn_obj(self): @@ -117,6 +86,14 @@ class winrm(connection): endpoints = {} + headers = { + "Content-Length": "0", + "Keep-Alive": "true", + "Content-Type": "application/soap+xml;charset=UTF-8", + "User-Agent": "Microsoft WinRM Client", + "Authorization": "Negotiate TlRMTVNTUAABAAAAB4IIogAAAAAAAAAAAAAAAAAAAAAGAbEdAAAADw==" + } + for protocol in self.args.check_proto: endpoints[protocol] = {} endpoints[protocol]["port"] = self.port[self.args.check_proto.index(protocol)] if len(self.port) == 2 else self.port[0] @@ -131,9 +108,12 @@ class winrm(connection): self.port = endpoints[protocol]["port"] try: self.logger.debug(f"Requesting URL: {endpoints[protocol]['url']}") - res = requests.post(endpoints[protocol]["url"], verify=False, timeout=self.args.http_timeout) + res = requests.post(endpoints[protocol]["url"], headers=headers, verify=False, timeout=self.args.http_timeout) self.logger.debug(f"Received response code: {res.status_code}") - self.auth_type = res.headers["WWW-Authenticate"] if "WWW-Authenticate" in res.headers else "NOAUTH" + self.challenge_header = res.headers["WWW-Authenticate"] + if (not self.challenge_header) or ("Negotiate" not in self.challenge_header): + self.logger.info('Failed to get NTLM challenge from target "/wsman" endpoint, maybe isn\'t winrm service.') + return False self.endpoint = endpoints[protocol]["url"] self.ssl = endpoints[protocol]["ssl"] return True diff --git a/nxc/protocols/winrm/proto_args.py b/nxc/protocols/winrm/proto_args.py index 0c896aee..ca4e81a5 100644 --- a/nxc/protocols/winrm/proto_args.py +++ b/nxc/protocols/winrm/proto_args.py @@ -1,6 +1,3 @@ -from argparse import _StoreTrueAction - - def proto_args(parser, std_parser, module_parser): winrm_parser = parser.add_parser("winrm", help="own stuff using WINRM", parents=[std_parser, module_parser]) winrm_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") @@ -9,12 +6,10 @@ def proto_args(parser, std_parser, module_parser): winrm_parser.add_argument("--check-proto", nargs="+", default=["http", "https"], help="Choose what prorocol you want to check, default is %(default)s, format: 'http https'(with space separated) or 'single-protocol'") winrm_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator") winrm_parser.add_argument("--http-timeout", dest="http_timeout", type=int, default=10, help="HTTP timeout for WinRM connections") - no_smb_arg = winrm_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection") dgroup = winrm_parser.add_mutually_exclusive_group() - domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to") + dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to") dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target") - no_smb_arg.make_required = [domain_arg] cgroup = winrm_parser.add_argument_group("Credential Gathering", "Options for gathering credentials") cgroup.add_argument("--dump-method", action="store", default="cmd", choices={"cmd", "powershell"}, help="Select shell type in hashes dump") @@ -29,18 +24,3 @@ def proto_args(parser, std_parser, module_parser): cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command") return parser - - -def get_conditional_action(baseAction): - class ConditionalAction(baseAction): - def __init__(self, option_strings, dest, **kwargs): - x = kwargs.pop("make_required", []) - super().__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().__call__(parser, namespace, values, option_string) - - return ConditionalAction \ No newline at end of file diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 0728e514..87f53999 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -1,10 +1,10 @@ import os -import struct import logging from io import StringIO -from six import indexbytes from datetime import datetime + +from nxc.helpers.ntlm_parser import parse_challenge from nxc.config import process_secret from nxc.connection import connection, dcom_FirewallChecker, requires_admin from nxc.logger import NXCAdapter @@ -18,7 +18,6 @@ from impacket.dcerpc.v5 import transport, epm from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, MSRPC_BIND, MSRPCBind, CtxItem, MSRPCHeader, SEC_TRAILER, MSRPCBindAck from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login -import contextlib MSRPC_UUID_PORTMAP = uuidtup_to_bin(("E1AF8308-5D1F-11C9-91A4-08002B14A0FA", "3.0")) @@ -86,7 +85,6 @@ class wmi(connection): def enum_host_info(self): # All code pick from DumpNTLNInfo.py # https://github.com/fortra/impacket/blob/master/examples/DumpNTLMInfo.py - ntlmChallenge = None bind = MSRPCBind() item = CtxItem() @@ -123,39 +121,19 @@ class wmi(connection): if buffer != 0: response = MSRPCHeader(buffer) bindResp = MSRPCBindAck(response.getData()) - - ntlmChallenge = ntlm.NTLMAuthChallenge(bindResp["auth_data"]) - - if ntlmChallenge["TargetInfoFields_len"] > 0: - av_pairs = ntlm.AV_PAIRS(ntlmChallenge["TargetInfoFields"][: ntlmChallenge["TargetInfoFields_len"]]) - if av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1] is not None: - try: - self.hostname = av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1].decode("utf-16le") - except Exception: - self.hostname = self.host - if av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1] is not None: - try: - self.domain = av_pairs[ntlm.NTLMSSP_AV_DNS_DOMAINNAME][1].decode("utf-16le") - except Exception: - self.domain = self.args.domain - if av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1] is not None: - with contextlib.suppress(Exception): - self.fqdn = av_pairs[ntlm.NTLMSSP_AV_DNS_HOSTNAME][1].decode("utf-16le") - if "Version" in ntlmChallenge.fields: - version = ntlmChallenge["Version"] - if len(version) >= 4: - self.server_os = "Windows NT %d.%d Build %d" % (indexbytes(version, 0), indexbytes(version, 1), struct.unpack("