Skip to content

Commit 9ad951c

Browse files
Merge branch 'main' into opsec
2 parents 246e7e5 + 97049a0 commit 9ad951c

5 files changed

Lines changed: 106 additions & 38 deletions

File tree

nxc/modules/get-info-users.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from nxc.parsers.ldap_results import parse_result_attributes
2+
3+
4+
class NXCModule:
5+
"""
6+
Get the info field of users
7+
Module by @sepauli
8+
"""
9+
name = "get-info-users"
10+
description = "Get the info field of all users. May contain password"
11+
supported_protocols = ["ldap"]
12+
opsec_safe = True
13+
multiple_hosts = True
14+
15+
def options(self, context, module_options):
16+
"""FILTER Apply the FILTER (grep-like) (default: '')"""
17+
self.FILTER = ""
18+
if "FILTER" in module_options:
19+
self.FILTER = module_options["FILTER"]
20+
21+
def on_login(self, context, connection):
22+
# Building the search filter
23+
resp = connection.search(
24+
searchFilter="(info=*)",
25+
attributes=["sAMAccountName", "info"]
26+
)
27+
28+
context.log.debug(f"Total of records returned {len(resp)}")
29+
resp_parsed = parse_result_attributes(resp)
30+
answers = [[x["sAMAccountName"], x["info"]] for x in resp_parsed]
31+
32+
answers = self.filter_answer(context, answers)
33+
if answers:
34+
context.log.success("Found following users: ")
35+
for answer in answers:
36+
context.log.highlight(f"User: {answer[0]:<20} Info: {answer[1]}")
37+
38+
def filter_answer(self, context, answers):
39+
# No option to filter
40+
if not self.FILTER:
41+
context.log.debug("No filter option enabled")
42+
return answers
43+
# Filter
44+
context.log.debug(f"Filter info field with: {self.FILTER}")
45+
return [answer for answer in answers if self.FILTER in answer[1]]
46+

nxc/protocols/ldap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ def check_if_admin(self):
664664
resp = self.search(search_filter, attributes, sizeLimit=0, baseDN=self.baseDN)
665665
resp_parsed = parse_result_attributes(resp)
666666
answers = []
667-
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "") and self.username != "":
667+
if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "" or self.use_kcache) and self.username != "":
668668
for item in resp_parsed:
669669
self.sid_domain = "-".join(item["objectSid"].split("-")[:-1])
670670

nxc/protocols/smb.py

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ def is_host_dc(self):
688688
from impacket.dcerpc.v5 import nrpc, epm
689689

