Skip to content

Commit fb51b53

Browse files
authored
update
1 parent 50d551d commit fb51b53

2 files changed

Lines changed: 98 additions & 92 deletions

File tree

nxc/modules/change-password.py

Lines changed: 94 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,125 @@ 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+
try:
46+
string_binding = epm.hept_map(connection.host, samr.MSRPC_UUID_SAMR, protocol=protocol)
47+
rpctransport = transport.DCERPCTransportFactory(string_binding)
48+
rpctransport.setRemoteHost(connection.host)
49+
50+
if anonymous:
51+
rpctransport.set_credentials("", "", "", "", "", "")
52+
rpctransport.set_kerberos(False, None)
53+
context.log.info("Connecting with null session credentials.")
54+
else:
55+
rpctransport.set_credentials(
56+
connection.username,
57+
connection.password,
58+
connection.domain,
59+
connection.lmhash,
60+
connection.nthash,
61+
aesKey=connection.aesKey,
62+
)
63+
context.log.info(f"Connecting as {connection.domain}\\{connection.username}")
64+
65+
dce = rpctransport.get_dce_rpc()
66+
dce.connect()
67+
context.log.info("[+] Successfully connected to DCE/RPC")
68+
dce.bind(samr.MSRPC_UUID_SAMR)
69+
context.log.debug("[+] Successfully bound to SAMR")
70+
return dce
71+
72+
except DCERPCException as e:
73+
context.log.error(f"DCE/RPC Exception: {e!s}")
74+
raise
75+
3876
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
77+
target_username = self.target_user or connection.username
4178
target_domain = connection.domain
4279

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
5980
new_lmhash, new_nthash = "", ""
60-
61-
if self.newpass:
62-
# If NEWPASS is provided, use it
63-
new_password = self.newpass
6481

6582
if self.newhash:
66-
# If NEWHASH is provided, split the hash and set new password to None
6783
try:
6884
new_lmhash, new_nthash = self.newhash.split(":")
69-
new_password = None # Don't set a plain password when using a hash
7085
except ValueError:
71-
new_lmhash = "00000000000000000000000000000000"
7286
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"
7787

7888
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)
89+
self.anonymous = False
90+
self.dce = self.authenticate(context, connection, protocol="ncacn_np", anonymous=self.anonymous)
8791
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)
92+
if "STATUS_PASSWORD_MUST_CHANGE" in str(e) or "STATUS_PASSWORD_EXPIRED" in str(e):
93+
context.log.warning("Password must be changed. Trying with null session.")
94+
self.anonymous = True
95+
self.dce = self.authenticate(context, connection, protocol="ncacn_ip_tcp", anonymous=self.anonymous)
96+
elif "STATUS_LOGON_FAILURE" in str(e):
97+
context.log.critical("Authentication failure: wrong credentials.")
98+
return False
99+
else:
100+
raise
105101

106-
dce = rpc_transport.get_dce_rpc()
107-
dce.connect()
108-
dce.bind(samr.MSRPC_UUID_SAMR)
102+
try:
103+
self._smb_samr_change(context, connection, target_username, target_domain, self.oldhash, self.newpass, new_nthash)
104+
except Exception as e:
105+
context.log.error(f"Password change failed: {e}")
109106

107+
def _smb_samr_change(self, context, connection, target_username, target_domain, oldHash, newPassword, newHash):
110108
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}")
109+
if not self.anonymous:
110+
server_handle = samr.hSamrConnect(self.dce, connection.host + "\x00")["ServerHandle"]
111+
domain_sid = samr.hSamrLookupDomainInSamServer(self.dce, server_handle, target_domain)["DomainId"]
112+
domain_handle = samr.hSamrOpenDomain(self.dce, server_handle, domainId=domain_sid)["DomainHandle"]
113+
user_rid = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, (target_username,))["RelativeIds"]["Element"][0]
114+
user_handle = samr.hSamrOpenUser(self.dce, domain_handle, userId=user_rid)["UserHandle"]
115+
116+
if self.reset:
117+
samr.hSamrSetNTInternal1(self.dce, user_handle, newPassword, newHash)
118+
context.log.success(f"Successfully changed password for {target_username}")
119+
else:
120+
121+
samr.hSamrUnicodeChangePasswordUser2(
122+
self.dce, "\x00", target_username, self.oldpass, newPassword, "", ""
123+
)
124+
context.log.success(f"Successfully changed password for {target_username}")
121125
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
126+
self.mustchangePassword(target_username, target_domain, self.oldpass, newPassword, "", oldHash, "", newHash)
135127
except Exception as e:
136-
context.log.error(f"SMB-SAMR password change failed: {e!s}")
128+
context.log.fail(f"SMB-SAMR password change failed: {e}")
137129
finally:
138-
dce.disconnect()
130+
self.dce.disconnect()
131+
132+
def mustchangePassword(self, target_username, targetDomain, oldPassword, newPassword, oldPwdHashLM, oldPwdHashNT, newPwdHashLM, newPwdHashNT):
133+
if newPassword and oldPassword:
134+
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, oldPassword, newPassword, "", "")
135+
self.context.log.success(f"Successfully changed password for {target_username}")
136+
elif newPassword and oldPwdHashNT:
137+
samr.hSamrUnicodeChangePasswordUser2(self.dce, "\x00", target_username, oldPassword, newPassword, "", oldPwdHashNT)
138+
self.context.log.success(f"Successfully changed password for {target_username}")
139+
else:
140+
samr.hSamrSetNTInternal1(self.dce, target_username, newPassword, newPwdHashNT)
141+
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)