diff --git a/documentation/modules/auxiliary/admin/ldap/change_password.md b/documentation/modules/auxiliary/admin/ldap/change_password.md new file mode 100755 index 000000000000..7e38236d5b3c --- /dev/null +++ b/documentation/modules/auxiliary/admin/ldap/change_password.md @@ -0,0 +1,39 @@ +## Introduction + +Allows changing or resetting users' passwords over the LDAP protocol (particularly for Active Directory). + +"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), but can usually change their password as long as they know the existing one. + +This module works with existing sessions (or relaying), especially for Resetting, wherein the target's password is not required. + +## Actions + +- `RESET` - Reset the target's password without knowing the existing one (requires appropriate permissions) +- `CHANGE` - Change the user's password, knowing the existing one. + +## 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 `USERNAME` and `PASSWORD`, even if using an existing session (since the API requires both of these to be specified, even for open LDAP sessions) +- The `NEW_PASSWORD` option must always be provided + +**USERNAME** + +The username to use to authenticate to the server. Required for changing a password, even if using an existing session. + +**PASSWORD** + +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 (username) 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. \ No newline at end of file diff --git a/lib/msf/core/exploit/remote/ldap.rb b/lib/msf/core/exploit/remote/ldap.rb index 216ea212f9b5..cdae87130be6 100644 --- a/lib/msf/core/exploit/remote/ldap.rb +++ b/lib/msf/core/exploit/remote/ldap.rb @@ -285,6 +285,8 @@ def validate_query_result!(query_result, filter=nil) fail_with(Msf::Module::Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist.') when 18 fail_with(Msf::Module::Failure::BadConfig, 'The LDAP search failed because some matching is not supported for the target attribute type!') + when 19 + fail_with(Msf::Module::Failure::BadConfig, 'A constraint on the operation was not satisfied') when 32 fail_with(Msf::Module::Failure::UnexpectedReply, 'The LDAP search failed because the operation targeted an entity within the base DN that does not exist.') when 33 diff --git a/modules/auxiliary/admin/ldap/change_password.rb b/modules/auxiliary/admin/ldap/change_password.rb new file mode 100644 index 000000000000..57f6ae44e2f0 --- /dev/null +++ b/modules/auxiliary/admin/ldap/change_password.rb @@ -0,0 +1,155 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Auxiliary + + include Msf::Auxiliary::Report + include Msf::Exploit::Remote::LDAP + include Msf::OptionalSession::LDAP + + ATTRIBUTE = 'unicodePwd'.freeze + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Change Password', + 'Description' => %q{ + This module allows Active Directory users to change their own passwords, or reset passwords for + accounts they have privileges over. + }, + 'Author' => [ + 'smashery' # module author + ], + 'References' => [ + ['URL', 'https://github.com/fortra/impacket/blob/master/examples/changepasswd.py'], + ['URL', 'https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2'], + ], + 'License' => MSF_LICENSE, + 'Actions' => [ + ['RESET', { 'Description' => "Reset a target user's password, having permissions over their account" }], + ['CHANGE', { 'Description' => "Change the user's password, knowing the existing password" }] + ], + 'DefaultAction' => 'RESET', + 'Notes' => { + 'Stability' => [], + 'SideEffects' => [ IOC_IN_LOGS ], + 'Reliability' => [] + } + ) + ) + + register_options([ + OptString.new('TARGET_USER', [false, 'The user to reset the password of.'], conditions: ['ACTION', 'in', %w[RESET]]), + OptString.new('NEW_PASSWORD', [ true, 'The new password to set for the user' ]) + ]) + end + + def fail_with_ldap_error(message) + ldap_result = @ldap.get_operation_result.table + return if ldap_result[:code] == 0 + + print_error(message) + if ldap_result[:code] == 19 + extra_error = '' + if action.name == 'CHANGE' && !datastore['SESSION'].blank? + # If you're already in a session, you could provide the wrong password, and you get this error + extra_error = ' or incorrect current password' + end + + error = "The password changed failed, likely due to a password policy violation (e.g. not sufficiently complex, matching previous password, or changing the password too often)#{extra_error}" + fail_with(Failure::NotFound, error) + else + validate_query_result!(ldap_result) + end + end + + def ldap_get(filter, attributes: []) + raw_obj = @ldap.search(base: @base_dn, filter: filter, attributes: attributes)&.first + return nil unless raw_obj + + obj = {} + + obj['dn'] = raw_obj['dn'].first.to_s + unless raw_obj['sAMAccountName'].empty? + obj['sAMAccountName'] = raw_obj['sAMAccountName'].first.to_s + end + + obj + end + + def run + if action.name == 'CHANGE' + fail_with(Failure::BadConfig, 'Must set USERNAME when changing password') if datastore['USERNAME'].blank? + fail_with(Failure::BadConfig, 'Must set PASSWORD when changing password') if datastore['PASSWORD'].blank? + elsif action.name == 'RESET' + fail_with(Failure::BadConfig, 'Must set TARGET_USER when resetting password') if datastore['TARGET_USER'].blank? + end + if session.blank? && datastore['USERNAME'].blank? && datastore['LDAP::Auth'] != Msf::Exploit::Remote::AuthOption::SCHANNEL + print_warning('Connecting with an anonymous bind') + end + ldap_connect do |ldap| + validate_bind_success!(ldap) + + if (@base_dn = datastore['BASE_DN']) + print_status("User-specified base DN: #{@base_dn}") + else + print_status('Discovering base DN automatically') + + if (@base_dn = ldap.base_dn) + print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}") + else + fail_with(Failure::UnexpectedReply, "Couldn't discover base DN!") + end + end + @ldap = ldap + + begin + send("action_#{action.name.downcase}") + rescue ::IOError => e + fail_with(Failure::UnexpectedReply, e.message) + end + end + rescue Errno::ECONNRESET + fail_with(Failure::Disconnected, 'The connection was reset.') + rescue Rex::ConnectionError => e + fail_with(Failure::Unreachable, e.message) + rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e + fail_with(Failure::NoAccess, e.message) + rescue Rex::Proto::LDAP::LdapException => e + fail_with(Failure::NoAccess, e.message) + rescue Net::LDAP::Error => e + fail_with(Failure::Unknown, "#{e.class}: #{e.message}") + end + + def get_user_obj(username) + obj = ldap_get("(sAMAccountName=#{ldap_escape_filter(username)})", attributes: ['sAMAccountName']) + fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{username}") unless obj + + obj + end + + def action_reset + target_user = datastore['TARGET_USER'] + obj = get_user_obj(target_user) + + new_pass = "\"#{datastore['NEW_PASSWORD']}\"".encode('utf-16le').bytes.pack('c*') + unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, new_pass) + fail_with_ldap_error("Failed to reset the password for #{datastore['TARGET_USER']}.") + end + print_good("Successfully reset password for #{datastore['TARGET_USER']}.") + end + + def action_change + obj = get_user_obj(datastore['USERNAME']) + + new_pass = "\"#{datastore['NEW_PASSWORD']}\"".encode('utf-16le').bytes.pack('c*') + old_pass = "\"#{datastore['PASSWORD']}\"".encode('utf-16le').bytes.pack('c*') + unless @ldap.modify(dn: obj['dn'], operations: [[:delete, ATTRIBUTE, old_pass], [:add, ATTRIBUTE, new_pass]]) + fail_with_ldap_error("Failed to reset the password for #{datastore['USERNAME']}.") + end + print_good("Successfully changed password for #{datastore['USERNAME']}.") + end +end