Skip to content

Commit a083405

Browse files
authored
Merge branch 'Pennyw0rth:main' into dc-list
2 parents a517be8 + 6aa7d6a commit a083405

6 files changed

Lines changed: 75 additions & 57 deletions

File tree

nxc/connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def parse_credentials(self):
389389
if "\\" in line and len(line.split("\\")) == 2:
390390
domain_single, username_single = line.split("\\")
391391
else:
392-
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
392+
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
393393
username_single = line
394394
domain.append(domain_single)
395395
username.append(username_single.strip())
@@ -398,7 +398,7 @@ def parse_credentials(self):
398398
if "\\" in user:
399399
domain_single, username_single = user.split("\\")
400400
else:
401-
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
401+
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
402402
username_single = user
403403
domain.append(domain_single)
404404
username.append(username_single)

nxc/helpers/pfx.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,8 @@ def pfx_auth(self):
503503
return None
504504

505505
username = self.args.username[0]
506-
log_ccache = os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache".replace(":", "-"))
506+
timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H%M%S").replace(":", "-")
507+
log_ccache = os.path.normpath(os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{timestamp}-{username}.ccache"))
507508

508509
# Request a TGT with the cert data
509510
req = ini.build_asreq(self.domain, username)

nxc/modules/schtask_as.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import contextlib
21
import os
2+
import contextlib
33
from time import sleep
44
from datetime import datetime, timedelta
55
from impacket.dcerpc.v5.dtypes import NULL
@@ -13,20 +13,35 @@ class NXCModule:
1313
"""
1414
Execute a scheduled task remotely as a already connected user by @Defte_
1515
Thanks @Shad0wC0ntr0ller for the idea of removing the hardcoded date that could be used as an IOC
16+
Modified by @Defte_ so that output on multiples lines are printed correctly (28/04/2025)
17+
Modified by @Defte_ so that we can upload a custom binary to execute using the BINARY option (28/04/2025)
1618
"""
1719

1820
def options(self, context, module_options):
1921
r"""
22+
BINARY Upload the binary to be executed by CMD
2023
CMD Command to execute
2124
USER User to execute command as
2225
TASK OPTIONAL: Set a name for the scheduled task name
2326
FILE OPTIONAL: Set a name for the command output file
2427
LOCATION OPTIONAL: Set a location for the command output file (e.g. '\tmp\')
28+
29+
Example:
30+
-------
31+
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD=whoami
32+
nxc smb <ip> -u <user> -p <password> -M schtask_as -o USER=Administrator CMD='bin.exe --option' BINARY=bin.exe
2533
"""
26-
self.cmd = self.user = self.task = self.file = self.location = self.time = None
34+
self.cmd = self.binary = self.user = self.task = self.file = self.location = self.time = None
35+
self.share = "C$"
36+
self.tmp_dir = "C:\\Windows\\Temp\\"
37+
self.tmp_share = self.tmp_dir.split(":")[1]
38+
2739
if "CMD" in module_options:
2840
self.cmd = module_options["CMD"]
2941

42+
if "BINARY" in module_options:
43+
self.binary = module_options["BINARY"]
44+
3045
if "USER" in module_options:
3146
self.user = module_options["USER"]
3247

@@ -47,13 +62,32 @@ def options(self, context, module_options):
4762

4863
def on_admin_login(self, context, connection):
4964
self.logger = context.log
65+
5066
if self.cmd is None:
5167
self.logger.fail("You need to specify a CMD to run")
5268
return 1
69+
5370
if self.user is None:
5471
self.logger.fail("You need to specify a USER to run the command as")
5572
return 1
5673

74+
if self.binary:
75+
if not os.path.isfile(self.binary):
76+
self.logger.fail(f"Cannot find {self.binary}")
77+
return 1
78+
else:
79+
self.logger.display(f"Uploading {self.binary}")
80+
with open(self.binary, "rb") as binary_to_upload:
81+
try:
82+
self.binary_name = os.path.basename(self.binary)
83+
connection.conn.putFile(self.share, f"{self.tmp_share}{self.binary_name}", binary_to_upload.read)
84+
self.logger.success(f"Binary {self.binary_name} successfully uploaded in {self.tmp_share}{self.binary_name}")
85+
except Exception as e:
86+
self.logger.fail(f"Error writing file to share {self.tmp_share}: {e}")
87+
return 1
88+
89+
# Returnes self.cmd or \Windows\temp\BinToExecute.exe depending if BINARY=BinToExecute.exe
90+
self.cmd = self.cmd if not self.binary else f"{self.tmp_share}{self.cmd}"
5791
self.logger.display("Connecting to the remote Service control endpoint")
5892
try:
5993
exec_method = TSCH_EXEC(
@@ -87,7 +121,8 @@ def on_admin_login(self, context, connection):
87121
# Required to decode specific French characters otherwise it'll print b"<result>"
88122
output = output.decode("cp437")
89123
if output:
90-
self.logger.highlight(output)
124+
for line in output.splitlines():
125+
self.logger.highlight(line.rstrip())
91126

92127
except Exception as e:
93128
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
@@ -96,6 +131,13 @@ def on_admin_login(self, context, connection):
96131
exec_method.deleteartifact()
97132
else:
98133
self.logger.fail(f"Failed to execute command: {e}")
134+
finally:
135+
if self.binary:
136+
try:
137+
connection.conn.deleteFile(self.share, f"{self.tmp_share}{self.binary_name}")
138+
context.log.success(f"Binary {self.binary_name} successfully deleted")
139+
except Exception as e:
140+
context.log.fail(f"Error deleting {self.binary_name} on {self.share}: {e}")
99141

100142

101143
class TSCH_EXEC:

nxc/protocols/mssql.py

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def check_if_admin(self):
8989
else:
9090
if is_admin:
9191
self.admin_privs = True
92-
92+
9393
@reconnect_mssql
9494
def enum_host_info(self):
9595
challenge = None
@@ -102,7 +102,7 @@ def enum_host_info(self):
102102
login["ClientPID"] = random.randint(0, 1024)
103103
login["PacketSize"] = self.conn.packetSize
104104
login["OptionFlags2"] = tds.TDS_INIT_LANG_FATAL | tds.TDS_ODBC_ON | tds.TDS_INTEGRATED_SECURITY_ON
105-
105+
106106
# NTLMSSP Negotiate
107107
auth = ntlm.getNTLMSSPType1("", "")
108108
login["SSPI"] = auth.getData()
@@ -144,16 +144,7 @@ def print_host_info(self):
144144
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})")
145145

