11import sys
2+ from impacket .dcerpc .v5 import samr , epm , transport
3+ from impacket .dcerpc .v5 .rpcrt import DCERPCException
24
35class 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 } " )
0 commit comments