I’ve often found that while performing password guessing on a network, I’ll find valid credentials, but the password will be expired.  This presents a challenge, because the credentials are of limited use until they are reset.

# crackmapexec smb 10.0.0.15 -u locked -p Password1
SMB         10.0.0.15       445    WIN-NDA9607EHKS  [*] Windows 10.0 Build 17763 x64 (name:WIN-NDA9607EHKS) (domain:n00py.local) (signing:True) (SMBv1:False)
SMB         10.0.0.15       445    WIN-NDA9607EHKS  [-] n00py.local\locked:Password1 STATUS_PASSWORD_MUST_CHANGE

# crackmapexec smb 10.0.0.15 -u expired -p Password1
SMB         10.0.0.15       445    WIN-NDA9607EHKS  [*] Windows 10.0 Build 17763 x64 (name:WIN-NDA9607EHKS) (domain:n00py.local) (signing:True) (SMBv1:False)
SMB         10.0.0.15       445    WIN-NDA9607EHKS  [-] n00py.local\expired:SPassword1 STATUS_PASSWORD_EXPIRED

Throughout my testing I’ve found multiple ways to reset the passwords, however each contain some caveats.

I’ve tested using an account that has an expired password (STATUS_PASSWORD_EXPIRED) as well as an account that has the “User must change password at next logon” box checked (STATUS_PASSWORD_MUST_CHANGE).  I’ve named them “expired” and “locked” respectively.

Please ignore my poor naming, as the “locked” account is NOT disabled (STATUS_ACCOUNT_DISABLED) or locked (STATUS_ACCOUNT_LOCKED_OUT) , and the “expired” account is NOT expired (STATUS_ACCOUNT_EXPIRED), only the password is.  As far as I know there is no possible way to unset those without admin.

See the original Twitter thread here:

Outlook Web Access (OWA) / Active Directory Federation Services (ADFS)

Caveats: Requires OWA/ADFS web interface to be accessible

This is a very reliable way to reset a password, however you of course need to find an exposed Outlook Web Access application. It will typically look for something like this at mail.DOMAIN.TLD or HOSTNAME/OWA/.

When you find one it’s simply a matter of entering the credentials and a change password screen will pop up.

Same procedure for ADFS, look for adfs.DOMAIN.TLD or HOSTNAME/ADFS/.

Remote Desktop Protocol

Caveats: Requires RDP without NLA enforced

This is the way I’ve been doing it a long time, and has been pretty reliable.  The hard part is finding a system without NLA required.  The good part however is that the user does not need permissions to RDP to the system.  You can still reset the password regardless.

If you have completed a Nessus scan, look for the finding “Terminal Services Doesn’t Use Network Level Authentication (NLA) Only”.

Anything that shows up there will work.  you can also use nmap:

# nmap 10.0.0.2 -p 3389 --script rdp-enum-encryption

PORT STATE SERVICE
3389/tcp open ms-wbt-server
| rdp-enum-encryption:
| Security layer
| CredSSP (NLA): SUCCESS
| CredSSP with Early User Auth: SUCCESS
| RDSTLS: SUCCESS
| SSL: SUCCESS
|_ RDP Protocol Version: Unknown
MAC Address: 00:0C:29:DE:EA:61 (VMware)

You will want to see the “SSL: SUCCESS”  in the output.

You can also use Metasploit to find this information as well:

msf6 auxiliary(scanner/rdp/rdp_scanner) > run

[*] 10.0.0.2:3389 - Detected RDP on 10.0.0.2:3389 (Windows version: 10.0.19041) (Requires NLA: No)
[*] 10.0.0.2:3389 - Scanned 1 of 1 hosts (100% complete)
[*] Auxiliary module execution completed

Use rdesktop to connect to the system without specifying any username or password.  Type “yes” to trust the certificate.

# rdesktop 10.0.0.2

Do you trust this certificate (yes/no)? yes
Failed to initialize NLA, do you have correct Kerberos TGT initialized ?
Core(warning): Certificate received from server is NOT trusted by this system, an exception has been added by the user to trust this specific certificate.
Connection established using SSL.

From here you can reset the password, and can then return to your other command line tools.

smbpasswd

Caveats: Requires anonymous access to IPC$ Share

smbpasswd probably the most simple way to perform a reset remotely, though it does have some conditions.  To perform the reset, simply provide the remote host with the -r flag and the username with the -U flag.

smbpasswd -r 10.0.0.15 -U 'expired'
Old SMB password:
New SMB password:
Retype new SMB password:

Password changed for user expired on 10.0.0.15.

