Skip to content

Commit 1393d13

Browse files
authored
Merge pull request Pennyw0rth#561 from crosscutsaw/presence
new module: smb > presence
2 parents 812c277 + e8f62d2 commit 1393d13

2 files changed

Lines changed: 254 additions & 1 deletion

File tree

nxc/modules/change-password.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ def options(self, context, module_options):
3434
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
3535
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
3636
"""
37-
self.context = context
3837
self.newpass = module_options.get("NEWPASS")
3938
self.newhash = module_options.get("NEWNTHASH")
4039
self.target_user = module_options.get("USER")
@@ -78,6 +77,7 @@ def authenticate(self, context, connection, protocol, anonymous=False):
7877
raise
7978

8079
def on_login(self, context, connection):
80+
self.context = context
8181
target_username = self.target_user or connection.username
8282
target_domain = connection.domain
8383

nxc/modules/presence.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import re
2+
from impacket.dcerpc.v5 import samr, tsch, transport
3+
from impacket.dcerpc.v5 import tsts as TSTS
4+
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
5+
from contextlib import suppress
6+
import traceback
7+
8+
9+
class NXCModule:
10+
"""
11+
Module to find Domain and Enterprise Admin presence on target systems over SMB.
12+
Made by @crosscutsaw, @NeffIsBack
13+
"""
14+
15+
name = "presence"
16+
description = "Traces Domain and Enterprise Admin presence in the target over SMB"
17+
supported_protocols = ["smb"]
18+
opsec_safe = True
19+
multiple_hosts = True
20+
21+
def options(self, context, module_options):
22+
"""There are no module options."""
23+
24+
def on_admin_login(self, context, connection):
25+
try:
26+
admin_users = self.enumerate_admin_users(context, connection)
27+
if not admin_users:
28+
context.log.fail("No admin users found.")
29+
return
30+
31+
# Update user objects to check if they are in tasklist, users directory or in scheduled tasks
32+
self.check_users_directory(context, connection, admin_users)
33+
self.check_tasklist(context, connection, admin_users)
34+
self.check_scheduled_tasks(context, connection, admin_users)
35+
36+
# print grouped/logged results nicely
37+
self.print_grouped_results(context, admin_users)
38+
except Exception as e:
39+
context.log.fail(str(e))
40+
context.log.debug(traceback.format_exc())
41+
42+
def get_dce_rpc(self, target, string_binding, dce_binding, connection):
43+
# Create a DCE/RPC transport object with the specified string binding
44+
rpctransport = transport.DCERPCTransportFactory(string_binding)
45+
rpctransport.setRemoteHost(target)
46+
rpctransport.set_credentials(
47+
connection.username,
48+
connection.password,
49+
connection.domain,
50+
connection.lmhash,
51+
connection.nthash,
52+
aesKey=connection.aesKey,
53+
)
54+
rpctransport.set_kerberos(connection.kerberos, connection.kdcHost)
55+
56+
# Connect to the DCE/RPC endpoint
57+
dce = rpctransport.get_dce_rpc()
58+
if connection.kerberos:
59+
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
60+
dce.set_credentials(*rpctransport.get_credentials())
61+
dce.connect()
62+
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
63+
dce.bind(dce_binding)
64+
return dce
65+
66+
def enumerate_admin_users(self, context, connection):
67+
admin_users = []
68+
69+
try:
70+
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
71+
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
72+
except Exception as e:
73+
context.log.fail(f"Failed to connect to SAMR: {e}")
74+
context.log.debug(traceback.format_exc())
75+
return admin_users
76+
77+
try:
78+
server_handle = samr.hSamrConnect2(dce)["ServerHandle"]
79+
domain = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)["Buffer"]["Buffer"][0]["Name"]
80+
resp = samr.hSamrLookupDomainInSamServer(dce, server_handle, domain)
81+
self.domain_sid = resp["DomainId"].formatCanonical()
82+
domain_handle = samr.hSamrOpenDomain(dce, server_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_LIST_ACCOUNTS, resp["DomainId"])["DomainHandle"]
83+
context.log.debug(f"Resolved domain SID for {domain}: {self.domain_sid}")
84+
except Exception as e:
85+
context.log.fail(f"Failed to open domain {domain}: {e!s}")
86+
context.log.debug(traceback.format_exc())
87+
return admin_users
88+
89+
admin_rids = {
90+
"Domain Admins": 512,
91+
"Enterprise Admins": 519,
92+
}
93+
94+
# Enumerate admin groups and their members
95+
for group_name, group_rid in admin_rids.items():
96+
context.log.debug(f"Looking up group: {group_name} with RID {group_rid}")
97+
98+
try:
99+
group_handle = samr.hSamrOpenGroup(dce, domain_handle, samr.GROUP_LIST_MEMBERS, group_rid)["GroupHandle"]
100+
resp = samr.hSamrGetMembersInGroup(dce, group_handle)
101+
for member in resp["Members"]["Members"]:
102+
rid = int.from_bytes(member.getData(), byteorder="little")
103+
try:
104+
user_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, rid)["UserHandle"]
105+
username = samr.hSamrQueryInformationUser2(dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["UserName"]
106+
107+
# If user already exists, append group name
108+
if any(u["sid"] == f"{self.domain_sid}-{rid}" for u in admin_users):
109+
user = next(u for u in admin_users if u["sid"] == f"{self.domain_sid}-{rid}")
110+
user["group"].append(group_name)
111+
else:
112+
admin_users.append({"username": username, "sid": f"{self.domain_sid}-{rid}", "domain": domain, "group": [group_name], "in_tasks": False, "in_directory": False, "in_scheduled_tasks": False})
113+
context.log.debug(f"Found user: {username} with RID {rid} in group {group_name}")
114+
except Exception as e:
115+
context.log.debug(f"Failed to get user info for RID {rid}: {e!s}")
116+
finally:
117+
with suppress(Exception):
118+
samr.hSamrCloseHandle(dce, user_handle)
119+
except Exception as e:
120+
context.log.debug(f"Failed to get members of group {group_name}: {e!s}")
121+
finally:
122+
with suppress(Exception):
123+
samr.hSamrCloseHandle(dce, group_handle)
124+
125+
return admin_users
126+
127+
def check_users_directory(self, context, connection, admin_users):
128+
dirs_found = set()
129+
130+
# try C$\Users first
131+
try:
132+
files = connection.conn.listPath("C$", "\\Users\\*")
133+
except Exception as e:
134+
context.log.debug(f"C$\\Users unavailable: {e}, trying Documents and Settings")
135+
try:
136+
files = connection.conn.listPath("C$", "\\Documents and Settings\\*")
137+
except Exception as e:
138+
context.log.fail(f"Error listing fallback directory: {e}")
139+
return
140+
else:
141+
context.log.debug("Successfully listed C$\\Users")
142+
143+
# collect folder names (lowercase) ignoring "." and ".."
144+
dirs_found.update([f.get_shortname().lower() for f in files if f.get_shortname().lower() not in [".", "..", "administrator"]])
145+
146+
# for admin users, check for folder presence
147+
for user in admin_users:
148+
# Look for administrator.domain to check if SID 500 Administrator is present (second check)
149+
if user["username"].lower() in dirs_found or \
150+
(user["username"].lower() == "administrator" and f"{user['username'].lower()}.{user['domain']}" in dirs_found):
151+
user["in_directory"] = True
152+
context.log.info(f"Found user {user['username']} in directories")
153+
154+
def check_tasklist(self, context, connection, admin_users):
155+
"""Checks tasklist over rpc."""
156+
try:
157+
with TSTS.LegacyAPI(connection.conn, connection.remoteName, kerberos=connection.kerberos) as legacy:
158+
handle = legacy.hRpcWinStationOpenServer()
159+
processes = legacy.hRpcWinStationGetAllProcesses(handle)
160+
except Exception as e:
161+
context.log.fail(f"Error in check_tasklist RPC method: {e}")
162+
return []
163+
164+
context.log.debug(f"Enumerated {len(processes)} processes on {connection.host}")
165+
166+
for process in processes:
167+
context.log.debug(f"ImageName: {process['ImageName']}, UniqueProcessId: {process['SessionId']}, pSid: {process['pSid']}")
168+
# Check if process SID matches any admin user SID
169+
for user in admin_users:
170+
if process["pSid"] == user["sid"]:
171+
user["in_tasks"] = True
172+
context.log.info(f"Matched process {process['ImageName']} with user {user['username']}")
173+
174+
def check_scheduled_tasks(self, context, connection, admin_users):
175+
"""Checks scheduled tasks over rpc."""
176+
try:
177+
target = connection.remoteName if connection.kerberos else connection.host
178+
stringbinding = f"ncacn_np:{target}[\\pipe\\atsvc]"
179+
dce = self.get_dce_rpc(target, stringbinding, tsch.MSRPC_UUID_TSCHS, connection)
180+
181+
# Also extract non admins where we can get the password
182+
self.non_admins = []
183+
non_admin_sids = set()
184+
185+
tasks = tsch.hSchRpcEnumTasks(dce, "\\")["pNames"]
186+
for task in tasks:
187+
xml = tsch.hSchRpcRetrieveTask(dce, task["Data"])["pXml"]
188+
# Extract SID and LogonType from the XML, if LogonType is "Password" we should be able to extract the password
189+
sid = re.search(fr"<UserId>({self.domain_sid}-\d{{3,}})</UserId>", xml)
190+
logon_type = re.search(r"<LogonType>(\w+)</LogonType>", xml)
191+
192+
# Check if SID and LogonType are found, then check if SID matches any admin user
193+
if sid and logon_type and logon_type.group(1) == "Password":
194+
context.log.debug(f"Found scheduled task '{task['Data']}' with SID {sid} and LogonType {logon_type.group(1)}")
195+
if any(user["sid"] == sid.group(1) for user in admin_users):
196+
user = next(user for user in admin_users if user["sid"] == sid.group(1))
197+
user["in_scheduled_tasks"] = True
198+
else:
199+
# If not an admin user, add to non_admin_sids for further processing
200+
non_admin_sids.add(sid.group(1))
201+
202+
if non_admin_sids:
203+
string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]"
204+
dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection)
205+
206+
# Get Domain Handle
207+
server_handle = samr.hSamrConnect2(dce)["ServerHandle"]
208+
domain = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)["Buffer"]["Buffer"][0]["Name"]
209+
domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, domain)["DomainId"]
210+
domain_handle = samr.hSamrOpenDomain(dce, server_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_LIST_ACCOUNTS, domain_sid)["DomainHandle"]
211+
212+
for sid in non_admin_sids:
213+
user_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, int(sid.split("-")[-1]))["UserHandle"]
214+
username = samr.hSamrQueryInformationUser2(dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["UserName"]
215+
self.non_admins.append(username)
216+
217+
except Exception as e:
218+
context.log.fail(f"Failed to enumerate scheduled tasks: {e}")
219+
context.log.debug(traceback.format_exc())
220+
221+
def print_grouped_results(self, context, admin_users):
222+
"""Logs all results grouped per host in order"""
223+
# Make less verbose for scanning large ranges
224+
context.log.info(f"Identified Admin Users: {', '.join([user['username'] for user in admin_users])}")
225+
226+
# Print directory users
227+
dir_users = [user for user in admin_users if user["in_directory"]]
228+
if dir_users:
229+
context.log.success("Found admins in directories:")
230+
for user in dir_users:
231+
context.log.highlight(f"{user['username']} ({', '.join(user['group'])})")
232+
233+
# Print tasklist users
234+
tasklist_users = [user for user in admin_users if user["in_tasks"]]
235+
if tasklist_users:
236+
context.log.success("Found admins in tasklist:")
237+
for user in tasklist_users:
238+
context.log.highlight(f"{user['username']} ({', '.join(user['group'])})")
239+
240+
# Print scheduled tasks users
241+
scheduled_tasks_users = [user for user in admin_users if user["in_scheduled_tasks"]]
242+
if scheduled_tasks_users:
243+
context.log.success("Found admins in scheduled tasks:")
244+
for user in scheduled_tasks_users:
245+
context.log.highlight(f"{user['username']} ({', '.join(user['group'])})")
246+
if self.non_admins:
247+
context.log.info(f"Found {len(self.non_admins)} non-admin scheduled tasks:")
248+
for sid in self.non_admins:
249+
context.log.info(sid)
250+
251+
# Making this less verbose to better scan large ranges
252+
if not dir_users and not tasklist_users:
253+
context.log.info("No matches found in users directory, tasklist or scheduled tasks.")

0 commit comments

Comments
 (0)