Skip to content

Commit 196d9fa

Browse files
authored
Merge branch 'main' into cleanup
2 parents 5efa2c3 + c198413 commit 196d9fa

9 files changed

Lines changed: 232 additions & 59 deletions

File tree

nxc/connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ def parse_credentials(self):
384384
if "\\" in line and len(line.split("\\")) == 2:
385385
domain_single, username_single = line.split("\\")
386386
else:
387-
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
387+
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
388388
username_single = line
389389
domain.append(domain_single)
390390
username.append(username_single.strip())
@@ -393,7 +393,7 @@ def parse_credentials(self):
393393
if "\\" in user:
394394
domain_single, username_single = user.split("\\")
395395
else:
396-
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain
396+
domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain is not None else self.domain
397397
username_single = user
398398
domain.append(domain_single)
399399
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/change-password.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import sys
2+
from impacket.dcerpc.v5 import samr, epm, transport
3+
from impacket.dcerpc.v5.rpcrt import DCERPCException
4+
5+
6+
class NXCModule:
7+
"""
8+
Module for changing or resetting user passwords
9+
Module by Fagan Afandiyev, termanix and NeffIsBack
10+
"""
11+
12+
name = "change-password"
13+
description = "Change or reset user passwords via various protocols"
14+
supported_protocols = ["smb"]
15+
opsec_safe = True
16+
multiple_hosts = False
17+
18+
def options(self, context, module_options):
19+
"""
20+
Required (one of):
21+
NEWPASS The new password of the user.
22+
NEWNTHASH The new NT hash of the user.
23+
24+
Optional:
25+
USER The user account if the target is not the current user.
26+
27+
Examples
28+
--------
29+
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
30+
netexec smb <DC_IP> -u username -p oldpass -M change-password -o NEWNTHASH='nthash'
31+
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o NEWPASS='newpass'
32+
33+
If want to change other user's password (with forcechangepassword priv or admin rights)
34+
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
35+
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
36+
"""
37+
self.context = context
38+
self.newpass = module_options.get("NEWPASS")
39+
self.newhash = module_options.get("NEWNTHASH")
40+
self.target_user = module_options.get("USER")
41+
42+
if not self.newpass and not self.newhash:
43+
context.log.fail("Either NEWPASS or NEWNTHASH is required!")
44+
sys.exit(1)
45+
46+
def authenticate(self, context, connection, protocol, anonymous=False):
47+
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
48+
try:
49+
# Map to the SAMR endpoint on the target
50+
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
51+
rpctransport = transport.DCERPCTransportFactory(string_binding)
52+
rpctransport.setRemoteHost(connection.host)
53+
54+
if anonymous:
55+
rpctransport.set_credentials("", "", "", "", "", "")
56+
rpctransport.set_kerberos(False, None)
57+
context.log.info("Connecting with null session credentials.")
58+
else:
59+
rpctransport.set_credentials(
60+
connection.username,
61+
connection.password,
62+
connection.domain,
63+
connection.lmhash,
64+
connection.nthash,
65+
aesKey=connection.aesKey,
66+
)
67+
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
68+
69+
# Connect to the DCE/RPC endpoint and bind to the SAMR service
70+
dce = rpctransport.get_dce_rpc()
71+
dce.connect()
72+
context.log.info("[+] Successfully connected to DCE/RPC")
73+
dce.bind(samr.MSRPC_UUID_SAMR)
74+
context.log.info("[+] Successfully bound to SAMR")
75+
return dce
76+
except DCERPCException as e:
77+
context.log.fail(f"DCE/RPC Exception: {e!s}")
78+
raise
79+
80+
def on_login(self, context, connection):
81+
target_username = self.target_user or connection.username
82+
target_domain = connection.domain
83+
84+
# Grab all creds from the connection to use for authentication
85+
self.oldpass = connection.password
86+
self.oldhash = connection.nthash
87+
88+
new_lmhash, new_nthash = "", ""
89+
90+
# Parse new hash values if provided
91+
if self.newhash:
92+
try:
93+
new_lmhash, new_nthash = self.newhash.split(":")
94+
except ValueError:
95+
new_nthash = self.newhash
96+
97+
try:
98+
self.anonymous = False
99+
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
100+
except Exception as e:
101+
# Handle specific errors like password expiration or must be change
102+
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
103+
context.log.warning("Password must be changed. Trying with null session.")
104+
self.anonymous = True
105+
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
106+
elif "STATUS_LOGON_FAILURE" in str(e):
107+
context.log.fail("Authentication failure: wrong credentials.")
108+
return False
109+
else:
110+
raise
111+
112+
try:
113+
# Perform the SMB SAMR password change
114+
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
115+
except Exception as e:
116+
context.log.fail(f"Password change failed: {e}")
117+
118+
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
119+
try:
120+
# Reset the password for a different user
121+
if target_username != connection.username:
122+
user_handle = self._hSamrOpenUser(connection, target_username)
123+
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
124+
context.log.success(f"Successfully changed password for {target_username}")
125+
else:
126+
# Change password for the current user
127+
if newPassword:
128+
# Change the password with new password
129+
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, self.oldpass, newPassword, "", oldHash)
130+
else:
131+
# Change the password with new hash
132+
user_handle = self._hSamrOpenUser(connection, target_username)
133+
samr.hSamrChangePasswordUser(self.dce, user_handle, self.oldpass, "", oldHash, "aad3b435b51404eeaad3b435b51404ee", newHash)
134+
context.log.highlight("Note: Target user must change password at next logon.")
135+
context.log.success(f"Successfully changed password for {target_username}")
136+
except Exception as e:
137+
context.log.fail(f"SMB-SAMR password change failed: {e}")
138+
finally:
139+
self.dce.disconnect()
140+
141+
def _hSamrOpenUser(self, connection, username):
142+
"""Get handle to the user object"""
143+
try:
144+
# Connect to the target server and retrieve handles
145+
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
146+
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, connection.domain)["DomainId"]
147+
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
148+
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (username,))["RelativeIds"]["Element"][0]
149+
return samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
150+
except Exception as e:
151+
self.context.log.fail(f"Failed to open user: {e}")

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:

0 commit comments

Comments
 (0)