Skip to content

Commit c198413

Browse files
authored
Merge pull request Pennyw0rth#512 from KriyosArcane/main
New change-password module.
2 parents 6aa7d6a + f3f2797 commit c198413

3 files changed

Lines changed: 157 additions & 2 deletions

File tree

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/protocols/smb.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,6 @@ def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="",
456456
)
457457
if error not in smb_error_status:
458458
self.inc_failed_login(username)
459-
return False
460459
return False
461460

462461
def plaintext_login(self, domain, username, password):
@@ -501,6 +500,8 @@ def plaintext_login(self, domain, username, password):
501500
f'{domain}\\{self.username}:{process_secret(self.password)} {error} {f"({desc})" if self.args.verbose else ""}',
502501
color="magenta" if error in smb_error_status else "red",
503502
)
503+
if error in ["STATUS_PASSWORD_MUST_CHANGE", "STATUS_PASSWORD_EXPIRED"] and self.args.module == ["change-password"]:
504+
return True
504505
if error not in smb_error_status:
505506
self.inc_failed_login(username)
506507
return False
@@ -563,7 +564,8 @@ def hash_login(self, domain, username, ntlm_hash):
563564
f"{domain}\\{self.username}:{process_secret(self.hash)} {error} {f'({desc})' if self.args.verbose else ''}",
564565
color="magenta" if error in smb_error_status else "red",
565566
)
566-
567+
if error in ["STATUS_PASSWORD_MUST_CHANGE", "STATUS_PASSWORD_EXPIRED"] and self.args.module == ["change-password"]:
568+
return True
567569
if error not in smb_error_status:
568570
self.inc_failed_login(self.username)
569571
return False

tests/e2e_commands.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M webdav -
152152
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
153153
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M winscp
154154
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon
155+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWPASS=Password123
156+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M change-password -o NEWNTHASH=58A478135A93AC3BF058A5EA0E8FDB71
155157
# test for multiple modules at once
156158
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M uac -M webdav -M wifi -M coerce_plus
157159
##### SMB Anonymous Auth

0 commit comments

Comments
 (0)