690690
self.logger.debug("Performing authentication attempts...")
691-
691+
692692
# First check if port 135 is open
693693
if self._is_port_open(135):
694694
self.logger.debug("Port 135 is open, attempting MSRPC connection...")
@@ -964,7 +964,7 @@ def enumerate_sessions_info(self, sessions):
964964
sessions[SessionId]["DisconnectTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["DisconnectTime"]
965965
sessions[SessionId]["LogonTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["LogonTime"]
966966
sessions[SessionId]["LastInputTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["LastInputTime"]
967-
967+
968968
try:
969969
with TSTS.RCMPublic(self.conn, self.host, self.kerberos) as rcm:
970970
for SessionId in sessions:
@@ -1011,33 +1011,32 @@ def qwinsta(self):
10111011
"WTS_SESSIONSTATE_LOCK": "Locked",
10121012
"WTS_SESSIONSTATE_UNLOCK": "Unlocked",
10131013
}
1014+
10141015
sessions = self.get_session_list()
1015-
if not len(sessions):
1016+
if not sessions:
10161017
return
1018+
10171019
self.enumerate_sessions_info(sessions)
10181020

1021+
# Calculate max lengths for formatting
10191022
maxSessionNameLen = max(len(sessions[i]["SessionName"]) + 1 for i in sessions)
1020-
maxSessionNameLen = maxSessionNameLen if len("SESSIONNAME") < maxSessionNameLen else len("SESSIONNAME") + 1
1023+
maxSessionNameLen = max(maxSessionNameLen, len("SESSIONNAME") + 1)
10211024
maxUsernameLen = max(len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions) + 1
1022-
maxUsernameLen = maxUsernameLen if len("Username") < maxUsernameLen else len("Username") + 1
1025+
maxUsernameLen = max(maxUsernameLen, len("USERNAME") + 1)
10231026
maxIdLen = max(len(str(i)) for i in sessions)
1024-
maxIdLen = maxIdLen if len("ID") < maxIdLen else len("ID") + 1
1027+
maxIdLen = max(maxIdLen, len("ID") + 1)
10251028
maxStateLen = max(len(sessions[i]["state"]) + 1 for i in sessions)
1026-
maxStateLen = maxStateLen if len("STATE") < maxStateLen else len("STATE") + 1
1027-
maxRemoteIp = max(len(sessions[i]["RemoteIp"]) + 1 for i in sessions)
1028-
maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1
1029-
maxClientName = max(len(sessions[i]["ClientName"]) + 1 for i in sessions)
1030-
maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1
1031-
template = ("{SESSIONNAME: <%d} " # noqa: UP031
1032-
"{USERNAME: <%d} "
1033-
"{ID: <%d} "
1029+
maxStateLen = max(maxStateLen, len("STATE") + 1)
1030+
1031+
# Create the template for formatting
1032+
template = (f"{{SESSIONNAME: <{maxSessionNameLen}}} "
1033+
f"{{USERNAME: <{maxUsernameLen}}} "
1034+
f"{{ID: <{maxIdLen}}} "
10341035
"{IPv4: <16} "
1035-
"{STATE: <%d} "
1036+
f"{{STATE: <{maxStateLen}}} "
10361037
"{DSTATE: <9} "
10371038
"{CONNTIME: <20} "
1038-
"{DISCTIME: <20} ") % (maxSessionNameLen, maxUsernameLen, maxIdLen, maxStateLen)
1039-
1040-
result = []
1039+
"{DISCTIME: <20} ")
10411040
header = template.format(
10421041
SESSIONNAME="SESSIONNAME",
10431042
USERNAME="USERNAME",
@@ -1048,7 +1047,6 @@ def qwinsta(self):
10481047
CONNTIME="ConnectTime",
10491048
DISCTIME="DisconnectTime",
10501049
)
1051-
10521050
header2 = template.replace(" <", "=<").format(
10531051
SESSIONNAME="",
10541052
USERNAME="",
@@ -1059,30 +1057,49 @@ def qwinsta(self):
10591057
CONNTIME="",
10601058
DISCTIME="",
10611059
)
1062-
result.extend((header, header2))
1060+
result = [header, header2]
1061+
1062+
# Check if we need to filter for usernames
1063+
usernames = None
1064+
if self.args.qwinsta:
1065+
arg = self.args.qwinsta
1066+
if os.path.isfile(arg):
1067+
with open(arg) as f:
1068+
usernames = [line.strip().lower() for line in f if line.strip()]
1069+
else:
1070+
usernames = [arg.lower()]
10631071

10641072
for i in sessions:
1073+
username = sessions[i]["Username"]
1074+
domain = sessions[i]["Domain"]
1075+
user_full = f"{domain}\\{username}" if username else ""
1076+
1077+
# If usernames are provided, filter them
1078+
if usernames and username.lower() not in usernames:
1079+
continue
1080+
10651081
connectTime = sessions[i]["ConnectTime"]
10661082
connectTime = connectTime.strftime(r"%Y/%m/%d %H:%M:%S") if connectTime.year > 1601 else "None"
10671083

10681084
disconnectTime = sessions[i]["DisconnectTime"]
10691085
disconnectTime = disconnectTime.strftime(r"%Y/%m/%d %H:%M:%S") if disconnectTime.year > 1601 else "None"
1070-
userName = sessions[i]["Domain"] + "\\" + sessions[i]["Username"] if len(sessions[i]["Username"]) else ""
10711086

1072-
result.append(template.format(
1087+
row = template.format(
10731088
SESSIONNAME=sessions[i]["SessionName"],
1074-
USERNAME=userName,
1089+
USERNAME=user_full,
10751090
ID=i,
10761091
IPv4=sessions[i]["RemoteIp"],
10771092
STATE=sessions[i]["state"],
10781093
DSTATE=desktop_states[sessions[i]["flags"]],
10791094
CONNTIME=connectTime,
10801095
DISCTIME=disconnectTime,
1081-
))
1096+
)
1097+
result.append(row)
10821098

1083-
self.logger.success("Enumerated qwinsta sessions")
1084-
for row in result:
1085-
self.logger.highlight(row)
1099+
if len(result) > 2:
1100+
self.logger.success("Enumerated qwinsta sessions")
1101+
for row in result:
1102+
self.logger.highlight(row)
10861103

10871104
@requires_admin
10881105
def tasklist(self):
@@ -1242,16 +1259,20 @@ def shares(self):
12421259
error = get_error_string(e)
12431260
self.logger.debug(f"Error adding share: {error}")
12441261

1262+
if self.args.filter_shares:
1263+
self.logger.display("[REMOVED] Use the --shares read,write options instead.")
1264+
12451265
self.logger.display("Enumerated shares")
12461266
self.logger.highlight(f"{'Share':<15} {'Permissions':<15} {'Remark'}")
12471267
self.logger.highlight(f"{'-----':<15} {'-----------':<15} {'------'}")
1268+
12481269
for share in permissions:
12491270
name = share["name"]
12501271
remark = share["remark"]
1251-
perms = share["access"]
1252-
if self.args.filter_shares and not any(x in perms for x in self.args.filter_shares):
1272+
perms = ",".join(share["access"])
1273+
if self.args.shares and self.args.shares.lower() not in perms.lower():
12531274
continue
1254-
self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}")
1275+
self.logger.highlight(f"{name:<15} {perms:<15} {remark}")
12551276
return permissions
12561277

12571278
def dir(self):
@@ -1531,16 +1552,16 @@ def rid_brute(self, max_rid=None):
15311552
max_rid = int(self.args.rid_brute)
15321553

15331554
KNOWN_PROTOCOLS = {
1534-
135: {"bindstr": rf"ncacn_ip_tcp:{self.host}"},
1535-
139: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
1536-
445: {"bindstr": rf"ncacn_np:{self.host}[\pipe\lsarpc]"},
1555+
135: {"bindstr": rf"ncacn_ip_tcp:{self.remoteName}"},
1556+
139: {"bindstr": rf"ncacn_np:{self.remoteName}[\pipe\lsarpc]"},
1557+
445: {"bindstr": rf"ncacn_np:{self.remoteName}[\pipe\lsarpc]"},
15371558
}
15381559

15391560
try:
15401561
string_binding = KNOWN_PROTOCOLS[self.port]["bindstr"]
15411562
self.logger.debug(f"StringBinding {string_binding}")
15421563
rpc_transport = transport.DCERPCTransportFactory(string_binding)
1543-
rpc_transport.setRemoteHost(self.host)
1564+
rpc_transport.setRemoteHost(self.remoteName)
15441565

15451566
if hasattr(rpc_transport, "set_credentials"):
15461567
# This method exists only for selected protocol sequences.

nxc/protocols/smb/proto_args.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ def proto_args(parser, parents):
3737
cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC")
3838

3939
mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating")
40-
mapping_enum_group.add_argument("--shares", action="store_true", help="Enumerate shares and access")
40+
mapping_enum_group.add_argument("--shares", type=str, nargs="?", const="", help="Enumerate shares and access, filter on specified argument (read ; write ; read,write)")
4141
mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')")
4242
mapping_enum_group.add_argument("--interfaces", action="store_true", help="Enumerate network interfaces")
4343
mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)")
44-
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'")
44+
mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'READ' 'WRITE' or 'READ,WRITE'")
4545
mapping_enum_group.add_argument("--smb-sessions", action="store_true", help="Enumerate active smb sessions")
4646
mapping_enum_group.add_argument("--disks", action="store_true", help="Enumerate disks")
4747
mapping_enum_group.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex")
@@ -53,7 +53,7 @@ def proto_args(parser, parents):
5353
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")
5454
mapping_enum_group.add_argument("--pass-pol", action="store_true", help="dump password policy")
5555
mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="Enumerate users by bruteforcing RIDs")
56-
mapping_enum_group.add_argument("--qwinsta", action="store_true", help="Enumerate RDP connections")
56+
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.")
5757
mapping_enum_group.add_argument("--tasklist", type=str, nargs="?", const=True, help="Enumerate running processes and filter for the specified one if specified")
5858
mapping_enum_group.add_argument("--taskkill", type=str, help="Kills a specific PID or a proces name's PID's")
5959

tests/e2e_commands.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -L
201201
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M adcs
202202
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M daclread -o TARGET=LOGIN_USERNAME ACTION=read
203203
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-desc-users
204+
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-info-users
204205
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-network
205206
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME
206207
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps

0 commit comments

Comments
 (0)