146146
@reconnect_mssql
147-
def kerberos_login(
148-
self,
149-
domain,
150-
username,
151-
password="",
152-
ntlm_hash="",
153-
aesKey="",
154-
kdcHost="",
155-
useCache=False,
156-
):
147+
def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False):
157148
self.username = username
158149
self.password = password
159150
self.domain = domain
@@ -200,29 +191,21 @@ def kerberos_login(
200191
return False
201192
except Exception:
202193
error_msg = self.handle_mssql_reply()
203-
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, kerb_pass, error_msg if error_msg else ""))
194+
self.logger.fail(f"{self.domain}\\{self.username}:{used_ccache} {error_msg if error_msg else ''}")
204195
return False
205196

206197
@reconnect_mssql
207198
def plaintext_login(self, domain, username, password):
208199
self.password = password
209200
self.username = username
210201
self.domain = domain
211-
202+
212203
try:
213-
res = self.conn.login(
214-
None,
215-
self.username,
216-
self.password,
217-
self.domain,
218-
None,
219-
not self.args.local_auth,
220-
)
204+
res = self.conn.login(None, self.username, self.password, self.domain, None, not self.args.local_auth)
221205
if res is not True:
222206
raise
223207
self.check_if_admin()
224-
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
225-
self.logger.success(out)
208+
self.logger.success(f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}")
226209
if not self.args.local_auth and self.username != "":
227210
add_user_bh(self.username, self.domain, self.logger, self.config)
228211
if self.admin_privs:
@@ -233,7 +216,7 @@ def plaintext_login(self, domain, username, password):
233216
return False
234217
except Exception:
235218
error_msg = self.handle_mssql_reply()
236-
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.password), error_msg if error_msg else ""))
219+
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {error_msg if error_msg else ''}")
237220
return False
238221