# smbpasswd -r 10.0.0.15 -U 'locked'
Old SMB password:
New SMB password:
Retype new SMB password:
Password changed for user locked on 10.0.0.15.

A review of the packet capture shows what happens; it first tried to authenticate with the account, gets the “password expired” message, and then connects anonymously to the IPC$ share. From there it is able to perform the password reset.

By default, anyone can connect over IPC$ anonymously.  It is sometimes the case however that IPC$ is not accessible; and in this case this will not work. Below is smbpassword with the debug flag set.  As you can see it fails when trying to establish the NULL session.

I’d still always recommend trying this method first, but if you ever find that it does not work it may be because of the NULL session limitation.

ChangePwd

Caveats: Requires access to IPC$ Share and Windows

This is a pretty old utility for Windows that has been around since 1999.  You can download it here. Despite its age, it works flawlessly and does not need any admin permissions to run.

A packet capture reveals that it works in much the same way as smbpassword, in regards to the connection to IPC$.  Notable however, if that it does not perform an anonymous connection, it connects to IPC$ with the current logged in user.  In this case, even if IPC$ was limited for NULL sessions it can still perform the reset as long as you have at least one other user.

Impacket smbpasswd

Caveats: Requires anonymous access to IPC$ Share

This is basically the same as smbpassword, but re-implemented in a non-interactive way in Impacket by snovvcrashBlog post here.  It also has the really cool feature of not needing to know the original plaintext, as long as you have the hash.

At the time of writing, his latest version (non-merged) is here.

# python3 smbpasswd.py locked:Password1@n00py.local -newpass Password2
Impacket v0.9.24.dev1+20210917.161743.0297480b - Copyright 2021 SecureAuth Corporation

[!] Password is expired, trying to bind with a null session.
[*] Password was changed successfully.

A review of the network traffic found that this was pretty much the same as the original smbpasswd.

Testing has indicated that without anonymous access to IPC$, this will also fail in the same way smbpasswd does.

Based on what I learned when looking at ChangePwd, it’s actually still possible to do this without needing a NULL sessions, assuming you have any other valid user creds.  This may get added to Impacket smbpasswd.py someday, but for now you can do this just by modifying the line where it says:

if anonymous:
	rpctransport.set_credentials(username='', password='', domain='', lmhash='', nthash='', aesKey='')

And just replacing the blank values with another set of valid creds.

SetADAccountPwd

Caveats: Requires Windows and RSAT tools

This method is also super easy, only hindered by the fact that you need RSAT tools. Sadly, you need admin to install them on a Windows desktop.

If you do happen to have them installed, the syntax is super simple.

Powershell Script

Caveats: Requires access to IPC$ Share and Windows

This method is great if you have access to Windows and don’t want to install anything or drop binaries.  I found this script on StackExchange and it works well:

function Set-PasswordRemotely {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)][string] $UserName,
        [Parameter(Mandatory = $true)][string] $OldPassword,
        [Parameter(Mandatory = $true)][string] $NewPassword,
        [Parameter(Mandatory = $true)][alias('DC', 'Server', 'ComputerName')][string] $DomainController
    )
    $DllImport = @'
[DllImport("netapi32.dll", CharSet = CharSet.Unicode)]
public static extern bool NetUserChangePassword(string domain, string username, string oldpassword, string newpassword);
'@
    $NetApi32 = Add-Type -MemberDefinition $DllImport -Name 'NetApi32' -Namespace 'Win32' -PassThru
    if ($result = $NetApi32::NetUserChangePassword($DomainController, $UserName, $OldPassword, $NewPassword)) {
        Write-Output -InputObject 'Password change failed. Please try again.'
    } else {
        Write-Output -InputObject 'Password change succeeded.'
    }
}

Original source is here.

You  can run it interactively or in one line.

PS C:\Users\n00py> Set-PasswordRemotely

cmdlet Set-PasswordRemotely at command pipeline position 1
Supply values for the following parameters:
UserName: locked
OldPassword: Password1
NewPassword: Password11
DomainController: n00py.local
Password change succeeded.

PS C:\Users\n00py> Set-PasswordRemotely locked Password1 Password12 n00py.local
Password change succeeded.

Rubeus

Caveats: Requires Windows / AV Evasion

A password change can also be performed with Rubeus using Kerberos magic.  I’m not going to pretend to understand how this actually works, so I’ll leave that to Harmj0y to explain in his blog post.  Notable is that you do not need the plaintext as long as you have a ticket for the user or their hash.

To perform the change first get a ticket with the old password:

And then change the password using the ticket:

