Skip to content

Commit 7371dea

Browse files
authored
Merge pull request #1 from termanix/changepassupdate
Improvements on STATUS_PASSWORD_MUST_CHANGE and STATUS_PASSWORD_EXPIRED
2 parents 50d551d + 761da40 commit 7371dea

2 files changed

Lines changed: 110 additions & 92 deletions

File tree

nxc/modules/change-password.py

Lines changed: 106 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import sys
2+
from impacket.dcerpc.v5 import samr, epm, transport
3+
from impacket.dcerpc.v5.rpcrt import DCERPCException
24

35
class NXCModule:
46
"""
57
Module for changing or resetting user passwords
68
Module by Fagan Afandiyev
7-
8-
This is NXC implementation of changepasswd.py from impacket
99
"""
1010

1111
name = "change-password"
@@ -17,122 +17,137 @@ class NXCModule:
1717
def options(self, context, module_options):
1818
"""
1919
Module options for password change
20+
21+
Required options:
22+
If STATUS_PASSWORD_MUST_CHANGE or STATUS_PASSWORD_EXPIRED (Change password for current user)
23+
netexec smb <DC_IP> -u username -p oldpass -M change-password -o OLDPASS='oldpass' NEWPASS='newpass'
24+
netexec smb <DC_IP> -u username -H oldnthash -M change-password -o OLDNTHASH='oldnthash' NEWPASS='newpass'
25+
26+
If want to change other user's password (with forcechangepassword priv or admin rights)
27+
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWPASS='target_user_newpass'
28+
netexec smb <DC_IP> -u username -p password -M change-password -o USER='target_user' NEWNTHASH='target_user_newnthash'
2029
21-
Supported options:
22-
- NEWPASS: New password to set
23-
- NEWHASH: New password hash (NTHASH or LMHASH:NTHASH)
24-
- OLDPASS: Current password (optional for reset)
25-
- USER: User whose password to change (default is current user)
26-
- RESET: Set to True to reset password with admin privileges
30+
NEWPASS or NEWHASH
2731
"""
32+
self.context = context
2833
self.newpass = module_options.get("NEWPASS")
29-
self.newhash = module_options.get("NEWHASH")
34+
self.newhash = module_options.get("NEWNTHASH")
3035
self.oldpass = module_options.get("OLDPASS")
36+
self.oldhash = module_options.get("OLDNTHASH")
3137
self.target_user = module_options.get("USER")
3238
self.reset = module_options.get("RESET", True)
3339

3440
if not self.newpass and not self.newhash:
3541
context.log.error("Either NEWPASS or NEWHASH is required!")
3642
sys.exit(1)
3743

44+
def authenticate(self, context, connection, protocol, anonymous=False):
45+
# Authenticate to the target using DCE/RPC with either user credentials or a null session. Establishes a connection and binds to the SAMR service.
46+
try:
47+
# Map to the SAMR endpoint on the target
48+
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
49+
rpctransport = transport.DCERPCTransportFactory(string_binding)
50+
rpctransport.setRemoteHost(connection.host)
51+
52+
if anonymous:
53+
rpctransport.set_credentials("", "", "", "", "", "")
54+
rpctransport.set_kerberos(False, None)
55+
context.log.info("Connecting with null session credentials.")
56+
else:
57+
rpctransport.set_credentials(
58+
connection.username,
59+
connection.password,
60+
connection.domain,
61+
connection.lmhash,
62+
connection.nthash,
63+
aesKey=connection.aesKey,
64+
)
65+
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
66+
67+
# Connect to the DCE/RPC endpoint and bind to the SAMR service
68+
dce = rpctransport.get_dce_rpc()
69+
dce.connect()
70+
context.log.info("[+] Successfully connected to DCE/RPC")
71+
dce.bind(samr.MSRPC_UUID_SAMR)
72+
context.log.debug("[+] Successfully bound to SAMR")
73+
return dce
74+
75+
except DCERPCException as e:
76+
context.log.error(f"DCE/RPC Exception: {e!s}")
77+
raise
78+
3879
def on_login(self, context, connection):
39-
# Determine which user's password to change (prioritize TARGETUSER)
40-
target_username = self.target_user if self.target_user else connection.username
80+
target_username = self.target_user or connection.username
4181
target_domain = connection.domain
4282

