diff --git a/Gemfile.lock b/Gemfile.lock index f0a3b9493daf..bf4faeb5e026 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -499,7 +499,7 @@ GEM ruby-progressbar (1.13.0) ruby-rc4 (0.1.5) ruby2_keywords (0.0.5) - ruby_smb (3.3.11) + ruby_smb (3.3.13) bindata (= 2.4.15) openssl-ccm openssl-cmac diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 8c12d80f34b8..c138d836a4a3 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -181,7 +181,7 @@ ruby-prof, 1.4.2, "Simplified BSD" ruby-progressbar, 1.13.0, MIT ruby-rc4, 0.1.5, MIT ruby2_keywords, 0.0.5, "ruby, Simplified BSD" -ruby_smb, 3.3.11, "New BSD" +ruby_smb, 3.3.13, "New BSD" rubyntlm, 0.6.5, MIT rubyzip, 2.3.2, "Simplified BSD" sawyer, 0.9.2, MIT diff --git a/documentation/modules/auxiliary/admin/smb/change_password.md b/documentation/modules/auxiliary/admin/smb/change_password.md new file mode 100755 index 000000000000..7321eaa53160 --- /dev/null +++ b/documentation/modules/auxiliary/admin/smb/change_password.md @@ -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). New AES kerberos keys will be generated. +- `RESET_NTLM` - Reset the target's NTLM hash, without knowing the existing password. AES kerberos authentication will not work until a standard password change occurs. +- `CHANGE` - Change the password, knowing the existing one. New AES kerberos keys will be generated. +- `CHANGE_NTLM` - Change the password to a NTLM hash value, knowing the existing password. AES kerberos authentication will not work until a standard password change occurs. + +## 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. \ No newline at end of file diff --git a/lib/msf/core/exploit/remote/smb/client.rb b/lib/msf/core/exploit/remote/smb/client.rb index 25f41b2abcb6..0f1653d09c7f 100644 --- a/lib/msf/core/exploit/remote/smb/client.rb +++ b/lib/msf/core/exploit/remote/smb/client.rb @@ -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']) @@ -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'], @@ -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'], diff --git a/modules/auxiliary/admin/smb/change_password.rb b/modules/auxiliary/admin/smb/change_password.rb new file mode 100644 index 000000000000..8a66324e22e6 --- /dev/null +++ b/modules/auxiliary/admin/smb/change_password.rb @@ -0,0 +1,293 @@ +## # 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). New AES kerberos keys will be generated." } ], + [ 'RESET_NTLM', { 'Description' => "Reset the target's NTLM hash, without knowing the existing password. AES kerberos authentication will not work until a standard password change occurs." } ], + [ 'CHANGE', { 'Description' => 'Change the password, knowing the existing one. New AES kerberos keys will be generated.' } ], + [ 'CHANGE_NTLM', { 'Description' => 'Change the password to a NTLM hash value, knowing the existing password. AES kerberos authentication will not work until a standard password change occurs.' } ] + ], + '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]], regex: /^([0-9a-fA-F]{32}:)?[0-9a-fA-F]{32}$/), + OptString.new('TARGET_USER', [false, 'The user to reset the password of.'], conditions: ['ACTION', 'in', %w[RESET RESET_NTLM]]) + ] + ) + end + + def connect_samr + 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 (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)) + if anonymous_on_expired + # Password has expired - we'll need to anonymous connect + print_warning('Password expired - binding anonymously') + opts = { + username: '', + password: '', + domain: '', + auth_protocol: Msf::Exploit::Remote::AuthOption::NTLM + } + disconnect + connect + smb_login(opts: opts) + elsif action.name == 'CHANGE_NTLM' + fail_with(Module::Failure::UnexpectedReply, 'Must change password first. Try using the CHANGE action instead') + else + raise + end + 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_lm, new_nt = 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']) + + if Net::NTLM.is_ntlm_hash?(Net::NTLM::EncodeUtil.encode_utf16le(datastore['SMBPass'])) + old_lm, old_nt = datastore['SMBPass'].split(':') + old_lm = [old_lm].pack('H*') + old_nt = [old_nt].pack('H*') + + @samr.samr_change_password_user(user_handle: user_handle, + new_nt_hash: new_nt, + new_lm_hash: new_lm, + old_nt_hash: old_nt, + old_lm_hash: old_lm) + else + @samr.samr_change_password_user(user_handle: user_handle, + old_password: datastore['SMBPass'], + new_nt_hash: new_nt, + new_lm_hash: new_lm) + end + + 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? + print_status('Changing password') + authenticate(anonymous_on_expired: true) + + if Net::NTLM.is_ntlm_hash?(Net::NTLM::EncodeUtil.encode_utf16le(datastore['SMBPass'])) + old_lm, old_nt = datastore['SMBPass'].split(':') + old_lm = [old_lm].pack('H*') + old_nt = [old_nt].pack('H*') + @samr.samr_unicode_change_password_user2(target_username: datastore['SMBUser'], new_password: datastore['NEW_PASSWORD'], old_nt_hash: old_nt, old_lm_hash: old_lm) + else + @samr.samr_unicode_change_password_user2(target_username: datastore['SMBUser'], old_password: datastore['SMBPass'], new_password: datastore['NEW_PASSWORD']) + end + + print_good("Successfully changed password for #{datastore['SMBUser']}") + end +end