@@ -19,28 +19,27 @@ class NXCModule:
1919 def options (self , context , module_options ):
2020 """There are no module options."""
2121
22- def on_admin_login (self , context , connection ):
23- try :
24- admin_users = self .enumerate_admin_users (context , connection )
25- if not admin_users :
26- context .log .fail ("No admin users found." )
27- return
22+ # --- helper: build/bind DCE-RPC while pinning TCP to IP and using safe server name ---
23+ def get_dce_rpc (self , tcp_host , string_binding , dce_binding , connection , remote_name = None ):
24+ """
25+ tcp_host: the TCP destination (IP)
26+ string_binding: ncacn_np binding (FQDN for Kerberos SPN; IP for NTLM is fine)
27+ remote_name: SMB server name for auth ('*SMBSERVER' for NTLM; FQDN for Kerberos)
28+ """
29+ rpctransport = transport .DCERPCTransportFactory (string_binding )
2830
29- # Update user objects to check if they are in tasklist, users directory or in scheduled tasks
30- self .check_users_directory (context , connection , admin_users )
31- self .check_tasklist (context , connection , admin_users )
32- self .check_scheduled_tasks (context , connection , admin_users )
31+ # Pin the actual TCP destination to the IP we intend to hit.
32+ rpctransport .setRemoteHost (tcp_host )
3333
34- # print grouped/logged results nicely
35- self .print_grouped_results (context , admin_users )
36- except Exception as e :
37- context .log .fail (str (e ))
38- context .log .debug (traceback .format_exc ())
34+ # Choose the SMB "server name" used by the session.
35+ if remote_name is None :
36+ remote_name = (
37+ f"{ connection .hostname } .{ connection .domain } "
38+ if connection .kerberos
39+ else "*SMBSERVER"
40+ )
41+ rpctransport .setRemoteName (remote_name )
3942
40- def get_dce_rpc (self , target , string_binding , dce_binding , connection ):
41- # Create a DCE/RPC transport object with the specified string binding
42- rpctransport = transport .DCERPCTransportFactory (string_binding )
43- rpctransport .setRemoteHost (target )
4443 rpctransport .set_credentials (
4544 connection .username ,
4645 connection .password ,
@@ -51,7 +50,6 @@ def get_dce_rpc(self, target, string_binding, dce_binding, connection):
5150 )
5251 rpctransport .set_kerberos (connection .kerberos , connection .kdcHost )
5352
54- # Connect to the DCE/RPC endpoint
5553 dce = rpctransport .get_dce_rpc ()
5654 if connection .kerberos :
5755 dce .set_auth_type (RPC_C_AUTHN_GSS_NEGOTIATE )
@@ -61,12 +59,44 @@ def get_dce_rpc(self, target, string_binding, dce_binding, connection):
6159 dce .bind (dce_binding )
6260 return dce
6361
62+ def on_admin_login (self , context , connection ):
63+ try :
64+ admin_users = self .enumerate_admin_users (context , connection )
65+ if not admin_users :
66+ context .log .fail ("No admin users found." )
67+ return
68+
69+ # Update user objects to check if they are in tasklist, users directory or in scheduled tasks
70+ self .check_users_directory (context , connection , admin_users )
71+ self .check_tasklist (context , connection , admin_users )
72+ self .check_scheduled_tasks (context , connection , admin_users )
73+
74+ # print grouped/logged results nicely
75+ self .print_grouped_results (context , admin_users )
76+ except Exception as e :
77+ context .log .fail (str (e ))
78+ context .log .debug (traceback .format_exc ())
79+
6480 def enumerate_admin_users (self , context , connection ):
6581 admin_users = []
6682
6783 try :
68- string_binding = fr"ncacn_np:{ connection .kdcHost } [\pipe\samr]"
69- dce = self .get_dce_rpc (connection .kdcHost , string_binding , samr .MSRPC_UUID_SAMR , connection )
84+ tcp_ip = connection .host # e.g., 10.10.11.181
85+ # Binding name: FQDN for Kerberos SPN; IP is fine for NTLM.
86+ bind_name = (
87+ f"{ connection .hostname } .{ connection .domain } "
88+ if connection .kerberos
89+ else tcp_ip
90+ )
91+ string_binding = fr"ncacn_np:{ bind_name } [\pipe\samr]"
92+ dce = self .get_dce_rpc (
93+ tcp_host = tcp_ip ,
94+ string_binding = string_binding ,
95+ dce_binding = samr .MSRPC_UUID_SAMR ,
96+ connection = connection ,
97+ remote_name = (f"{ connection .hostname } .{ connection .domain } "
98+ if connection .kerberos else "*SMBSERVER" ),
99+ )
70100 except Exception as e :
71101 context .log .fail (f"Failed to connect to SAMR: { e } " )
72102 context .log .debug (traceback .format_exc ())
@@ -77,7 +107,9 @@ def enumerate_admin_users(self, context, connection):
77107 domain = samr .hSamrEnumerateDomainsInSamServer (dce , server_handle )["Buffer" ]["Buffer" ][0 ]["Name" ]
78108 resp = samr .hSamrLookupDomainInSamServer (dce , server_handle , domain )
79109 self .domain_sid = resp ["DomainId" ].formatCanonical ()
80- domain_handle = samr .hSamrOpenDomain (dce , server_handle , samr .DOMAIN_LOOKUP | samr .DOMAIN_LIST_ACCOUNTS , resp ["DomainId" ])["DomainHandle" ]
110+ domain_handle = samr .hSamrOpenDomain (
111+ dce , server_handle , samr .DOMAIN_LOOKUP | samr .DOMAIN_LIST_ACCOUNTS , resp ["DomainId" ]
112+ )["DomainHandle" ]
81113 context .log .debug (f"Resolved domain SID for { domain } : { self .domain_sid } " )
82114 except Exception as e :
83115 context .log .fail (f"Failed to open domain { domain } : { e !s} " )
@@ -100,14 +132,26 @@ def enumerate_admin_users(self, context, connection):
100132 rid = int .from_bytes (member .getData (), byteorder = "little" )
101133 try :
102134 user_handle = samr .hSamrOpenUser (dce , domain_handle , samr .MAXIMUM_ALLOWED , rid )["UserHandle" ]
103- username = samr .hSamrQueryInformationUser2 (dce , user_handle , samr .USER_INFORMATION_CLASS .UserAllInformation )["Buffer" ]["All" ]["UserName" ]
135+ username = samr .hSamrQueryInformationUser2 (
136+ dce , user_handle , samr .USER_INFORMATION_CLASS .UserAllInformation
137+ )["Buffer" ]["All" ]["UserName" ]
104138
105139 # If user already exists, append group name
106140 if any (u ["sid" ] == f"{ self .domain_sid } -{ rid } " for u in admin_users ):
107141 user = next (u for u in admin_users if u ["sid" ] == f"{ self .domain_sid } -{ rid } " )
108142 user ["group" ].append (group_name )
109143 else :
110- 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 })
144+ admin_users .append (
145+ {
146+ "username" : username ,
147+ "sid" : f"{ self .domain_sid } -{ rid } " ,
148+ "domain" : domain ,
149+ "group" : [group_name ],
150+ "in_tasks" : False ,
151+ "in_directory" : False ,
152+ "in_scheduled_tasks" : False ,
153+ }
154+ )
111155 context .log .debug (f"Found user: { username } with RID { rid } in group { group_name } " )
112156 except Exception as e :
113157 context .log .debug (f"Failed to get user info for RID { rid } : { e !s} " )
@@ -151,19 +195,30 @@ def check_users_directory(self, context, connection, admin_users):
151195
152196 def check_tasklist (self , context , connection , admin_users ):
153197 """Checks tasklist over rpc."""
198+ # Choose a stable server name for the RPC layer (matches our SMB session)
199+ remote_name = (
200+ f"{ connection .hostname } .{ connection .domain } "
201+ if connection .kerberos else "*SMBSERVER"
202+ )
154203 try :
155- with TSTS .LegacyAPI (connection .conn , connection .remoteName , kerberos = connection .kerberos ) as legacy :
204+ # LegacyAPI reuses the existing SMB connection; give it the right name
205+ with TSTS .LegacyAPI (connection .conn , remote_name , kerberos = connection .kerberos ) as legacy :
156206 handle = legacy .hRpcWinStationOpenServer ()
157207 processes = legacy .hRpcWinStationGetAllProcesses (handle )
158208 except Exception as e :
209+ msg = str (e )
210+ # If the WinStation/TSTS pipe or object isn't there, just skip this check
211+ if "STATUS_OBJECT_NAME_NOT_FOUND" in msg or "0xc0000034" in msg .lower ():
212+ context .log .debug ("TSTS/WinStation endpoint not present; skipping tasklist enumeration" )
213+ return []
214+ # Other errors: log and continue without failing the whole module
159215 context .log .fail (f"Error in check_tasklist RPC method: { e } " )
160216 return []
161217
162218 context .log .debug (f"Enumerated { len (processes )} processes on { connection .host } " )
163219
164220 for process in processes :
165221 context .log .debug (f"ImageName: { process ['ImageName' ]} , UniqueProcessId: { process ['SessionId' ]} , pSid: { process ['pSid' ]} " )
166- # Check if process SID matches any admin user SID
167222 for user in admin_users :
168223 if process ["pSid" ] == user ["sid" ]:
169224 user ["in_tasks" ] = True
@@ -172,9 +227,21 @@ def check_tasklist(self, context, connection, admin_users):
172227 def check_scheduled_tasks (self , context , connection , admin_users ):
173228 """Checks scheduled tasks over rpc."""
174229 try :
175- target = connection .remoteName if connection .kerberos else connection .host
176- stringbinding = f"ncacn_np:{ target } [\\ pipe\\ atsvc]"
177- dce = self .get_dce_rpc (target , stringbinding , tsch .MSRPC_UUID_TSCHS , connection )
230+ tcp_ip = connection .host
231+ target_name = (
232+ f"{ connection .hostname } .{ connection .domain } "
233+ if connection .kerberos
234+ else tcp_ip
235+ )
236+ stringbinding = f"ncacn_np:{ target_name } [\\ pipe\\ atsvc]"
237+ dce = self .get_dce_rpc (
238+ tcp_host = tcp_ip ,
239+ string_binding = stringbinding ,
240+ dce_binding = tsch .MSRPC_UUID_TSCHS ,
241+ connection = connection ,
242+ remote_name = (f"{ connection .hostname } .{ connection .domain } "
243+ if connection .kerberos else "*SMBSERVER" ),
244+ )
178245
179246 # Also extract non admins where we can get the password
180247 self .non_admins = []
@@ -198,18 +265,36 @@ def check_scheduled_tasks(self, context, connection, admin_users):
198265 non_admin_sids .add (sid .group (1 ))
199266
200267 if non_admin_sids :
201- string_binding = fr"ncacn_np:{ connection .kdcHost } [\pipe\samr]"
202- dce = self .get_dce_rpc (connection .kdcHost , string_binding , samr .MSRPC_UUID_SAMR , connection )
268+ # Re-use SAMR with pinned TCP to IP and correct server name.
269+ tcp_ip = connection .host
270+ bind_name = (
271+ f"{ connection .hostname } .{ connection .domain } "
272+ if connection .kerberos
273+ else tcp_ip
274+ )
275+ string_binding = fr"ncacn_np:{ bind_name } [\pipe\samr]"
276+ dce = self .get_dce_rpc (
277+ tcp_host = tcp_ip ,
278+ string_binding = string_binding ,
279+ dce_binding = samr .MSRPC_UUID_SAMR ,
280+ connection = connection ,
281+ remote_name = (f"{ connection .hostname } .{ connection .domain } "
282+ if connection .kerberos else "*SMBSERVER" ),
283+ )
203284
204285 # Get Domain Handle
205286 server_handle = samr .hSamrConnect2 (dce )["ServerHandle" ]
206287 domain = samr .hSamrEnumerateDomainsInSamServer (dce , server_handle )["Buffer" ]["Buffer" ][0 ]["Name" ]
207288 domain_sid = samr .hSamrLookupDomainInSamServer (dce , server_handle , domain )["DomainId" ]
208- domain_handle = samr .hSamrOpenDomain (dce , server_handle , samr .DOMAIN_LOOKUP | samr .DOMAIN_LIST_ACCOUNTS , domain_sid )["DomainHandle" ]
289+ domain_handle = samr .hSamrOpenDomain (
290+ dce , server_handle , samr .DOMAIN_LOOKUP | samr .DOMAIN_LIST_ACCOUNTS , domain_sid
291+ )["DomainHandle" ]
209292
210293 for sid in non_admin_sids :
211294 user_handle = samr .hSamrOpenUser (dce , domain_handle , samr .MAXIMUM_ALLOWED , int (sid .split ("-" )[- 1 ]))["UserHandle" ]
212- username = samr .hSamrQueryInformationUser2 (dce , user_handle , samr .USER_INFORMATION_CLASS .UserAllInformation )["Buffer" ]["All" ]["UserName" ]
295+ username = samr .hSamrQueryInformationUser2 (
296+ dce , user_handle , samr .USER_INFORMATION_CLASS .UserAllInformation
297+ )["Buffer" ]["All" ]["UserName" ]
213298 self .non_admins .append (username )
214299
215300 except Exception as e :
@@ -241,7 +326,7 @@ def print_grouped_results(self, context, admin_users):
241326 context .log .success ("Found admins in scheduled tasks:" )
242327 for user in scheduled_tasks_users :
243328 context .log .highlight (f"{ user ['username' ]} ({ ', ' .join (user ['group' ])} )" )
244- if self . non_admins :
329+ if getattr ( self , " non_admins" , None ) :
245330 context .log .info (f"Found { len (self .non_admins )} non-admin scheduled tasks:" )
246331 for sid in self .non_admins :
247332 context .log .info (sid )
0 commit comments