From fa7d677d452eda59cd712d840e2eee44d22f3af9 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 24 Oct 2022 13:58:27 -0400 Subject: [PATCH] Consolidate and improve LDAP error handling --- .../modules/auxiliary/admin/ldap/rbcd.md | 0 lib/msf/core/exploit/remote/ldap.rb | 24 +++++ modules/auxiliary/admin/ldap/rbcd.rb | 89 +++++++++++-------- modules/auxiliary/gather/ldap_query.rb | 23 +---- 4 files changed, 78 insertions(+), 58 deletions(-) create mode 100644 documentation/modules/auxiliary/admin/ldap/rbcd.md diff --git a/documentation/modules/auxiliary/admin/ldap/rbcd.md b/documentation/modules/auxiliary/admin/ldap/rbcd.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/msf/core/exploit/remote/ldap.rb b/lib/msf/core/exploit/remote/ldap.rb index 2edd66d8a8..13d1b89c24 100644 --- a/lib/msf/core/exploit/remote/ldap.rb +++ b/lib/msf/core/exploit/remote/ldap.rb @@ -135,5 +135,29 @@ module Msf print_good("#{peer} Discovered base DN: #{base_dn}") base_dn end + + def validate_bind_success!(ldap) + bind_result = ldap.as_json['result']['ldap_result'] + + # Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes + case bind_result['resultCode'] + when 0 + print_good('Successfully bound to the LDAP server!') + when 1 + fail_with(Msf::Exploit::Remote::Failure::NoAccess, "An operational error occurred, perhaps due to lack of authorization. The error was: #{bind_result['errorMessage'].strip}") + when 7 + fail_with(Msf::Exploit::Remote::Failure::NoTarget, 'Target does not support the simple authentication mechanism!') + when 8 + fail_with(Msf::Exploit::Remote::Failure::NoTarget, "Server requires a stronger form of authentication than we can provide! The error was: #{bind_result['errorMessage'].strip}") + when 14 + fail_with(Msf::Exploit::Remote::Failure::NoTarget, "Server requires additional information to complete the bind. Error was: #{bind_result['errorMessage'].strip}") + when 48 + fail_with(Msf::Exploit::Remote::Failure::NoAccess, "Target doesn't support the requested authentication type we sent. Try binding to the same user without a password, or providing credentials if you were doing anonymous authentication.") + when 49 + fail_with(Msf::Exploit::Remote::Failure::NoAccess, 'Invalid credentials provided!') + else + fail_with(Msf::Exploit::Remote::Failure::Unknown, "Unknown error occurred whilst binding: #{bind_result['errorMessage'].strip}") + end + end end end diff --git a/modules/auxiliary/admin/ldap/rbcd.rb b/modules/auxiliary/admin/ldap/rbcd.rb index f1f11070b4..96bd1f85bd 100644 --- a/modules/auxiliary/admin/ldap/rbcd.rb +++ b/modules/auxiliary/admin/ldap/rbcd.rb @@ -15,6 +15,10 @@ class MetasploitModule < Msf::Auxiliary info, 'Name' => 'Role Base Constrained Delegation', 'Description' => %q{ + This module can read and write the necessary LDAP attributes to configure a particular object for Role Based + Constrained Delegation (RBCD). When writing, the module will add an access control entry to allow the account + specified in DELEGATE_FROM to the object specified in DELEGATE_TO. In order for this to succeed, the + authenticated user must have write access to the target object (the object specified in DELEGATE_TO). }, 'Author' => [ 'Podalirius', # Remi Gascou (@podalirius_), Impacket reference implementation @@ -28,9 +32,9 @@ class MetasploitModule < Msf::Auxiliary ], 'License' => MSF_LICENSE, 'Actions' => [ + ['FLUSH', { 'Description' => 'Delete the security descriptor' }], ['READ', { 'Description' => 'Read the security descriptor' }], ['REMOVE', { 'Description' => 'Remove matching ACEs from the security descriptor DACL' }], - ['FLUSH', { 'Description' => 'Delete the security descriptor' }], ['WRITE', { 'Description' => 'Add an ACE to the security descriptor DACL' }] ], 'DefaultAction' => 'READ', @@ -57,6 +61,34 @@ class MetasploitModule < Msf::Auxiliary }) end + def fail_with_ldap_error(message) + ldap_result = @ldap.as_json['result']['ldap_result'] + return if ldap_result['resultCode'] == 0 + + print_error(message) + # Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes + case ldap_result['resultCode'] + when 1 + fail_with(Failure::Unknown, "An LDAP operational error occurred. The error was: #{ldap_result['errorMessage'].strip}") + when 16 + fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist.') + when 50 + fail_with(Failure::NoAccess, 'The LDAP operation failed due to insufficient access rights.') + when 51 + fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is too busy to perform the request.') + when 52 + fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is not currently available to process the request.') + when 53 + fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is unwilling to perform the request.') + when 64 + fail_with(Failure::Unknown, 'The LDAP operation failed due to a naming violation.') + when 65 + fail_with(Failure::Unknown, 'The LDAP operation failed due to an object class violation.') + end + + fail_with(Failure::Unknown, "Unknown LDAP error occurred: result: #{ldap_result['resultCode']} message: #{ldap_result['errorMessage'].strip}") + end + def get_delegate_from_obj delegate_from = datastore['DELEGATE_FROM'] if delegate_from.blank? @@ -84,8 +116,8 @@ class MetasploitModule < Msf::Auxiliary obj['ObjectSid'] = Rex::Proto::MsDtyp::MsDtypSid.read(raw_obj['ObjectSid'].first) end - unless raw_obj['msDS-AllowedToActOnBehalfOfOtherIdentity'].empty? - obj['msDS-AllowedToActOnBehalfOfOtherIdentity'] = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(raw_obj['msDS-AllowedToActOnBehalfOfOtherIdentity'].first) + unless raw_obj[ATTRIBUTE].empty? + obj[ATTRIBUTE] = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(raw_obj[ATTRIBUTE].first) end obj @@ -93,27 +125,8 @@ class MetasploitModule < Msf::Auxiliary def run ldap_connect do |ldap| - bind_result = ldap.as_json['result']['ldap_result'] + validate_bind_success!(ldap) - # Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes - case bind_result['resultCode'] - when 0 - print_good('Successfully bound to the LDAP server!') - when 1 - fail_with(Failure::NoAccess, "An operational error occurred, perhaps due to lack of authorization. The error was: #{bind_result['errorMessage']}") - when 7 - fail_with(Failure::NoTarget, 'Target does not support the simple authentication mechanism!') - when 8 - fail_with(Failure::NoTarget, "Server requires a stronger form of authentication than we can provide! The error was: #{bind_result['errorMessage']}") - when 14 - fail_with(Failure::NoTarget, "Server requires additional information to complete the bind. Error was: #{bind_result['errorMessage']}") - when 48 - fail_with(Failure::NoAccess, "Target doesn't support the requested authentication type we sent. Try binding to the same user without a password, or providing credentials if you were doing anonymous authentication.") - when 49 - fail_with(Failure::NoAccess, 'Invalid credentials provided!') - else - fail_with(Failure::Unknown, "Unknown error occurred whilst binding: #{bind_result['errorMessage']}") - end if (@base_dn = datastore['BASE_DN']) print_status("User-specified base DN: #{@base_dn}") else @@ -125,7 +138,7 @@ class MetasploitModule < Msf::Auxiliary end @ldap = ldap - obj = ldap_get("(sAMAccountName=#{datastore['DELEGATE_TO']})", attributes: ['sAMAccountName', 'ObjectSID', 'msDS-AllowedToActOnBehalfOfOtherIdentity']) + obj = ldap_get("(sAMAccountName=#{datastore['DELEGATE_TO']})", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE]) fail_with(Failure::NotFound, "Failed to find: #{datastore['DELEGATE_TO']}") unless obj send("action_#{action.name.downcase}", obj) @@ -158,7 +171,7 @@ class MetasploitModule < Msf::Auxiliary security_descriptor = obj[ATTRIBUTE] unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty? - print_status('No DACL ACEs are present, no changes are necessary.') + print_status('No DACL ACEs are present. No changes are necessary.') return end @@ -166,7 +179,7 @@ class MetasploitModule < Msf::Auxiliary aces.delete_if { |ace| ace.body[:sid] == delegate_from['ObjectSid'] } delta = security_descriptor.dacl.aces.length - aces.length if delta == 0 - print_status('No DACL ACEs matched, no changes are necessary.') + print_status('No DACL ACEs matched. No changes are necessary.') return else print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.") @@ -176,25 +189,28 @@ class MetasploitModule < Msf::Auxiliary security_descriptor.dacl.acl_count.clear security_descriptor.dacl.acl_size.clear - unless @ldap.replace_attribute(obj['dn'], 'msDS-AllowedToActOnBehalfOfOtherIdentity', security_descriptor.to_binary_s) - print_error('Failed to update the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') - return + unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s) + fail_with_ldap_error('Failed to update the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') end print_good('Successfully updated the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') end def action_flush(obj) - unless @ldap.delete_attribute(obj['dn'], 'msDS-AllowedToActOnBehalfOfOtherIdentity') - print_error('Failed to deleted the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') + unless obj[ATTRIBUTE] + print_status('The msDS-AllowedToActOnBehalfOfOtherIdentity field is empty. No changes are necessary.') return end + unless @ldap.delete_attribute(obj['dn'], ATTRIBUTE) + fail_with_ldap_error('Failed to deleted the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') + end + print_good('Successfully deleted the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') end def action_write(obj) delegate_from = get_delegate_from_obj - if obj['msDS-AllowedToActOnBehalfOfOtherIdentity'] + if obj[ATTRIBUTE] _action_write_update(obj, delegate_from) else _action_write_create(obj, delegate_from) @@ -208,9 +224,8 @@ class MetasploitModule < Msf::Auxiliary security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS security_descriptor.dacl.aces << build_ace(delegate_from['ObjectSid']) - unless @ldap.add_attribute(obj['dn'], 'msDS-AllowedToActOnBehalfOfOtherIdentity', security_descriptor.to_binary_s) - print_error('Failed to create the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') - return + unless @ldap.add_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s) + fail_with_ldap_error('Failed to create the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') end print_good('Successfully created the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') end @@ -233,8 +248,8 @@ class MetasploitModule < Msf::Auxiliary security_descriptor.dacl.aces << build_ace(delegate_from['ObjectSid']) - unless @ldap.replace_attribute(obj['dn'], 'msDS-AllowedToActOnBehalfOfOtherIdentity', security_descriptor.to_binary_s) - print_error('Failed to update the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') + unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s) + fail_with_ldap_error('Failed to update the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') return end print_good('Successfully updated the msDS-AllowedToActOnBehalfOfOtherIdentity attribute.') diff --git a/modules/auxiliary/gather/ldap_query.rb b/modules/auxiliary/gather/ldap_query.rb index 78ceff306c..3a11cb52cb 100644 --- a/modules/auxiliary/gather/ldap_query.rb +++ b/modules/auxiliary/gather/ldap_query.rb @@ -435,27 +435,8 @@ class MetasploitModule < Msf::Auxiliary entries = nil begin ldap_connect do |ldap| - bind_result = ldap.as_json['result']['ldap_result'] + validate_bind_success!(ldap) - # Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes - case bind_result['resultCode'] - when 0 - print_good('Successfully bound to the LDAP server!') - when 1 - fail_with(Failure::NoAccess, "An operational error occurred, perhaps due to lack of authorization. The error was: #{bind_result['errorMessage']}") - when 7 - fail_with(Failure::NoTarget, 'Target does not support the simple authentication mechanism!') - when 8 - fail_with(Failure::NoTarget, "Server requires a stronger form of authentication than we can provide! The error was: #{bind_result['errorMessage']}") - when 14 - fail_with(Failure::NoTarget, "Server requires additional information to complete the bind. Error was: #{bind_result['errorMessage']}") - when 48 - fail_with(Failure::NoAccess, "Target doesn't support the requested authentication type we sent. Try binding to the same user without a password, or providing credentials if you were doing anonymous authentication.") - when 49 - fail_with(Failure::NoAccess, 'Invalid credentials provided!') - else - fail_with(Failure::Unknown, "Unknown error occurred whilst binding: #{bind_result['errorMessage']}") - end if (@base_dn = datastore['BASE_DN']) print_status("User-specified base DN: #{@base_dn}") else @@ -469,7 +450,7 @@ class MetasploitModule < Msf::Auxiliary case action.name when 'RUN_QUERY_FILE' unless datastore['QUERY_FILE_PATH'] - fail_with(Failure::BadConfig, 'When using the RUN_QUERY_FILE action, one must specify the path to the JASON/YAML file containing the queries via QUERY_FILE_PATH!') + fail_with(Failure::BadConfig, 'When using the RUN_QUERY_FILE action, one must specify the path to the JSON/YAML file containing the queries via QUERY_FILE_PATH!') end print_status("Loading queries from #{datastore['QUERY_FILE_PATH']}...")