Files
metasploit-gs/modules/auxiliary/admin/ldap/ldap_object_attribute.rb
T
2025-07-30 15:08:14 -07:00

154 lines
5.5 KiB
Ruby

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::LDAP
include Msf::OptionalSession::LDAP
def initialize(info = {})
super(
update_info(
info,
'Name' => 'LDAP Update Object',
'Description' => %q{
This module allows creating, reading, updating and deleting attributes of LDAP objects.
Users can specify the object and must specify a corresponding attribute.
},
'Author' => ['jheysel'],
'License' => MSF_LICENSE,
'Actions' => [
['CREATE', { 'Description' => 'Create an LDAP object' }],
['READ', { 'Description' => 'Read the the LDAP object' }],
['UPDATE', { 'Description' => 'Modify the LDAP object' }],
['DELETE', { 'Description' => 'Delete the LDAP object' }]
],
'DefaultAction' => 'READ',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS, CONFIG_CHANGES]
}
)
)
register_options(
[
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),
OptEnum.new('OBJECT_LOOKUP', [true, 'How to look up the target LDAP object', 'dN', ['dN', 'sAMAccountName']]),
OptString.new('OBJECT', [true, 'The target LDAP object']),
OptString.new('ATTRIBUTE', [true, 'The LDAP attribute to update (e.g., userPrincipalName)']),
OptString.new('VALUE', [false, 'The value for the specified LDAP object attribute'], conditions: ['ACTION', 'in', %w[Create Update] ])
]
)
end
def find_target_object
search_filter = "(&(#{ldap_escape_filter(datastore['OBJECT_LOOKUP'])}=#{ldap_escape_filter(datastore['OBJECT'])}))"
result = []
@ldap.search(base: @base_dn, filter: search_filter, attributes: ['distinguishedName', datastore['ATTRIBUTE']]) do |entry|
result << entry
end
if result.empty?
fail_with(Failure::NotFound, "Could not find any object matching the filter: #{search_filter}")
elsif result.size > 1
fail_with(Failure::UnexpectedReply, "Found multiple objects matching the filter: #{search_filter}. This should not happen.")
end
result.first
end
def action_read
target_object = find_target_object
target_dn = target_object['dN'].first
attribute_value = target_object[datastore['ATTRIBUTE'].to_sym]&.first
if attribute_value.blank?
fail_with(Failure::NotFound, "Attribute #{datastore['ATTRIBUTE']} is not set for #{target_dn}")
end
print_good("Found #{target_dn} with #{datastore['ATTRIBUTE']} set to #{attribute_value}")
attribute_value
end
def action_create
target_object = find_target_object
target_dn = target_object['dN'].first
attribute = datastore['ATTRIBUTE'].to_sym
value = datastore['VALUE']
print_status("Attempting to add attribute #{datastore['ATTRIBUTE']} with value #{value} to #{target_dn}...")
ops = [[:add, attribute, value]]
@ldap.modify(dn: target_dn, operations: ops)
validate_query_result!(@ldap.get_operation_result.table)
print_good("Successfully added attribute #{datastore['ATTRIBUTE']} with value #{value} to #{target_dn}")
end
def action_update
target_object = find_target_object
target_dn = target_object['dN'].first
attribute = datastore['ATTRIBUTE'].to_sym
original_value = target_object[attribute]&.first
print_status("Current value of #{datastore['OBJECT']}'s #{datastore['ATTRIBUTE']}: #{original_value}")
ops = [[:replace, attribute, datastore['VALUE']]]
print_status("Attempting to update #{datastore['ATTRIBUTE']} for #{target_dn} to #{datastore['VALUE']}...")
@ldap.modify(dn: target_dn, operations: ops)
validate_query_result!(@ldap.get_operation_result.table)
print_good("Successfully updated #{target_dn}'s #{datastore['ATTRIBUTE']} to #{datastore['VALUE']}")
original_value
end
def action_delete
target_object = find_target_object
target_dn = target_object['dN'].first
attribute = datastore['ATTRIBUTE'].to_sym
print_status("Attempting to delete attribute #{datastore['ATTRIBUTE']} from #{target_dn}...")
ops = [[:delete, attribute]]
@ldap.modify(dn: target_dn, operations: ops)
validate_query_result!(@ldap.get_operation_result.table)
print_good("Successfully deleted attribute #{datastore['ATTRIBUTE']} from #{target_dn}")
end
def run
if (datastore['ACTION'].downcase == 'update' || datastore['ACTION'].downcase == 'create') && datastore['VALUE'].blank?
fail_with(Failure::BadConfig, 'The VALUE option must be set for CREATE and UPDATE actions.')
end
ldap_connect do |ldap|
validate_bind_success!(ldap)
if (@base_dn = datastore['BASE_DN'])
vprint_status("User-specified base DN: #{@base_dn}")
else
vprint_status('Discovering base DN automatically')
unless (@base_dn = ldap.base_dn)
fail_with(Failure::NotFound, "Couldn't discover base DN!")
end
end
@ldap = ldap
result = send("action_#{action.name.downcase}")
print_good('The operation completed successfully!')
result
end
rescue Errno::ECONNRESET
fail_with(Failure::Disconnected, 'The connection was reset.')
rescue Rex::ConnectionError => e
fail_with(Failure::Unreachable, e.message)
rescue Net::LDAP::Error => e
fail_with(Failure::Unknown, "#{e.class}: #{e.message}")
end
end