-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19671 from smashery/ldap_change_pw
LDAP Change Password module
- Loading branch information
Showing
3 changed files
with
196 additions
and
0 deletions.
There are no files selected for viewing
39 changes: 39 additions & 0 deletions
39
documentation/modules/auxiliary/admin/ldap/change_password.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |