diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1ffd2a82..01472c95 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,7 @@ If applicable, add screenshots to help explain your problem. **NetExec info** - OS: [e.g. Kali] - - Version of nxc [e.g. v1.5.2] + - Version of nxc: [e.g. v1.5.2] - Installed from: apt/github/pip/docker/...? Please try with latest release before openning an issue **Additional context** diff --git a/nxc/connection.py b/nxc/connection.py index 6c1eecdf..6616bb4c 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -1,12 +1,10 @@ import random -import socket -from socket import AF_INET, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME -from socket import getaddrinfo from os.path import isfile from threading import BoundedSemaphore from functools import wraps from time import sleep from ipaddress import ip_address +from socket import AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME, getaddrinfo from nxc.config import pwned_label from nxc.helpers.logger import highlight @@ -22,15 +20,22 @@ user_failed_logins = {} def gethost_addrinfo(hostname): - try: - for res in getaddrinfo(hostname, None, AF_INET6, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME): - af, socktype, proto, canonname, sa = res - host = canonname if ip_address(sa[0]).is_link_local else sa[0] - except socket.gaierror: - for res in getaddrinfo(hostname, None, AF_INET, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME): - af, socktype, proto, canonname, sa = res - host = sa[0] if sa[0] else canonname - return host + is_ipv6 = False + is_link_local_ipv6 = False + address_info = {"AF_INET6": "", "AF_INET": ""} + + for res in getaddrinfo(hostname, None, AF_UNSPEC, SOCK_DGRAM, IPPROTO_IP, AI_CANONNAME): + af, _, _, canonname, sa = res + address_info[af.name] = sa[0] + + # IPv4 preferred + if address_info["AF_INET"]: + host = address_info["AF_INET"] + else: + is_ipv6 = True + host, is_link_local_ipv6 = (canonname, True) if ip_address(address_info["AF_INET6"]).is_link_local else (address_info["AF_INET6"], False) + + return host, is_ipv6, is_link_local_ipv6 def requires_admin(func): @@ -78,6 +83,7 @@ class connection: self.args = args self.db = db self.hostname = host + self.port = self.args.port self.conn = None self.admin_privs = False self.password = "" @@ -91,10 +97,10 @@ class connection: self.logger = nxc_logger try: - self.host = gethost_addrinfo(self.hostname) + self.host, self.is_ipv6, self.is_link_local_ipv6 = gethost_addrinfo(self.hostname) if self.args.kerberos: self.host = self.hostname - self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={ 'True' if self.args.kerberos else 'False' }") + self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}") except Exception as e: self.logger.info(f"Error resolving hostname {self.hostname}: {e}") return @@ -389,7 +395,8 @@ class connection: return False if self.args.continue_on_success and owned: return False - + if hasattr(self.args, "delegate") and self.args.delegate: + self.args.kerberos = True with sem: if cred_type == "plaintext": if self.args.kerberos: diff --git a/nxc/helpers/bloodhound.py b/nxc/helpers/bloodhound.py index c23bc71b..bc87130c 100644 --- a/nxc/helpers/bloodhound.py +++ b/nxc/helpers/bloodhound.py @@ -42,25 +42,25 @@ def add_user_bh(user, domain, logger, config): try: with driver.session().begin_transaction() as tx: for info in users_owned: - distinguished_name = "".join(["DC=" + dc + "," for dc in info["domain"].split(".")]).rstrip(",") - domain_query = tx.run(f'MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH "{distinguished_name}" RETURN d').data() + distinguished_name = "".join([f"DC={dc}," for dc in info["domain"].split(".")]).rstrip(",") + domain_query = tx.run(f"MATCH (d:Domain) WHERE d.distinguishedname STARTS WITH '{distinguished_name}' RETURN d").data() if not domain_query: raise Exception("Domain not found in bloodhound") else: domain = domain_query[0]["d"].get("name") if info["username"][-1] == "$": - user_owned = info["username"][:-1] + "." + domain + user_owned = f"{info['username'][:-1]}.{domain}" account_type = "Computer" else: - user_owned = info["username"] + "@" + domain + user_owned = f"{info['username']}@{domain}" account_type = "User" - result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) RETURN c') + result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) RETURN c") if result.data()[0]["c"].get("owned") in (False, None): - logger.debug(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name') - result = tx.run(f'MATCH (c:{account_type} {{name:"{user_owned}"}}) SET c.owned=True RETURN c.name AS name') + logger.debug(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name") + result = tx.run(f"MATCH (c:{account_type} {{name:'{user_owned}'}}) SET c.owned=True RETURN c.name AS name") logger.highlight(f"Node {user_owned} successfully set as owned in BloodHound") except AuthError: logger.fail(f"Provided Neo4J credentials ({config.get('BloodHound', 'bh_user')}:{config.get('BloodHound', 'bh_pass')}) are not valid.") diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 5960bbd7..25322126 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -197,7 +197,7 @@ class NXCModule: """ name = "daclread" - description = "Read and backup the Discretionary Access Control List of objects. Based on the work of @_nwodtuhs and @BlWasp_. Be carefull, this module cannot read the DACLS recursively, more explains in the options." + description = "Read and backup the Discretionary Access Control List of objects. Based on the work of @_nwodtuhs and @BlWasp_. Be careful, this module cannot read the DACLS recursively, more explains in the options." supported_protocols = ["ldap"] opsec_safe = True multiple_hosts = False @@ -208,11 +208,11 @@ class NXCModule: def options(self, context, module_options): """ - Be carefull, this module cannot read the DACLS recursively. + Be careful, this module cannot read the DACLS recursively. For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually. - TARGET The objects that we want to read or backup the DACLs, sepcified by its SamAccountName - TARGET_DN The object that we want to read or backup the DACL, specified by its DN (usefull to target the domain itself) + TARGET The objects that we want to read or backup the DACLs, specified by its SamAccountName + TARGET_DN The object that we want to read or backup the DACL, specified by its DN (useful to target the domain itself) PRINCIPAL The trustee that we want to filter on ACTION The action to realise on the DACL (read, backup) ACE_TYPE The type of ACE to read (Allowed or Denied) @@ -271,8 +271,8 @@ class NXCModule: self.filename = None def on_login(self, context, connection): - """On a successful LDAP login we perform a search for the targets' SID, their Security Decriptors and the principal's SID if there is one specified""" - context.log.highlight("Be carefull, this module cannot read the DACLS recursively.") + """On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified""" + context.log.highlight("Be careful, this module cannot read the DACLS recursively.") self.baseDN = connection.ldapConnection._baseDN self.ldap_session = connection.ldapConnection @@ -292,7 +292,7 @@ class NXCModule: context.log.fail(f"Principal SID not found in LDAP ({_lookedup_principal})") sys.exit(1) - # Searching for the targets SID and their Security Decriptors + # Searching for the targets SID and their Security Descriptors # If there is only one target if (self.target_sAMAccountName or self.target_DN) and self.target_file is None: # Searching for target account with its security descriptor @@ -383,7 +383,7 @@ class NXCModule: context.log.fail(f"Principal not found in LDAP ({_lookedup_principal}), probably an LDAP session issue.") sys.exit(0) - # Attempts to retieve the SID and Distinguisehd Name from the sAMAccountName + # Attempts to retrieve the SID and Distinguisehd Name from the sAMAccountName # Not used for the moment # - samname : a sAMAccountName def get_user_info(self, context, samname): diff --git a/nxc/modules/example_module.py b/nxc/modules/example_module.py index 265bb4c3..274a86e6 100644 --- a/nxc/modules/example_module.py +++ b/nxc/modules/example_module.py @@ -39,9 +39,9 @@ class NXCModule: # These are for more critical error handling context.log.error("I'm doing something") # This will not be printed in the module context and should only be used for critical errors (e.g. a required python file is missing) try: - raise Exception("Exception that might occure") + raise Exception("Exception that might have occurred") except Exception as e: - context.log.exception(f"Exception occured: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors + context.log.exception(f"Exception occurred: {e}") # This will display an exception traceback screen after an exception was raised and should only be used for critical errors def on_admin_login(self, context, connection): """Concurrent. diff --git a/nxc/modules/keepass_trigger.py b/nxc/modules/keepass_trigger.py index dc763dc5..8b17b205 100644 --- a/nxc/modules/keepass_trigger.py +++ b/nxc/modules/keepass_trigger.py @@ -74,7 +74,7 @@ class NXCModule: USER Targeted user running KeePass, used to restart the appropriate process (used by RESTART action) - EXPORT_NAME Name fo the database export file, default: export.xml + EXPORT_NAME Name of the database export file, default: export.xml EXPORT_PATH Path where to export the KeePass database in cleartext default: C:\\Users\\Public, %APPDATA% works well too for user permissions diff --git a/nxc/modules/ldap-checker.py b/nxc/modules/ldap-checker.py index 33bdc4fc..075132a4 100644 --- a/nxc/modules/ldap-checker.py +++ b/nxc/modules/ldap-checker.py @@ -54,10 +54,10 @@ class NXCModule: # Conduct a bind to LDAPS with channel binding supported # but intentionally miscalculated. In the case that and - # LDAPS bind has without channel binding supported has occured, + # LDAPS bind has without channel binding supported has occurred, # you can determine whether the policy is set to "never" or # if it's set to "when supported" based on the potential - # error recieved from the bind attempt. + # error received from the bind attempt. async def run_ldaps_withEPA(target, credential): ldapsClientConn = MSLDAPClientConnection(target, credential) _, err = await ldapsClientConn.connect() diff --git a/nxc/modules/scan-network.py b/nxc/modules/scan-network.py index 3832193e..1bb51816 100644 --- a/nxc/modules/scan-network.py +++ b/nxc/modules/scan-network.py @@ -52,7 +52,7 @@ def new_record(rtype, serial): nr["Type"] = rtype nr["Serial"] = serial nr["TtlSeconds"] = 180 - # From authoritive zone + # From authoritative zone nr["Rank"] = 240 return nr diff --git a/nxc/modules/schtask_as.py b/nxc/modules/schtask_as.py index f6a4fb85..ff3646eb 100644 --- a/nxc/modules/schtask_as.py +++ b/nxc/modules/schtask_as.py @@ -56,7 +56,7 @@ class NXCModule: connection.hash, self.logger, connection.args.get_output_tries, - "C$", # This one shouldn't be hardcoded but I don't know where to retrive the info + "C$", # This one shouldn't be hardcoded but I don't know where to retrieve the info ) self.logger.display(f"Executing {self.cmd} as {self.user}") @@ -66,7 +66,7 @@ class NXCModule: if not isinstance(output, str): output = output.decode(connection.args.codec) except UnicodeDecodeError: - # Required to decode specific french caracters otherwise it'll print b"" + # Required to decode specific French characters otherwise it'll print b"" output = output.decode("cp437") if output: self.logger.highlight(output) diff --git a/nxc/modules/teams_localdb.py b/nxc/modules/teams_localdb.py index 5e35760a..da32fd20 100644 --- a/nxc/modules/teams_localdb.py +++ b/nxc/modules/teams_localdb.py @@ -44,7 +44,7 @@ class NXCModule: if row is None: context.log.fail("No " + name + " present in Microsoft Teams Cookies database") else: - context.log.success("Succesfully extracted " + name + ": ") + context.log.success("Successfully extracted " + name + ": ") context.log.success(row[0]) conn.close() except Exception as e: diff --git a/nxc/modules/user_desc.py b/nxc/modules/user_desc.py index e6177270..0836a903 100644 --- a/nxc/modules/user_desc.py +++ b/nxc/modules/user_desc.py @@ -28,10 +28,10 @@ class NXCModule: def options(self, context, module_options): """ LDAP_FILTER Custom LDAP search filter (fully replaces the default search) - DESC_FILTER An additional seach filter for descriptions (supports wildcard *) - DESC_INVERT An additional seach filter for descriptions (shows non matching) - USER_FILTER An additional seach filter for usernames (supports wildcard *) - USER_INVERT An additional seach filter for usernames (shows non matching) + DESC_FILTER An additional search filter for descriptions (supports wildcard *) + DESC_INVERT An additional search filter for descriptions (shows non matching) + USER_FILTER An additional search filter for usernames (supports wildcard *) + USER_INVERT An additional search filter for usernames (shows non matching) KEYWORDS Use a custom set of keywords (comma separated) ADD_KEYWORDS Add additional keywords to the default set (comma separated) """ diff --git a/nxc/netexec.py b/nxc/netexec.py index deeda945..e09e249e 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -26,7 +26,7 @@ from rich.progress import Progress import platform # Increase file_limit to prevent error "Too many open files" -if platform != "Windows": +if platform.system() != "Windows": import resource file_limit = list(resource.getrlimit(resource.RLIMIT_NOFILE)) diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index 32bc9c87..bf2a39ad 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -17,7 +17,7 @@ class ftp(connection): extra={ "protocol": "FTP", "host": self.host, - "port": self.args.port, + "port": self.port, "hostname": self.hostname, } ) @@ -41,7 +41,7 @@ class ftp(connection): def create_conn_obj(self): self.conn = FTP() try: - self.conn.connect(host=self.host, port=self.args.port) + self.conn.connect(host=self.host, port=self.port) except Exception as e: self.logger.debug(f"Error connecting to FTP host: {e}") return False @@ -61,8 +61,8 @@ class ftp(connection): # 230 is "User logged in, proceed" response, ftplib raises an exception on failed login if "230" in resp: - self.logger.debug(f"Host: {self.host} Port: {self.args.port}") - self.db.add_host(self.host, self.args.port, self.remote_version) + self.logger.debug(f"Host: {self.host} Port: {self.port}") + self.db.add_host(self.host, self.port, self.remote_version) cred_id = self.db.add_credential(username, password) diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 83a987ef..6854c0be 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -154,14 +154,14 @@ class ldap(connection): extra={ "protocol": "LDAP", "host": self.host, - "port": self.args.port, + "port": self.port, "hostname": self.hostname, } ) def get_ldap_info(self, host): try: - proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap" + proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{host}" self.logger.info(f"Connecting to {ldap_url} with no baseDN") try: @@ -349,7 +349,7 @@ class ldap(connection): try: # Connect to LDAP - proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap" + proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} [1]") self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN) @@ -374,7 +374,7 @@ class ldap(connection): self.logger.extra["protocol"] = "LDAP" - self.logger.extra["port"] = "636" if (self.args.gmsa or self.args.port == 636) else "389" + self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389" self.logger.success(out) if not self.args.local_auth: @@ -476,7 +476,7 @@ class ldap(connection): try: # Connect to LDAP - proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap" + proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.debug(f"Connecting to {ldap_url} - {self.baseDN} [3]") self.ldapConnection = ldap_impacket.LDAPConnection(ldap_url, self.baseDN) @@ -487,7 +487,7 @@ class ldap(connection): out = f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}" self.logger.extra["protocol"] = "LDAP" - self.logger.extra["port"] = "636" if (self.args.gmsa or self.args.port == 636) else "389" + self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389" self.logger.success(out) if not self.args.local_auth: @@ -571,7 +571,7 @@ class ldap(connection): try: # Connect to LDAP - proto = "ldaps" if (self.args.gmsa or self.args.port == 636) else "ldap" + proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldaps_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN}") self.ldapConnection = ldap_impacket.LDAPConnection(ldaps_url, self.baseDN) @@ -581,7 +581,7 @@ class ldap(connection): # Prepare success credential text out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}" self.logger.extra["protocol"] = "LDAP" - self.logger.extra["port"] = "636" if (self.args.gmsa or self.args.port == 636) else "389" + self.logger.extra["port"] = "636" if (self.args.gmsa or self.port == 636) else "389" self.logger.success(out) if not self.args.local_auth: @@ -1331,7 +1331,7 @@ class ldap(connection): self.logger.highlight("Using kerberos auth from ccache") timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") + "_" - bloodhound = BloodHound(ad, self.hostname, self.host, self.args.port) + bloodhound = BloodHound(ad, self.hostname, self.host, self.port) bloodhound.connect() bloodhound.run( diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 60673d4a..97daf0fc 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -52,7 +52,7 @@ class mssql(connection): extra={ "protocol": "MSSQL", "host": self.host, - "port": self.args.port, + "port": self.port, "hostname": "None", } ) @@ -112,7 +112,7 @@ class mssql(connection): def create_conn_obj(self): try: - self.conn = tds.MSSQL(self.host, self.args.port) + self.conn = tds.MSSQL(self.host, self.port) self.conn.connect() except OSError as e: self.logger.debug(f"Error connecting to MSSQL: {e}") diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index f676f855..8ba5b6f1 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -91,7 +91,7 @@ class rdp(connection): extra={ "protocol": "RDP", "host": self.host, - "port": self.args.port, + "port": self.port, "hostname": self.hostname, } ) @@ -105,7 +105,7 @@ class rdp(connection): return True def create_conn_obj(self): - self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.args.port, timeout=self.args.rdp_timeout) + self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout) self.auth = NTLMCredential(secret="pass", username="user", domain="FAKE", stype=asyauthSecret.PASS) self.check_nla() @@ -147,7 +147,7 @@ class rdp(connection): self.target = RDPTarget( ip=self.host, hostname=self.hostname, - port=self.args.port, + port=self.port, domain=self.domain, dc_ip=self.domain, timeout=self.args.rdp_timeout, diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 58f24eee..76c16ac7 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -23,7 +23,8 @@ from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP from impacket.dcerpc.v5.samr import SID_NAME_USE from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED from impacket.krb5.kerberosv5 import SessionKeyDecryptionError -from impacket.krb5.types import KerberosException +from impacket.krb5.types import KerberosException, Principal +from impacket.krb5 import constants from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login @@ -33,6 +34,7 @@ from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker from nxc.helpers.misc import gen_random_string, validate_ntlm from nxc.logger import NXCAdapter from nxc.protocols.smb.firefox import FirefoxTriage +from nxc.protocols.smb.kerberos import kerberos_login_with_S4U from nxc.servers.smb import NXCSMBServer from nxc.protocols.smb.wmiexec import WMIEXEC from nxc.protocols.smb.atexec import TSCH_EXEC @@ -169,7 +171,7 @@ class smb(connection): extra={ "protocol": "SMB", "host": self.host, - "port": self.args.port, + "port": self.port, "hostname": self.hostname, } ) @@ -382,22 +384,32 @@ class smb(connection): kerb_pass = "" self.logger.debug(f"Attempting to do Kerberos Login with useCache: {useCache}") - self.conn.kerberosLogin(username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache) + tgs = None + if self.args.delegate: + kerb_pass = "" + self.username = self.args.delegate + serverName = Principal(f"cifs/{self.hostname}", type=constants.PrincipalNameType.NT_SRV_INST.value) + tgs = kerberos_login_with_S4U(domain, self.hostname, username, password, nthash, lmhash, aesKey, kdcHost, self.args.delegate, serverName, useCache, no_s4u2proxy=self.args.no_s4u2proxy) + self.logger.debug(f"Got TGS for {self.args.delegate} through S4U") + + self.conn.kerberosLogin(self.username, password, domain, lmhash, nthash, aesKey, kdcHost, useCache=useCache, TGS=tgs) self.check_if_admin() if username == "": self.username = self.conn.getCredentials()[0] - else: + elif not self.args.delegate: self.username = username used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" + if self.args.delegate: + used_ccache = f" through S4U with {username}" else: self.plaintext_login(self.hostname, username, password) return True out = f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}" self.logger.success(out) - if not self.args.local_auth: + if not self.args.local_auth and not self.args.delegate: add_user_bh(self.username, domain, self.logger, self.config) if self.admin_privs: add_user_bh(f"{self.hostname}$", domain, self.logger, self.config) @@ -406,6 +418,7 @@ class smb(connection): if self.args.continue_on_success and self.signing: with contextlib.suppress(Exception): self.conn.logoff() + self.create_conn_obj() return True @@ -421,10 +434,14 @@ class smb(connection): return False except OSError as e: used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" + if self.args.delegate: + used_ccache = f" through S4U with {username}" self.logger.fail(f"{domain}\\{self.username}{used_ccache} {e}") except (SessionError, Exception) as e: error, desc = e.getErrorString() used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" + if self.args.delegate: + used_ccache = f" through S4U with {username}" self.logger.fail( f"{domain}\\{self.username}{used_ccache} {error} {f'({desc})' if self.args.verbose else ''}", color="magenta" if error in smb_error_status else "red", @@ -569,7 +586,7 @@ class smb(connection): kdc if kdc else self.host, kdc if kdc else self.host, None, - self.args.port, + self.port, preferredDialect=SMB_DIALECT, timeout=self.args.smb_timeout, ) @@ -590,7 +607,7 @@ class smb(connection): kdc if kdc else self.host, kdc if kdc else self.host, None, - self.args.port, + self.port, timeout=self.args.smb_timeout, ) self.smbv1 = False @@ -722,7 +739,7 @@ class smb(connection): self.host if not self.kerberos else self.hostname + "." + self.domain, self.smb_share_name, self.conn, - self.args.port, + self.port, self.username, self.password, self.domain, @@ -731,7 +748,7 @@ class smb(connection): self.kdcHost, self.hash, self.args.share, - self.args.port, + self.port, self.logger, self.args.get_output_tries ) @@ -1243,12 +1260,12 @@ class smb(connection): try: full_hostname = self.host if not self.kerberos else self.hostname + "." + self.domain - string_binding = KNOWN_PROTOCOLS[self.args.port]["bindstr"] + string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"] logging.debug(f"StringBinding {string_binding}") rpc_transport = transport.DCERPCTransportFactory(string_binding) - rpc_transport.set_dport(self.args.port) + rpc_transport.set_dport(self.port) - if KNOWN_PROTOCOLS[self.args.port]["set_host"]: + if KNOWN_PROTOCOLS[self.port]["set_host"]: rpc_transport.setRemoteHost(full_hostname) if hasattr(rpc_transport, "set_credentials"): diff --git a/nxc/protocols/smb/kerberos.py b/nxc/protocols/smb/kerberos.py new file mode 100644 index 00000000..2a567fa1 --- /dev/null +++ b/nxc/protocols/smb/kerberos.py @@ -0,0 +1,278 @@ +import datetime +import struct +import random +from six import b + +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue + +from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, \ + seq_set, seq_set_iter, PA_FOR_USER_ENC, Ticket as TicketAsn1, EncTGSRepPart, \ + PA_PAC_OPTIONS +from impacket.krb5.types import Principal, KerberosTime, Ticket +from impacket.krb5.kerberosv5 import sendReceive, getKerberosTGT +from impacket.krb5.ccache import CCache +from impacket.krb5.crypto import Key, _enctype_table, _HMACMD5 +from impacket.krb5 import constants + +from nxc.logger import nxc_logger + + +def kerberos_login_with_S4U(domain, hostname, username, password, nthash, lmhash, aesKey, kdcHost, impersonate, spn, use_cache, no_s4u2proxy=False): + my_tgt = None + if use_cache: + domain, _, tgt, _ = CCache.parseFile(domain, username, f"cifs/{hostname}") + if my_tgt is None: + raise + my_tgt = tgt["KDC_REP"] + cipher = tgt["cipher"] + session_key = tgt["sessionKey"] + if my_tgt is None: + principal = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + nxc_logger.debug("Getting TGT for user") + tgt, cipher, _, session_key = getKerberosTGT(principal, password, domain, lmhash, nthash, aesKey, kdcHost) + my_tgt = decoder.decode(tgt, asn1Spec=AS_REP())[0] + decoded_tgt = my_tgt + # Extract the ticket from the TGT + ticket = Ticket() + ticket.from_asn1(decoded_tgt["ticket"]) + + ap_req = AP_REQ() + ap_req["pvno"] = 5 + ap_req["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + ap_req["ap-options"] = constants.encodeFlags(opts) + seq_set(ap_req, "ticket", ticket.to_asn1) + + authenticator = Authenticator() + authenticator["authenticator-vno"] = 5 + authenticator["crealm"] = str(decoded_tgt["crealm"]) + + client_name = Principal() + client_name.from_asn1(decoded_tgt, "crealm", "cname") + + seq_set(authenticator, "cname", client_name.components_to_asn1) + + now = datetime.datetime.utcnow() + authenticator["cusec"] = now.microsecond + authenticator["ctime"] = KerberosTime.to_asn1(now) + + encoded_authenticator = encoder.encode(authenticator) + + # Key Usage 7 + # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes + # TGS authenticator subkey), encrypted with the TGS session + # key (Section 5.5.1) + encrypted_encoded_authenticator = cipher.encrypt(session_key, 7, encoded_authenticator, None) + + ap_req["authenticator"] = noValue + ap_req["authenticator"]["etype"] = cipher.enctype + ap_req["authenticator"]["cipher"] = encrypted_encoded_authenticator + + encoded_ap_req = encoder.encode(ap_req) + + tgs_req = TGS_REQ() + + tgs_req["pvno"] = 5 + tgs_req["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value) + + tgs_req["padata"] = noValue + tgs_req["padata"][0] = noValue + tgs_req["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) + tgs_req["padata"][0]["padata-value"] = encoded_ap_req + + # In the S4U2self KRB_TGS_REQ/KRB_TGS_REP protocol extension, a service + # requests a service ticket to itself on behalf of a user. The user is + # identified to the KDC by the user's name and realm. + client_name = Principal(impersonate, type=constants.PrincipalNameType.NT_PRINCIPAL.value) + + s4u_byte_array = struct.pack("