11import os
2- import contextlib
3- from time import sleep
4- from datetime import datetime , timedelta
5- from impacket .dcerpc .v5 .dtypes import NULL
6- from impacket .dcerpc .v5 import tsch , transport
7- from nxc .helpers .misc import gen_random_string
8- from nxc .paths import TMP_PATH
9- from impacket .dcerpc .v5 .rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE , RPC_C_AUTHN_LEVEL_PKT_PRIVACY
2+ from traceback import format_exc
3+ from nxc .protocols .smb .atexec import TSCH_EXEC
104
115
126class NXCModule :
@@ -31,28 +25,28 @@ def options(self, context, module_options):
3125 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=whoami
3226 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='bin.exe --option' BINARY=bin.exe
3327 """
34- self .cmd = self .binary = self .user = self .task = self .file = self .location = self .time = None
28+ self .command_to_run = self .binary_to_upload = self .run_task_as = self .task_name = self .output_filename = self .output_file_location = self .time = None
3529 self .share = "C$"
3630 self .tmp_dir = "C:\\ Windows\\ Temp\\ "
3731 self .tmp_share = self .tmp_dir .split (":" )[1 ]
3832
3933 if "CMD" in module_options :
40- self .cmd = module_options ["CMD" ]
34+ self .command_to_run = module_options ["CMD" ]
4135
4236 if "BINARY" in module_options :
43- self .binary = module_options ["BINARY" ]
37+ self .binary_to_upload = module_options ["BINARY" ]
4438
4539 if "USER" in module_options :
46- self .user = module_options ["USER" ]
40+ self .run_task_as = module_options ["USER" ]
4741
4842 if "TASK" in module_options :
49- self .task = module_options ["TASK" ]
43+ self .task_name = module_options ["TASK" ]
5044
5145 if "FILE" in module_options :
52- self .file = module_options ["FILE" ]
46+ self .output_filename = module_options ["FILE" ]
5347
5448 if "LOCATION" in module_options :
55- self .location = module_options ["LOCATION" ]
49+ self .output_file_location = module_options ["LOCATION" ]
5650
5751 name = "schtask_as"
5852 description = "Remotely execute a scheduled task as a logged on user"
@@ -61,31 +55,31 @@ def options(self, context, module_options):
6155 def on_admin_login (self , context , connection ):
6256 self .logger = context .log
6357
64- if self .cmd is None :
58+ if self .command_to_run is None :
6559 self .logger .fail ("You need to specify a CMD to run" )
6660 return 1
6761
68- if self .user is None :
62+ if self .run_task_as is None :
6963 self .logger .fail ("You need to specify a USER to run the command as" )
7064 return 1
7165
72- if self .binary :
73- if not os .path .isfile (self .binary ):
74- self .logger .fail (f"Cannot find { self .binary } " )
66+ if self .binary_to_upload :
67+ if not os .path .isfile (self .binary_to_upload ):
68+ self .logger .fail (f"Cannot find { self .binary_to_upload } " )
7569 return 1
7670 else :
77- self .logger .display (f"Uploading { self .binary } " )
78- with open (self .binary , "rb" ) as binary_to_upload :
71+ self .logger .display (f"Uploading { self .binary_to_upload } " )
72+ with open (self .binary_to_upload , "rb" ) as binary_to_upload :
7973 try :
80- self .binary_name = os .path .basename (self .binary )
81- connection .conn .putFile (self .share , f"{ self .tmp_share } { self .binary_name } " , binary_to_upload .read )
82- self .logger .success (f"Binary { self .binary_name } successfully uploaded in { self .tmp_share } { self .binary_name } " )
74+ self .binary_to_upload_name = os .path .basename (self .binary_to_upload )
75+ connection .conn .putFile (self .share , f"{ self .tmp_share } { self .binary_to_upload_name } " , binary_to_upload .read )
76+ self .logger .success (f"Binary { self .binary_to_upload_name } successfully uploaded in { self .tmp_share } { self .binary_to_upload_name } " )
8377 except Exception as e :
8478 self .logger .fail (f"Error writing file to share { self .tmp_share } : { e } " )
8579 return 1
8680
87- # Returnes self.cmd or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
88- self .cmd = self .cmd if not self .binary else f"{ self .tmp_share } { self .cmd } "
81+ # Returnes self.command_to_run or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
82+ self .command_to_run = self .command_to_run if not self .binary_to_upload else f"{ self .tmp_share } { self .command_to_run } "
8983 self .logger .display ("Connecting to the remote Service control endpoint" )
9084 try :
9185 exec_method = TSCH_EXEC (
@@ -94,23 +88,23 @@ def on_admin_login(self, context, connection):
9488 connection .username ,
9589 connection .password ,
9690 connection .domain ,
97- self .user ,
98- self .cmd ,
99- self .file ,
100- self .task ,
101- self .location ,
10291 connection .kerberos ,
10392 connection .aesKey ,
10493 connection .host ,
10594 connection .kdcHost ,
10695 connection .hash ,
10796 self .logger ,
10897 connection .args .get_output_tries ,
109- "C$" , # This one shouldn't be hardcoded but I don't know where to retrieve the info
98+ connection .args .share ,
99+ self .run_task_as ,
100+ self .command_to_run ,
101+ self .output_filename ,
102+ self .task_name ,
103+ self .output_file_location ,
110104 )
111105
112- self .logger .display (f"Executing { self .cmd } as { self .user } " )
113- output = exec_method .execute (self .cmd , True )
106+ self .logger .display (f"Executing { self .command_to_run } as { self .run_task_as } " )
107+ output = exec_method .execute (self .command_to_run , True )
114108
115109 try :
116110 if not isinstance (output , str ):
@@ -122,246 +116,13 @@ def on_admin_login(self, context, connection):
122116 for line in output .splitlines ():
123117 self .logger .highlight (line .rstrip ())
124118
125- except Exception as e :
126- if "SCHED_S_TASK_HAS_NOT_RUN" in str (e ):
127- self .logger .fail ("Task was not run, seems like the specified user has no active session on the target" )
128- with contextlib .suppress (Exception ):
129- exec_method .deleteartifact ()
130- else :
131- self .logger .fail (f"Failed to execute command: { e } " )
119+ except Exception :
120+ self .logger .debug ("Error executing command via atexec, traceback:" )
121+ self .logger .debug (format_exc ())
132122 finally :
133- if self .binary :
123+ if self .binary_to_upload :
134124 try :
135- connection .conn .deleteFile (self .share , f"{ self .tmp_share } { self .binary_name } " )
136- context .log .success (f"Binary { self .binary_name } successfully deleted" )
125+ connection .conn .deleteFile (self .share , f"{ self .tmp_share } { self .binary_to_upload_name } " )
126+ context .log .success (f"Binary { self .binary_to_upload_name } successfully deleted" )
137127 except Exception as e :
138- context .log .fail (f"Error deleting { self .binary_name } on { self .share } : { e } " )
139-
140-
141- class TSCH_EXEC :
142- def __init__ (self , target , share_name , username , password , domain , user , cmd , file , task , location , doKerberos = False , aesKey = None , remoteHost = None , kdcHost = None , hashes = None , logger = None , tries = None , share = None ):
143- self .__target = target
144- self .__username = username
145- self .__password = password
146- self .__domain = domain
147- self .__share_name = share_name
148- self .__lmhash = ""
149- self .__nthash = ""
150- self .__outputBuffer = b""
151- self .__retOutput = False
152- self .__aesKey = aesKey
153- self .__doKerberos = doKerberos
154- self .__remoteHost = remoteHost
155- self .__kdcHost = kdcHost
156- self .__tries = tries
157- self .__output_filename = None
158- self .__share = share
159- self .logger = logger
160- self .cmd = cmd
161- self .user = user
162- self .file = file
163- self .task = task
164- self .location = location
165-
166- if hashes is not None :
167- if hashes .find (":" ) != - 1 :
168- self .__lmhash , self .__nthash = hashes .split (":" )
169- else :
170- self .__nthash = hashes
171-
172- if self .__password is None :
173- self .__password = ""
174-
175- stringbinding = f"ncacn_np:{ self .__target } [\\ pipe\\ atsvc]"
176- self .__rpctransport = transport .DCERPCTransportFactory (stringbinding )
177- self .__rpctransport .setRemoteHost (self .__remoteHost )
178-
179- if hasattr (self .__rpctransport , "set_credentials" ):
180- # This method exists only for selected protocol sequences.
181- self .__rpctransport .set_credentials (
182- self .__username ,
183- self .__password ,
184- self .__domain ,
185- self .__lmhash ,
186- self .__nthash ,
187- self .__aesKey ,
188- )
189- self .__rpctransport .set_kerberos (self .__doKerberos , self .__kdcHost )
190-
191- def deleteartifact (self ):
192- dce = self .__rpctransport .get_dce_rpc ()
193- if self .__doKerberos :
194- dce .set_auth_type (RPC_C_AUTHN_GSS_NEGOTIATE )
195- dce .set_credentials (* self .__rpctransport .get_credentials ())
196- dce .connect ()
197- dce .set_auth_level (RPC_C_AUTHN_LEVEL_PKT_PRIVACY )
198- dce .bind (tsch .MSRPC_UUID_TSCHS )
199- self .logger .display (f"Deleting task \\ { self .task } " )
200- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
201- dce .disconnect ()
202-
203- def execute (self , command , output = False ):
204- self .__retOutput = output
205- self .execute_handler (command )
206- return self .__outputBuffer
207-
208- def output_callback (self , data ):
209- self .__outputBuffer = data
210-
211- def get_end_boundary (self ):
212- # Get current date and time + 5 minutes
213- end_boundary = datetime .now () + timedelta (minutes = 5 )
214-
215- # Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
216- return end_boundary .strftime ("%Y-%m-%dT%H:%M:%S.%f" )[:- 3 ]
217-
218- def gen_xml (self , command , fileless = False ):
219- xml = f"""<?xml version="1.0" encoding="UTF-16"?>
220- <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
221- <Triggers>
222- <RegistrationTrigger>
223- <EndBoundary>{ self .get_end_boundary ()} </EndBoundary>
224- </RegistrationTrigger>
225- </Triggers>
226- <Principals>
227- <Principal id="LocalSystem">
228- <UserId>{ self .user } </UserId>
229- <RunLevel>HighestAvailable</RunLevel>
230- </Principal>
231- </Principals>
232- <Settings>
233- <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
234- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
235- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
236- <AllowHardTerminate>true</AllowHardTerminate>
237- <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
238- <IdleSettings>
239- <StopOnIdleEnd>true</StopOnIdleEnd>
240- <RestartOnIdle>false</RestartOnIdle>
241- </IdleSettings>
242- <AllowStartOnDemand>true</AllowStartOnDemand>
243- <Enabled>true</Enabled>
244- <Hidden>true</Hidden>
245- <RunOnlyIfIdle>false</RunOnlyIfIdle>
246- <WakeToRun>false</WakeToRun>
247- <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
248- <Priority>7</Priority>
249- </Settings>
250- <Actions Context="LocalSystem">
251- <Exec>
252- <Command>cmd.exe</Command>
253- """
254- if self .__retOutput :
255- fileLocation = "\\ Windows\\ Temp\\ " if self .location is None else self .location
256- if self .file is None :
257- self .__output_filename = os .path .join (fileLocation , gen_random_string (6 ))
258- else :
259- self .__output_filename = os .path .join (fileLocation , self .file )
260- if fileless :
261- local_ip = self .__rpctransport .get_socket ().getsockname ()[0 ]
262- argument_xml = f" <Arguments>/C { command } > \\ \\ { local_ip } \\ { self .__share_name } \\ { self .__output_filename } 2>&1</Arguments>"
263- else :
264- argument_xml = f" <Arguments>/C { command } > { self .__output_filename } 2>&1</Arguments>"
265-
266- elif self .__retOutput is False :
267- argument_xml = f" <Arguments>/C { command } </Arguments>"
268-
269- self .logger .debug (f"Generated argument XML: { argument_xml } " )
270- xml += argument_xml
271- xml += """
272- </Exec>
273- </Actions>
274- </Task>
275- """
276- return xml
277-
278- def execute_handler (self , command , fileless = False ):
279- dce = self .__rpctransport .get_dce_rpc ()
280-
281- if self .__doKerberos :
282- dce .set_auth_type (RPC_C_AUTHN_GSS_NEGOTIATE )
283-
284- dce .set_credentials (* self .__rpctransport .get_credentials ())
285- dce .connect ()
286- # Give self.task a random string as name if not already specified
287- self .task = gen_random_string (8 ) if self .task is None else self .task
288- xml = self .gen_xml (command , fileless )
289-
290- self .logger .info (f"Task XML: { xml } " )
291- self .logger .info (f"Creating task \\ { self .task } " )
292- try :
293- # windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
294- dce .set_auth_level (RPC_C_AUTHN_LEVEL_PKT_PRIVACY )
295- dce .bind (tsch .MSRPC_UUID_TSCHS )
296- tsch .hSchRpcRegisterTask (dce , f"\\ { self .task } " , xml , tsch .TASK_CREATE , NULL , tsch .TASK_LOGON_NONE )
297- except Exception as e :
298- if "ERROR_NONE_MAPPED" in str (e ):
299- self .logger .fail (f"User { self .user } is not connected on the target, cannot run the task" )
300- with contextlib .suppress (Exception ):
301- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
302- elif e .error_code and hex (e .error_code ) == "0x80070005" :
303- self .logger .fail ("Create schedule task got blocked." )
304- with contextlib .suppress (Exception ):
305- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
306- elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str (e ):
307- self .logger .fail (f"User { self .user } does not exist in the domain." )
308- with contextlib .suppress (Exception ):
309- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
310- elif "SCHED_S_TASK_HAS_NOT_RUN" in str (e ):
311- with contextlib .suppress (Exception ):
312- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
313- elif "ERROR_ALREADY_EXISTS" in str (e ):
314- self .logger .fail (f"Create schedule task failed: { e } " )
315- else :
316- self .logger .fail (f"Create schedule task failed: { e } " )
317- with contextlib .suppress (Exception ):
318- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
319- return
320-
321- done = False
322- while not done :
323- self .logger .debug (f"Calling SchRpcGetLastRunInfo for \\ { self .task } " )
324- resp = tsch .hSchRpcGetLastRunInfo (dce , f"\\ { self .task } " )
325- if resp ["pLastRuntime" ]["wYear" ] != 0 :
326- done = True
327- else :
328- sleep (2 )
329-
330- self .logger .info (f"Deleting task \\ { self .task } " )
331- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
332-
333- if self .__retOutput :
334- if fileless :
335- while True :
336- try :
337- with open (os .path .join (TMP_PATH , self .__output_filename )) as output :
338- self .output_callback (output .read ())
339- break
340- except OSError :
341- sleep (2 )
342- else :
343- smbConnection = self .__rpctransport .get_smb_connection ()
344- tries = 1
345- while True :
346- try :
347- self .logger .info (f"Attempting to read { self .__share } \\ { self .__output_filename } " )
348- smbConnection .getFile (self .__share , self .__output_filename , self .output_callback )
349- break
350- except Exception as e :
351- if tries >= self .__tries :
352- self .logger .fail ("Schtask_as: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'." )
353- break
354- if "STATUS_BAD_NETWORK_NAME" in str (e ):
355- self .logger .fail (f"Schtask_as: Getting the output file failed - target has blocked access to the share: { self .__share } (but the command may have executed!)" )
356- break
357- if "SHARING" in str (e ) or "STATUS_OBJECT_NAME_NOT_FOUND" in str (e ):
358- sleep (3 )
359- tries += 1
360- else :
361- self .logger .debug (str (e ))
362-
363- if self .__outputBuffer :
364- self .logger .debug (f"Deleting file { self .__share } \\ { self .__output_filename } " )
365- smbConnection .deleteFile (self .__share , self .__output_filename )
366-
367- dce .disconnect ()
128+ context .log .fail (f"Error deleting { self .binary_to_upload_name } on { self .share } : { e } " )
0 commit comments