239222
@reconnect_mssql
@@ -242,26 +225,18 @@ def hash_login(self, domain, username, ntlm_hash):
242225
self.domain = domain
243226
self.lmhash = ""
244227
self.nthash = ""
245-
228+
246229
if ntlm_hash.find(":") != -1:
247230
self.lmhash, self.nthash = ntlm_hash.split(":")
248231
else:
249232
self.nthash = ntlm_hash
250233

251234
try:
252-
res = self.conn.login(
253-
None,
254-
self.username,
255-
"",
256-
self.domain,
257-
f"{self.lmhash}:{self.nthash}",
258-
not self.args.local_auth,
259-
)
235+
res = self.conn.login(None, self.username, "", self.domain, f"{self.lmhash}:{self.nthash}", not self.args.local_auth)
260236
if res is not True:
261237
raise
262238
self.check_if_admin()
263-
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
264-
self.logger.success(out)
239+
self.logger.success(f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}")
265240
if not self.args.local_auth and self.username != "":
266241
add_user_bh(self.username, self.domain, self.logger, self.config)
267242
if self.admin_privs:
@@ -272,7 +247,7 @@ def hash_login(self, domain, username, ntlm_hash):
272247
return False
273248
except Exception:
274249
error_msg = self.handle_mssql_reply()
275-
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.nthash), error_msg if error_msg else ""))
250+
self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {error_msg if error_msg else ''}")
276251
return False
277252

278253
def mssql_query(self):
@@ -305,10 +280,10 @@ def execute(self, payload=None, get_output=False):
305280
if not payload:
306281
self.logger.error("No command to execute specified!")
307282
return None
308-
283+
309284
get_output = True if not self.args.no_output else get_output
310285
self.logger.debug(f"{get_output=}")
311-
286+
312287
try:
313288
exec_method = MSSQLEXEC(self.conn, self.logger)
314289
output = exec_method.execute(payload)
@@ -317,7 +292,7 @@ def execute(self, payload=None, get_output=False):
317292
self.logger.fail(f"Execute command failed, error: {e!s}")
318293
return False
319294
else:
320-
self.logger.success("Executed command via mssqlexec")
295+
self.logger.success("Executed command via mssqlexec")
321296
if output:
322297
output_lines = StringIO(output).readlines()
323298
for line in output_lines:
@@ -330,24 +305,24 @@ def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=Fa
330305
if not payload:
331306
self.logger.error("No command to execute specified!")
332307
return None
333-
308+
334309
response = []
335310
obfs = obfs if obfs else self.args.obfs
336311
encode = encode if encode else not self.args.no_encode
337312
force_ps32 = force_ps32 if force_ps32 else self.args.force_ps32
338313
get_output = True if not self.args.no_output else get_output
339-
314+
340315
self.logger.debug(f"Starting PS execute: {payload=} {get_output=} {methods=} {force_ps32=} {obfs=} {encode=}")
341316
amsi_bypass = self.args.amsi_bypass[0] if self.args.amsi_bypass else None
342317
self.logger.debug(f"AMSI Bypass: {amsi_bypass}")
343-
318+
344319
if os.path.isfile(payload):
345320
self.logger.debug(f"File payload set: {payload}")
346321
with open(payload) as commands:
347322
response = [self.execute(create_ps_command(c.strip(), force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output) for c in commands]
348323
else:
349324
response = [self.execute(create_ps_command(payload, force_ps32=force_ps32, obfs=obfs, custom_amsi=amsi_bypass, encode=encode), get_output)]
350-
325+
351326
self.logger.debug(f"ps_execute response: {response}")
352327
return response
353328

@@ -368,7 +343,7 @@ def put_file(self):
368343
self.logger.fail(f"Error during upload : {e}")
369344

370345
@requires_admin
371-
def get_file(self):
346+
def get_file(self):
372347
remote_path = self.args.get_file[0]
373348
download_path = self.args.get_file[1]
374349
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')

poetry.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies = [
3232
"neo4j>=5.0.0",
3333
"paramiko>=3.3.1",
3434
"pyasn1-modules>=0.3.0",
35-
"pylnk3>=0.4.2",
35+
"pylnk3>=0.4.3",
3636
"pypsrp>=0.8.1",
3737
"pypykatz>=0.6.8",
3838
"python-dateutil>=2.8.2",

0 commit comments

Comments
 (0)