diff --git a/nxc/helpers/ntlm_parser.py b/nxc/helpers/ntlm_parser.py new file mode 100644 index 00000000..671c3b19 --- /dev/null +++ b/nxc/helpers/ntlm_parser.py @@ -0,0 +1,107 @@ +# Original from here: https://github.com/nopfor/ntlm_challenger + +import datetime + +from impacket.smb3 import WIN_VERSIONS + + +def decoder(byte_string, decode_type): + if decode_type == "byte": + return byte_string.decode("UTF-8").replace("\x00", "") + else: + return int.from_bytes(byte_string, "little") + + +def parse_version(version_bytes): + major_version = version_bytes[0] + minor_version = version_bytes[1] + product_build = decoder(version_bytes[2:4], "int") + if product_build in WIN_VERSIONS: + return f"{WIN_VERSIONS[product_build]} Build {product_build}" + else: + return f"Windows {major_version}.{minor_version} Build {product_build}" + + +def parse_target_info(target_info_bytes): + MsvAvEOL = 0x0000 + MsvAvNbComputerName = 0x0001 + MsvAvNbDomainName = 0x0002 + MsvAvDnsComputerName = 0x0003 + MsvAvDnsDomainName = 0x0004 + MsvAvDnsTreeName = 0x0005 + MsvAvFlags = 0x0006 + MsvAvTimestamp = 0x0007 + MsvAvSingleHost = 0x0008 + MsvAvTargetName = 0x0009 + MsvAvChannelBindings = 0x000A + + target_info = { + "MsvAvNbComputerName": None, + "MsvAvDnsDomainName": None, + } + info_offset = 0 + + while info_offset < len(target_info_bytes): + av_id = decoder(target_info_bytes[info_offset:info_offset + 2], "int") + av_len = decoder(target_info_bytes[info_offset + 2:info_offset + 4], "int") + av_value = target_info_bytes[info_offset + 4:info_offset + 4 + av_len] + + info_offset = info_offset + 4 + av_len + + if av_id == MsvAvEOL: + pass + elif av_id == MsvAvNbComputerName: + target_info["MsvAvNbComputerName"] = decoder(av_value, "byte") + elif av_id == MsvAvNbDomainName: + target_info["MsvAvNbDomainName"] = decoder(av_value, "byte") + elif av_id == MsvAvDnsComputerName: + target_info["MsvAvDnsComputerName"] = decoder(av_value, "byte") + elif av_id == MsvAvDnsDomainName: + target_info["MsvAvDnsDomainName"] = decoder(av_value, "byte") + elif av_id == MsvAvDnsTreeName: + target_info["MsvAvDnsTreeName"] = decoder(av_value, "byte") + elif av_id == MsvAvFlags: + pass + elif av_id == MsvAvTimestamp: + filetime = decoder(av_value, "int") + microseconds = (filetime - 116444736000000000) / 10 + time = datetime.datetime(1970, 1, 1) + datetime.timedelta(microseconds=microseconds) + target_info["MsvAvTimestamp"] = time.strftime("%b %d, %Y %H:%M:%S.%f") + elif av_id == MsvAvSingleHost: + target_info["MsvAvSingleHost"] = decoder(av_value, "byte") + elif av_id == MsvAvTargetName: + target_info["MsvAvTargetName"] = decoder(av_value, "byte") + elif av_id == MsvAvChannelBindings: + target_info["MsvAvChannelBindings"] = av_value + return target_info + + +def parse_challenge(challenge_message): + # TargetNameFields + target_name_fields = challenge_message[12:20] + target_name_len = decoder(target_name_fields[0:2], "int") + target_name_offset = decoder(target_name_fields[4:8], "int") + + # TargetInfoFields + target_info_fields = challenge_message[40:48] + target_info_len = decoder(target_info_fields[0:2], "int") + target_info_offset = decoder(target_info_fields[4:8], "int") + + # Version + version = None + version_bytes = challenge_message[48:56] + version = parse_version(version_bytes) + + # TargetName + target_name = challenge_message[target_name_offset:target_name_offset + target_name_len] + target_name = decoder(target_name, "byte") + + # TargetInfo + target_info_bytes = challenge_message[target_info_offset:target_info_offset + target_info_len] + target_info = parse_target_info(target_info_bytes) + + return { + "target_name": target_name, + "version": version, + "target_info": target_info + } diff --git a/nxc/modules/enum_ca.py b/nxc/modules/enum_ca.py new file mode 100644 index 00000000..a9571aec --- /dev/null +++ b/nxc/modules/enum_ca.py @@ -0,0 +1,106 @@ +from impacket.dcerpc.v5 import transport, epm +from impacket.http import AUTH_NTLM +from impacket.dcerpc.v5.rpch import RPC_PROXY_INVALID_RPC_PORT_ERR, \ + RPC_PROXY_CONN_A1_0X6BA_ERR, RPC_PROXY_CONN_A1_404_ERR, \ + RPC_PROXY_RPC_OUT_DATA_404_ERR +from impacket import uuid +import requests + + +class NXCModule: + """ + ------- + Module by @0xjbb, original code from Impacket rpcdump.py + """ + KNOWN_PROTOCOLS = { + 135: {"bindstr": r"ncacn_ip_tcp:%s[135]"}, + 139: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"}, + 443: {"bindstr": r"ncacn_http:[593,RpcProxy=%s:443]"}, + 445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"}, + 593: {"bindstr": r"ncacn_http:%s"} + } + + name = "enum_ca" + description = "Anonymously uses RPC endpoints to hunt for ADCS CAs" + supported_protocols = ["smb"] # Example: ['smb', 'mssql'] + opsec_safe = True # Does the module touch disk? + multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time? + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + + def options(self, context, module_options): + pass + + def on_login(self, context, connection): + self.__username = connection.username + self.__password = connection.password + self.__domain = connection.domain + self.__lmhash = "" + self.__nthash = "" + self.__port = 135. + self.__stringbinding = "" + + if context.hash and ":" in context.hash[0]: + hashList = context.hash[0].split(":") + self.__nthash = hashList[-1] + self.__lmhash = hashList[0] + elif context.hash and ":" not in context.hash[0]: + self.__nthash = context.hash[0] + self.__lmhash = "00000000000000000000000000000000" + + self.__stringbinding = self.KNOWN_PROTOCOLS[self.__port]["bindstr"] % connection.host + context.log.debug(f"StringBinding {self.__stringbinding}") + + rpctransport = transport.DCERPCTransportFactory(self.__stringbinding) + + if self.__port in [139, 445]: + # Setting credentials for SMB + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + rpctransport.setRemoteHost(connection.host) + rpctransport.set_dport(self.__port) + elif self.__port in [443]: + # Setting credentials only for RPC Proxy, but not for the MSRPC level + rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, self.__nthash) + rpctransport.set_auth_type(AUTH_NTLM) + else: + pass + + try: + entries = self.__fetchList(rpctransport) + except Exception as e: + error_text = f"Protocol failed: {e}" + context.log.fail(error_text) + + if RPC_PROXY_INVALID_RPC_PORT_ERR in error_text or \ + RPC_PROXY_RPC_OUT_DATA_404_ERR in error_text or \ + RPC_PROXY_CONN_A1_404_ERR in error_text or \ + RPC_PROXY_CONN_A1_0X6BA_ERR in error_text: + context.log.fail("This usually means the target does not allow " + "to connect to its epmapper using RpcProxy.") + return + for entry in entries: + tmpUUID = str(entry["tower"]["Floors"][0]) + + if uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18] in epm.KNOWN_UUIDS: + exename = epm.KNOWN_UUIDS[uuid.uuidtup_to_bin(uuid.string_to_uuidtup(tmpUUID))[:18]] + context.log.debug("EXEs %s" % exename) + if exename == "certsrv.exe": + context.log.highlight("Active Directory Certificate Services Found.") + url = f"http://{connection.host}/certsrv/certfnsh.asp" + context.log.highlight(url) + try: + response = requests.get(url, timeout=5) + if response.status_code == 401 and "WWW-Authenticate" in response.headers and "ntlm" in response.headers["WWW-Authenticate"].lower(): + context.log.highlight("Web enrollment found on HTTP (ESC8).") + except requests.RequestException as e: + context.log.debug(e) + return + + def __fetchList(self, rpctransport): + dce = rpctransport.get_dce_rpc() + dce.connect() + resp = epm.hept_lookup(None, dce=dce) + dce.disconnect() + return resp 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/winrm.py b/nxc/protocols/winrm.py index 2b37b802..9a094557 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["target_info"]["MsvAvDnsDomainName"] + self.hostname = ntlm_info["target_info"]["MsvAvNbComputerName"] + self.server_os = ntlm_info["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