Pretending to Be smbpasswd with impacket
For me the most interesting aspect of pwning the Fuse machine from HTB was dealing with an expired domain user password. I found no other tools except smbpasswd to invoke such a password change remotely from Linux which seemed odd to me. So I decided to create a simple Python script with impacket which binds to the \samr pipe over SMB (MSRPC-SAMR) with a null session and calls SamrUnicodeChangePasswordUser2 to trigger the password change.
Change Password over SMB
There is a user tlavel
with an expired password Fabricorp01
. When trying to connect to an SMB resource (at FUSE.FABRICORP.LOCAL) STATUS_PASSWORD_MUST_CHANGE (0xC0000224) error is raised:
One way to get out of this situation remotely from Linux is to use this smbpasswd
tool:
$ smbpasswd -r fuse.fabricorp.local -U 'tlavel'
Old SMB password: Fabricorp01
New SMB password: snovvcrash1!
Retype new SMB password: snovvcrash1!
Under the hood it will try to authenticate as tlavel:Fabricorp01
(red block in the Wireshark capture) and when it fails because of the above mentioned error, it will initiate a null session and call SamrUnicodeChangePasswordUser2 (55) function via DCE/RPC over MS-SAMR protocol to change tlavel
’s password (green block in the Wireshark capture):
So, let’s say I’m annoyed with all this secure-interactive-input stuff like in smbpasswd and I want to be able to do the same thing in one line with password values passed as CLI arguments. When I try to achieve this with rpcclient and null authentication, I shall fail:
$ rpcclient -U'%' -c'chgpasswd2 tlavel Fabricorp01 snovvcrash1!' fuse.fabricorp.local
It’s no secret that rpcclient does a bad job when dealing with null session requests. Long story short, after successfully binding to the IPC$
share and creating \samr
pipe, it makes a bunch of undesirable requests (red block in the Wireshark capture) which it does not really have permissions for within a null session and eventually dies with NT_STATUS_ACCESS_DENIED
.
Another way to try a password change over SMB is the impacket’s smbclient.py password functionality, but it will not work when the password is expired either:
$ smbclient.py 'tlavel:Fabricorp01@10.10.10.193'
Same story with Samba’s NetCommand:
$ net rpc password tlavel 'snovvcrash1!' -S FUSE -U 'tlavel'%'Fabricorp01'
session setup failed: NT_STATUS_PASSWORD_MUST_CHANGE
Failed to set password for 'tlavel' with error: Failed to connect to IPC$ share on FUSE.
That’s not what we want, so we can create a super simple Python script using impacket to initiate a null session and then change tlavel
’s password directly with one single call to hSamrUnicodeChangePasswordUser2:
#!/usr/bin/python2.7
from argparse import ArgumentParser
from impacket.dcerpc.v5 import transport, samr
def connect(host_name_or_ip):
rpctransport = transport.SMBTransport(host_name_or_ip, filename=r'\samr')
if hasattr(rpctransport, 'set_credentials'):
rpctransport.set_credentials(username='', password='', domain='', lmhash='', nthash='', aesKey='') # null session
dce = rpctransport.get_dce_rpc()
dce.connect()
dce.bind(samr.MSRPC_UUID_SAMR)
return dce
def hSamrUnicodeChangePasswordUser2(username, oldpass, newpass, target):
dce = connect(target)
resp = samr.hSamrUnicodeChangePasswordUser2(dce, '\x00', username, oldpass, newpass)
resp.dump()
parser = ArgumentParser()
parser.add_argument('username', help='username to change password for')
parser.add_argument('oldpass', help='old password')
parser.add_argument('newpass', help='new password')
parser.add_argument('target', help='hostname or IP')
args = parser.parse_args()
hSamrUnicodeChangePasswordUser2(args.username, args.oldpass, args.newpass, args.target)
Now we can trigger password change with a single command insecurely leaving all the sensitive information in .bash_history
… Just as we’ve always wanted
$ ./smbpasswd.py tlavel Fabricorp01 'snovvcrash01!' FUSE.FABRICORP.LOCAL
Later on I groomed up the code a little and made a pull request to the impacket’s master branch.
Password Policies Notes
There is a couple of points to be cleared:
1. From now on both passwords will be valid for approximately one hour after the password change. If I try to authorize with the old Fabricorp01
password after the change was made, it actually works and the new snovvcrash01!
password is valid too:
2. There is a scheduled task which reverts the password back to Fabricorp01
every 1 minute:
3. If I try to change the password two times in a row but before it was reverted to default Fabricorp01
by the scheduler task (Fabricorp01
⟶ snovvcrash01!
⟶ snovvcrash02!
), it fails due to the minimum password age policy (1 day):
4. If I try to set the same password again after it was reverted to default Fabricorp01
by the scheduler task (Fabricorp01
⟶ snovvcrash01!
⟶ Fabricorp01
⟶ snovvcrash01!
), it fails due to the enforce password history policy (24 passwords):
Cheers and happy hacking!