43-
# Prepare authentication details
44-
username = connection.username
45-
domain = connection.domain
46-
password = connection.password
47-
lmhash, nthash = "", ""
48-
49-
if context.hash and ":" in context.hash[0]:
50-
hash_list = context.hash[0].split(":")
51-
nthash = hash_list[-1]
52-
lmhash = hash_list[0]
53-
elif context.hash:
54-
nthash = context.hash[0]
55-
lmhash = "00000000000000000000000000000000"
56-
57-
# Prepare new password details
58-
new_password = None # Start with None for new_password
5983
new_lmhash, new_nthash = "", ""
60-
61-
if self.newpass:
62-
# If NEWPASS is provided, use it
63-
new_password = self.newpass
6484

85+
# Parse new hash values if provided
6586
if self.newhash:
66-
# If NEWHASH is provided, split the hash and set new password to None
6787
try:
6888
new_lmhash, new_nthash = self.newhash.split(":")
69-
new_password = None # Don't set a plain password when using a hash
7089
except ValueError:
71-
new_lmhash = "00000000000000000000000000000000"
7290
new_nthash = self.newhash
73-
new_password = None # Ensure no password is set for hash-only change
74-
75-
# Use the appropriate protocol based on netexec's context
76-
protocol = "smb"
7791

7892
try:
79-
if protocol == "smb":
80-
self._smb_samr_change(
81-
context, connection, target_username, target_domain, username, domain, password,
82-
lmhash, nthash, self.oldpass, new_password, new_lmhash, new_nthash
83-
)
84-
else:
85-
context.log.error(f"Unsupported protocol: {protocol}")
86-
sys.exit(1)
93+
self.anonymous = False
94+
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
8795
except Exception as e:
88-
context.log.error(f"Password change failed: {e!s}")
89-
90-
def _smb_samr_change(self, context, connection, target_username, target_domain,
91-
username, domain, password, lmhash, nthash,
92-
old_password, new_password, new_lmhash, new_nthash):
93-
"""Change password using SMB-SAMR protocol"""
94-
from impacket.dcerpc.v5 import samr, epm, transport
95-
96-
if not new_password and not new_lmhash and not new_nthash:
97-
context.log.error("New password or hash cannot be None or empty")
98-
return
99-
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol="ncacn_np")
100-
rpc_transport = transport.DCERPCTransportFactory(string_binding)
101-
rpc_transport.setRemoteHost(connection.host)
102-
103-
if hasattr(rpc_transport, "set_credentials"):
104-
rpc_transport.set_credentials(username, password, domain, lmhash, nthash)
96+
# Handle specific errors like password expiration or must be change
97+
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
98+
context.log.warning("Password must be changed. Trying with null session.")
99+
self.anonymous = True
100+
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
101+
elif "STATUS_LOGON_FAILURE" in str(e):
102+
context.log.critical("Authentication failure: wrong credentials.")
103+
return False
104+
else:
105+
raise
105106

106-
dce = rpc_transport.get_dce_rpc()
107-
dce.connect()
108-
dce.bind(samr.MSRPC_UUID_SAMR)
107+
try:
108+
# Perform the SMB SAMR password change
109+
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
110+
except Exception as e:
111+
context.log.error(f"Password change failed: {e}")
109112