Now for the techniques I tried, but do not appear to work in any way.

LDAP

Findings: Requires one other set of valid credentials / LDAPS

Initially, this method was not working.  Any attempt to bind with an expired password fails.  I’ve seen multiple people (1,2) mention that this should be possible, but I had no luck with it.  It is mentioned to change the password you need to use LDAPS, but as far as I can tell this is only relevant to changing the password and not the initial bind.

Below is my attempt to use ldap3 in Python to bind using an expired password.  Note the response containing the 49 result with error 773 and 532 respectively.

# python3           
>>> import ldap3
>>> from ldap3 import ALL, Server, Connection, NTLM, extend, SUBTREE
>>> server = ldap3.Server('n00py.local',get_info = ldap3.ALL)
>>> user = 'locked'
>>> password = 'Password1'
>>> c = Connection(server, "n00py\\" + user, password=password, authentication=NTLM)
>>> c.bind()
False
>>> c.result
{'result': 49, 'description': 'invalidCredentials', 'dn': '', 'message': '8009030C: LdapErr: DSID-0C090690, comment: AcceptSecurityContext error, data 773, v4563\x00', 'referrals': None, 'saslCreds': None, 'type': 'bindResponse'}

>>> user = 'expired'
>>> c = Connection(server, "n00py\\" + user, password=password, authentication=NTLM)
>>> c.bind()
False
>>> c.result
{'result': 49, 'description': 'invalidCredentials', 'dn': '', 'message': '8009030C: LdapErr: DSID-0C090690, comment: AcceptSecurityContext error, data 532, v4563\x00', 'referrals': None, 'saslCreds': None, 'type': 'bindResponse'}

Packet captures:

I also did retry it in Python ldap3 using LDAPS, but it gave the same result.

Much like was discussed regarding smbpasswd.py,  I did find that this can be done if you have one other set of valid credentials for the domain.  All you need to do is authenticate with any valid user first, find the DN of the account you want to update the password for, and then change it.

>>> import ldap3
>>> from ldap3 import ALL, Server, Connection, NTLM, extend, SUBTREE
>>> user = 'n00py'
>>> password = 'Password1'
>>> server = ldap3.Server('n00py.local',get_info = ldap3.ALL, port=636, use_ssl = True)
>>> c = Connection(server, user, password=password)
>>> c.bind()
True
>>> c.search('dc=n00py,dc=local', '(objectclass=person)')
True
>>> for entry in c.entries:
...     print(entry.entry_dn)
... 
CN=Administrator,CN=Users,DC=n00py,DC=local
CN=Guest,CN=Users,DC=n00py,DC=local
CN=WIN-NDA9607EHKS,OU=Domain Controllers,DC=n00py,DC=local
CN=krbtgt,CN=Users,DC=n00py,DC=local
CN=DESKTOP-EP5911R,CN=Computers,DC=n00py,DC=local
CN=locked,OU=locked,OU=Employees,DC=n00py,DC=local
CN=expired,OU=expire,OU=Employees,DC=n00py,DC=local
CN=expired2,OU=expire,OU=Employees,DC=n00py,DC=local
CN=expired3,OU=expire,OU=Employees,DC=n00py,DC=local
CN=locked2,OU=locked,OU=Employees,DC=n00py,DC=local
CN=locked3,OU=locked,OU=Employees,DC=n00py,DC=local
CN=expired4,OU=expire,OU=Employees,DC=n00py,DC=local
CN=expired5,OU=expire,OU=Employees,DC=n00py,DC=local
CN=n00py,OU=Employees,DC=n00py,DC=local

>>> c.extend.microsoft.modify_password('CN=locked,OU=locked,OU=Employees,DC=n00py,DC=local', 'Password2', old_password='Password1')
True
>>> c.extend.microsoft.modify_password('CN=expired4,OU=expire,OU=Employees,DC=n00py,DC=local', 'Password2', old_password='Password1')
True

File Explorer

Findings: Does not work / Not possible

I thought perhaps this may be possible to do in Explorer when connecting to a remote share.  While it does at least give an informative error message, it does not allow any way to update the password.

Kpasswd

Findings: Does not work / Not possible

I was hoping kpasswd would do the trick, however it simply gives you the message “Password Incorrect” with an expired password.

kpasswd locked@n00py.local                                                                                            
locked@n00py.local's Password: 
kpasswd: Password incorrect

Kinit does at least give you the courtesy of telling you the password expired, but does not contain any mechanism to reset the password either.

kinit locked@n00py.local                                                                                                
locked@n00py.local's Password: 
Password has expired
kinit: Password incorrect