Skip to content

Commit

Permalink
Merge pull request #19671 from smashery/ldap_change_pw
Browse files Browse the repository at this point in the history
LDAP Change Password module
  • Loading branch information
smcintyre-r7 authored Dec 6, 2024
2 parents b4762b7 + a708f8c commit 909476e
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 0 deletions.
39 changes: 39 additions & 0 deletions documentation/modules/auxiliary/admin/ldap/change_password.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions lib/msf/core/exploit/remote/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
155 changes: 155 additions & 0 deletions modules/auxiliary/admin/ldap/change_password.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 909476e

Please sign in to comment.