11import os
22import 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
3+ from traceback import format_exc
4+ from nxc .protocols .smb .atexec import TSCH_EXEC
105
116
127class NXCModule :
@@ -31,62 +26,63 @@ def options(self, context, module_options):
3126 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=whoami
3227 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='bin.exe --option' BINARY=bin.exe
3328 """
34- self .cmd = self .binary = self .user = self .task = self .file = self .location = self .time = None
29+ 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
3530 self .share = "C$"
3631 self .tmp_dir = "C:\\ Windows\\ Temp\\ "
3732 self .tmp_share = self .tmp_dir .split (":" )[1 ]
3833
3934 if "CMD" in module_options :
40- self .cmd = module_options ["CMD" ]
35+ self .command_to_run = module_options ["CMD" ]
4136
4237 if "BINARY" in module_options :
43- self .binary = module_options ["BINARY" ]
38+ self .binary_to_upload = module_options ["BINARY" ]
4439
4540 if "USER" in module_options :
46- self .user = module_options ["USER" ]
41+ self .run_task_as = module_options ["USER" ]
4742
4843 if "TASK" in module_options :
49- self .task = module_options ["TASK" ]
44+ self .task_name = module_options ["TASK" ]
5045
5146 if "FILE" in module_options :
52- self .file = module_options ["FILE" ]
47+ self .output_filename = module_options ["FILE" ]
5348
5449 if "LOCATION" in module_options :
55- self .location = module_options ["LOCATION" ]
50+ self .output_file_location = module_options ["LOCATION" ]
5651
5752 name = "schtask_as"
5853 description = "Remotely execute a scheduled task as a logged on user"
5954 supported_protocols = ["smb" ]
6055 multiple_hosts = False
6156
6257 def on_admin_login (self , context , connection ):
58+ print (vars (connection ))
6359 self .logger = context .log
6460
65- if self .cmd is None :
61+ if self .command_to_run is None :
6662 self .logger .fail ("You need to specify a CMD to run" )
6763 return 1
6864
69- if self .user is None :
65+ if self .run_task_as is None :
7066 self .logger .fail ("You need to specify a USER to run the command as" )
7167 return 1
7268
73- if self .binary :
74- if not os .path .isfile (self .binary ):
75- self .logger .fail (f"Cannot find { self .binary } " )
69+ if self .binary_to_upload :
70+ if not os .path .isfile (self .binary_to_upload ):
71+ self .logger .fail (f"Cannot find { self .binary_to_upload } " )
7672 return 1
7773 else :
78- self .logger .display (f"Uploading { self .binary } " )
79- with open (self .binary , "rb" ) as binary_to_upload :
74+ self .logger .display (f"Uploading { self .binary_to_upload } " )
75+ with open (self .binary_to_upload , "rb" ) as binary_to_upload :
8076 try :
81- self .binary_name = os .path .basename (self .binary )
82- connection .conn .putFile (self .share , f"{ self .tmp_share } { self .binary_name } " , binary_to_upload .read )
83- self .logger .success (f"Binary { self .binary_name } successfully uploaded in { self .tmp_share } { self .binary_name } " )
77+ self .binary_to_upload_name = os .path .basename (self .binary_to_upload )
78+ connection .conn .putFile (self .share , f"{ self .tmp_share } { self .binary_to_upload_name } " , binary_to_upload .read )
79+ self .logger .success (f"Binary { self .binary_to_upload_name } successfully uploaded in { self .tmp_share } { self .binary_to_upload_name } " )
8480 except Exception as e :
8581 self .logger .fail (f"Error writing file to share { self .tmp_share } : { e } " )
8682 return 1
8783
88- # Returnes self.cmd or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
89- self .cmd = self .cmd if not self .binary else f"{ self .tmp_share } { self .cmd } "
84+ # Returnes self.command_to_run or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
85+ self .command_to_run = self .command_to_run if not self .binary_to_upload else f"{ self .tmp_share } { self .command_to_run } "
9086 self .logger .display ("Connecting to the remote Service control endpoint" )
9187 try :
9288 exec_method = TSCH_EXEC (
@@ -95,23 +91,23 @@ def on_admin_login(self, context, connection):
9591 connection .username ,
9692 connection .password ,
9793 connection .domain ,
98- self .user ,
99- self .cmd ,
100- self .file ,
101- self .task ,
102- self .location ,
10394 connection .kerberos ,
10495 connection .aesKey ,
10596 connection .host ,
10697 connection .kdcHost ,
10798 connection .hash ,
10899 self .logger ,
109100 connection .args .get_output_tries ,
110- "C$" , # This one shouldn't be hardcoded but I don't know where to retrieve the info
101+ connection .args .share ,
102+ self .run_task_as ,
103+ self .command_to_run ,
104+ self .output_filename ,
105+ self .task_name ,
106+ self .output_file_location ,
111107 )
112108
113- self .logger .display (f"Executing { self .cmd } as { self .user } " )
114- output = exec_method .execute (self .cmd , True )
109+ self .logger .display (f"Executing { self .command_to_run } as { self .run_task_as } " )
110+ output = exec_method .execute (self .command_to_run , True )
115111
116112 try :
117113 if not isinstance (output , str ):
@@ -123,246 +119,15 @@ def on_admin_login(self, context, connection):
123119 for line in output .splitlines ():
124120 self .logger .highlight (line .rstrip ())
125121
126- except Exception as e :
127- if "SCHED_S_TASK_HAS_NOT_RUN" in str (e ):
128- self .logger .fail ("Task was not run, seems like the specified user has no active session on the target" )
129- with contextlib .suppress (Exception ):
130- exec_method .deleteartifact ()
131- else :
132- self .logger .fail (f"Failed to execute command: { e } " )
122+ except Exception :
123+ self .logger .debug ("Error executing command via atexec, traceback:" )
124+ self .logger .debug (format_exc ())
125+ with contextlib .suppress (Exception ):
126+ exec_method .deleteartifact ()
133127 finally :
134- if self .binary :
128+ if self .binary_to_upload :
135129 try :
136- connection .conn .deleteFile (self .share , f"{ self .tmp_share } { self .binary_name } " )
137- context .log .success (f"Binary { self .binary_name } successfully deleted" )
130+ connection .conn .deleteFile (self .share , f"{ self .tmp_share } { self .binary_to_upload_name } " )
131+ context .log .success (f"Binary { self .binary_to_upload_name } successfully deleted" )
138132 except Exception as e :
139- context .log .fail (f"Error deleting { self .binary_name } on { self .share } : { e } " )
140-
141-
142- class TSCH_EXEC :
143- 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 ):
144- self .__target = target
145- self .__username = username
146- self .__password = password
147- self .__domain = domain
148- self .__share_name = share_name
149- self .__lmhash = ""
150- self .__nthash = ""
151- self .__outputBuffer = b""
152- self .__retOutput = False
153- self .__aesKey = aesKey
154- self .__doKerberos = doKerberos
155- self .__remoteHost = remoteHost
156- self .__kdcHost = kdcHost
157- self .__tries = tries
158- self .__output_filename = None
159- self .__share = share
160- self .logger = logger
161- self .cmd = cmd
162- self .user = user
163- self .file = file
164- self .task = task
165- self .location = location
166-
167- if hashes is not None :
168- if hashes .find (":" ) != - 1 :
169- self .__lmhash , self .__nthash = hashes .split (":" )
170- else :
171- self .__nthash = hashes
172-
173- if self .__password is None :
174- self .__password = ""
175-
176- stringbinding = f"ncacn_np:{ self .__target } [\\ pipe\\ atsvc]"
177- self .__rpctransport = transport .DCERPCTransportFactory (stringbinding )
178- self .__rpctransport .setRemoteHost (self .__remoteHost )
179-
180- if hasattr (self .__rpctransport , "set_credentials" ):
181- # This method exists only for selected protocol sequences.
182- self .__rpctransport .set_credentials (
183- self .__username ,
184- self .__password ,
185- self .__domain ,
186- self .__lmhash ,
187- self .__nthash ,
188- self .__aesKey ,
189- )
190- self .__rpctransport .set_kerberos (self .__doKerberos , self .__kdcHost )
191-
192- def deleteartifact (self ):
193- dce = self .__rpctransport .get_dce_rpc ()
194- if self .__doKerberos :
195- dce .set_auth_type (RPC_C_AUTHN_GSS_NEGOTIATE )
196- dce .set_credentials (* self .__rpctransport .get_credentials ())
197- dce .connect ()
198- dce .set_auth_level (RPC_C_AUTHN_LEVEL_PKT_PRIVACY )
199- dce .bind (tsch .MSRPC_UUID_TSCHS )
200- self .logger .display (f"Deleting task \\ { self .task } " )
201- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
202- dce .disconnect ()
203-
204- def execute (self , command , output = False ):
205- self .__retOutput = output
206- self .execute_handler (command )
207- return self .__outputBuffer
208-
209- def output_callback (self , data ):
210- self .__outputBuffer = data
211-
212- def get_end_boundary (self ):
213- # Get current date and time + 5 minutes
214- end_boundary = datetime .now () + timedelta (minutes = 5 )
215-
216- # Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
217- return end_boundary .strftime ("%Y-%m-%dT%H:%M:%S.%f" )[:- 3 ]
218-
219- def gen_xml (self , command , fileless = False ):
220- xml = f"""<?xml version="1.0" encoding="UTF-16"?>
221- <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
222- <Triggers>
223- <RegistrationTrigger>
224- <EndBoundary>{ self .get_end_boundary ()} </EndBoundary>
225- </RegistrationTrigger>
226- </Triggers>
227- <Principals>
228- <Principal id="LocalSystem">
229- <UserId>{ self .user } </UserId>
230- <RunLevel>HighestAvailable</RunLevel>
231- </Principal>
232- </Principals>
233- <Settings>
234- <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
235- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
236- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
237- <AllowHardTerminate>true</AllowHardTerminate>
238- <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
239- <IdleSettings>
240- <StopOnIdleEnd>true</StopOnIdleEnd>
241- <RestartOnIdle>false</RestartOnIdle>
242- </IdleSettings>
243- <AllowStartOnDemand>true</AllowStartOnDemand>
244- <Enabled>true</Enabled>
245- <Hidden>true</Hidden>
246- <RunOnlyIfIdle>false</RunOnlyIfIdle>
247- <WakeToRun>false</WakeToRun>
248- <ExecutionTimeLimit>P3D</ExecutionTimeLimit>
249- <Priority>7</Priority>
250- </Settings>
251- <Actions Context="LocalSystem">
252- <Exec>
253- <Command>cmd.exe</Command>
254- """
255- if self .__retOutput :
256- fileLocation = "\\ Windows\\ Temp\\ " if self .location is None else self .location
257- if self .file is None :
258- self .__output_filename = os .path .join (fileLocation , gen_random_string (6 ))
259- else :
260- self .__output_filename = os .path .join (fileLocation , self .file )
261- if fileless :
262- local_ip = self .__rpctransport .get_socket ().getsockname ()[0 ]
263- argument_xml = f" <Arguments>/C { command } > \\ \\ { local_ip } \\ { self .__share_name } \\ { self .__output_filename } 2>&1</Arguments>"
264- else :
265- argument_xml = f" <Arguments>/C { command } > { self .__output_filename } 2>&1</Arguments>"
266-
267- elif self .__retOutput is False :
268- argument_xml = f" <Arguments>/C { command } </Arguments>"
269-
270- self .logger .debug (f"Generated argument XML: { argument_xml } " )
271- xml += argument_xml
272- xml += """
273- </Exec>
274- </Actions>
275- </Task>
276- """
277- return xml
278-
279- def execute_handler (self , command , fileless = False ):
280- dce = self .__rpctransport .get_dce_rpc ()
281-
282- if self .__doKerberos :
283- dce .set_auth_type (RPC_C_AUTHN_GSS_NEGOTIATE )
284-
285- dce .set_credentials (* self .__rpctransport .get_credentials ())
286- dce .connect ()
287- # Give self.task a random string as name if not already specified
288- self .task = gen_random_string (8 ) if self .task is None else self .task
289- xml = self .gen_xml (command , fileless )
290-
291- self .logger .info (f"Task XML: { xml } " )
292- self .logger .info (f"Creating task \\ { self .task } " )
293- try :
294- # windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
295- dce .set_auth_level (RPC_C_AUTHN_LEVEL_PKT_PRIVACY )
296- dce .bind (tsch .MSRPC_UUID_TSCHS )
297- tsch .hSchRpcRegisterTask (dce , f"\\ { self .task } " , xml , tsch .TASK_CREATE , NULL , tsch .TASK_LOGON_NONE )
298- except Exception as e :
299- if "ERROR_NONE_MAPPED" in str (e ):
300- self .logger .fail (f"User { self .user } is not connected on the target, cannot run the task" )
301- with contextlib .suppress (Exception ):
302- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
303- elif e .error_code and hex (e .error_code ) == "0x80070005" :
304- self .logger .fail ("Create schedule task got blocked." )
305- with contextlib .suppress (Exception ):
306- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
307- elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str (e ):
308- self .logger .fail (f"User { self .user } does not exist in the domain." )
309- with contextlib .suppress (Exception ):
310- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
311- elif "SCHED_S_TASK_HAS_NOT_RUN" in str (e ):
312- with contextlib .suppress (Exception ):
313- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
314- elif "ERROR_ALREADY_EXISTS" in str (e ):
315- self .logger .fail (f"Create schedule task failed: { e } " )
316- else :
317- self .logger .fail (f"Create schedule task failed: { e } " )
318- with contextlib .suppress (Exception ):
319- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
320- return
321-
322- done = False
323- while not done :
324- self .logger .debug (f"Calling SchRpcGetLastRunInfo for \\ { self .task } " )
325- resp = tsch .hSchRpcGetLastRunInfo (dce , f"\\ { self .task } " )
326- if resp ["pLastRuntime" ]["wYear" ] != 0 :
327- done = True
328- else :
329- sleep (2 )
330-
331- self .logger .info (f"Deleting task \\ { self .task } " )
332- tsch .hSchRpcDelete (dce , f"\\ { self .task } " )
333-
334- if self .__retOutput :
335- if fileless :
336- while True :
337- try :
338- with open (os .path .join (TMP_PATH , self .__output_filename )) as output :
339- self .output_callback (output .read ())
340- break
341- except OSError :
342- sleep (2 )
343- else :
344- smbConnection = self .__rpctransport .get_smb_connection ()
345- tries = 1
346- while True :
347- try :
348- self .logger .info (f"Attempting to read { self .__share } \\ { self .__output_filename } " )
349- smbConnection .getFile (self .__share , self .__output_filename , self .output_callback )
350- break
351- except Exception as e :
352- if tries >= self .__tries :
353- 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'." )
354- break
355- if "STATUS_BAD_NETWORK_NAME" in str (e ):
356- 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!)" )
357- break
358- if "SHARING" in str (e ) or "STATUS_OBJECT_NAME_NOT_FOUND" in str (e ):
359- sleep (3 )
360- tries += 1
361- else :
362- self .logger .debug (str (e ))
363-
364- if self .__outputBuffer :
365- self .logger .debug (f"Deleting file { self .__share } \\ { self .__output_filename } " )
366- smbConnection .deleteFile (self .__share , self .__output_filename )
367-
368- dce .disconnect ()
133+ context .log .fail (f"Error deleting { self .binary_to_upload_name } on { self .share } : { e } " )
0 commit comments