113+
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
110114
try:
111-
# Retrieve the user handle by connecting to SAMR and looking up the username.
112-
server_handle = samr.hSamrConnect(dce, connection.host + "\x00")["ServerHandle"]
113-
domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, target_domain)["DomainId"]
114-
domain_handle = samr.hSamrOpenDomain(dce, server_handle, domainId=domain_sid)["DomainHandle"]
115-
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (target_username,))["RelativeIds"]["Element"][0]
116-
user_handle = samr.hSamrOpenUser(dce, domain_handle, userId=user_rid)["UserHandle"]
117-
if self.reset:
118-
# Reset the password
119-
samr.hSamrSetNTInternal1(dce, user_handle, new_password, new_nthash)
120-
context.log.success(f"Successfully reset password for {target_username}")
115+
if not self.anonymous:
116+
# Connect to the target server and retrieve handles
117+
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"] # Does not work for null session auth.
118+
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, target_domain)["DomainId"]
119+
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
120+
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (target_username,))["RelativeIds"]["Element"][0]
121+
user_handle = samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
122+
123+
if self.reset:
124+
# Change the password with new password hash
125+
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
126+
context.log.success(f"Successfully changed password for {target_username}")
127+
else:
128+
# Change the password with new password
129+
samr.hSamrUnicodeChangePasswordUser2(
130+
self.dce, "\x00", target_username, self.oldpass, newPassword, "", ""
131+
)
132+
context.log.success(f"Successfully changed password for {target_username}")
121133
else:
122-
try:
123-
if new_password:
124-
# If using new password
125-
samr.hSamrUnicodeChangePasswordUser2(
126-
dce, "\x00", target_username, old_password, new_password, "", ""
127-
)
128-
elif new_lmhash and new_nthash:
129-
# If using hash (NEWHASH)
130-
samr.hSamrSetNTInternal1(dce, user_handle, new_password, new_nthash)
131-
context.log.success(f"Successfully changed password for {target_username}")
132-
except AttributeError as encode_error:
133-
context.log.error(f"Encoding issue in new password: {encode_error!s}")
134-
return
134+
# Handle anonymous/null session password change
135+
self.mustchangePassword(target_username, target_domain, self.oldpass, newPassword, "", oldHash, "", newHash)
135136
except Exception as e:
136-
context.log.error(f"SMB-SAMR password change failed: {e!s}")
137+
context.log.fail(f"SMB-SAMR password change failed: {e}")
137138
finally:
138-
dce.disconnect()
139+
self.dce.disconnect()
140+
141+
def mustchangePassword(self, target_username, targetDomain, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT):
142+
if newPassword and oldPassword:
143+
# Change password using old and new plaintext passwords
144+
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, oldPassword, newPassword, "", "")
145+
self.context.log.success(f"Successfully changed password for {target_username}")
146+
elif newPassword and oldPwdHashNT:
147+
# Change password using hash for authentication
148+
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, oldPassword, newPassword, "", oldPwdHashNT)
149+
self.context.log.success(f"Successfully changed password for {target_username}")
150+
else:
151+
# Use NT internal function to set new password or hash
152+
samr.hSamrSetNTInternal1(self.dce, target_username, newPassword, newPwdHashNT)
153+
self.context.log.success(f"Successfully changed password for {target_username}")

nxc/protocols/smb.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ def plaintext_login(self, domain, username, password):
460460
f'{domain}\\{self.username}:{process_secret(self.password)} {error} {f"({desc})" if self.args.verbose else ""}',
461461
color="magenta" if error in smb_error_status else "red",
462462
)
463+
if str(error) == "STATUS_PASSWORD_MUST_CHANGE" or "STATUS_PASSWORD_EXPIRED":
464+
return True
463465
if error not in smb_error_status:
464466
self.inc_failed_login(username)
465467
return False
@@ -524,7 +526,8 @@ def hash_login(self, domain, username, ntlm_hash):
524526
f"{domain}\\{self.username}:{process_secret(self.hash)} {error} {f'({desc})' if self.args.verbose else ''}",
525527
color="magenta" if error in smb_error_status else "red",
526528
)
527-
529+
if str(error) == "STATUS_PASSWORD_MUST_CHANGE" or "STATUS_PASSWORD_EXPIRED":
530+
return True
528531
if error not in smb_error_status:
529532
self.inc_failed_login(self.username)
530533
return False

0 commit comments

Comments
 (0)