lib/msf/core/exploit/remote/unirpc.rb
# Encoding: ASCII-8BIT
module Msf
class Exploit
class Remote
# Adapted from https://github.com/rbowes-r7/libneptune
module Unirpc
class UniRPCError < StandardError; end
class UniRPCCommunicationError < UniRPCError; end
class UniRPCUnexpectedResponseError < UniRPCError; end
# This exception is caused by using illegal values in the module and
# probably doesn't need to be caught
class UniRPCUsageError < UniRPCError; end
# Argument types
UNIRPC_TYPE_INTEGER = 0
UNIRPC_TYPE_FLOAT = 1
UNIRPC_TYPE_STRING = 2
UNIRPC_TYPE_BYTES = 3
# Message types
UNIRPC_MESSAGE_LOGIN = 0x0F
UNIRPC_MESSAGE_OSCOMMAND = 0x06
def initialize(info = {})
super
@error_codes = YAML.safe_load(::File.join(Msf::Config.data_directory, 'unirpc-errors.yaml'))
# This will let the module decide whether or not to use the
# packet-level encoding
register_advanced_options([
OptBool.new('UNIRPC_ENCODE_MESSAGES', [true, "Use UniRPC's message encoding (which obscures messages by XORing with a constant", true])
])
end
def unirpc_get_version
# These are the services we've found that return version numbers
['defcs', 'udserver'].each do |service|
vprint_status("Trying to get version number from service #{service}...")
connect
sock.put(build_unirpc_message(args: [
# Service name
{ type: :string, value: service },
# "Secure" flag - this must be non-zero if the server is started in
# "secure" mode (-s) - it makes no actual difference to us,
# so just use secure mode to cover all bases
{ type: :integer, value: 1 },
]))
result = recv_unirpc_message(sock)
if result&.dig(:args, 0, :type) == :string
version = result.dig(:args, 0, :value)&.gsub(/.*:/, '')
unless version.nil?
return version
end
end
ensure
disconnect
end
raise(UniRPCUnexpectedResponseError, 'Could not determine UniRPC version!')
end
# Build a unirpc packet. There are lots of arguments defined, pretty much all
# of them optional.
#
# Header fields:.
# * version_byte: The protocol version (this is always 0x6c in the protocol)
# * other_version_byte: Another version byte (always 0x01 in the protocol)
# * body_length_override: The length of the body (automatically calculated, normally)
# * argcount_override: If set, specifies a custom number of "args"
# (automatically calculated, normally)
#
# Body fields:
#
# * body_override: If set, use it as the literal body and ignore the rest of these
# * oldschool_data: The service supports two different types of serialized
# data; AFAICT, this field is just free-form string data that nothing really
# seems to support
# * args: An array of arguments (the most common way to pass arguments to an
# rpc call).
#
# Args are an array of hashes with :type / :value
# Valid types:
# :integer - :value is the integer (32-bits)
# :string / :bytes - value is the string or nil
# :float - :value is just a 64-bit value
#
# Integer and Float values also have an :extra field, which is sent
# where the string's length would go - I think it's normally set to
# uninitialized memory, so probably you never need it.
#
# String values have a boolean :null_terminate field as well, in case
# you want to disable null-termination (the service uses the length
# field in some cases, and null termination in others, so it could be
# interesting)
#
# Set :skip_header to not attach a header (some services require only
# a body)
def build_unirpc_message(
version_byte: 0x6c,
other_version_byte: 0x01,
body_length_override: nil,
argcount_override: nil,
body_override: nil,
oldschool_data: '',
args: [],
skip_header: false
)
encrypt = datastore['UNIRPC_ENCODE_MESSAGES']
# Ensure this is a string (in case the caller sets it to nil or something
oldschool_data = oldschool_data.to_s
# Allow the caller to override the body entirely, instead of packing
# arguments
if body_override
body = body_override
else
# Pack the args at the start of the body - this is kinda metadata-ish
body = args.map do |a|
case a[:type]
when :integer
# Ints ignore the first value, and the second is always 0
[a[:extra] || 0, UNIRPC_TYPE_INTEGER].pack('NN')
when :string
# Strings store the length in the first value, and the value in the body
if a[:null_terminate].nil? || a[:null_terminate] == true
[a[:value].length + 1, UNIRPC_TYPE_STRING].pack('NN')
else
[a[:value].length, UNIRPC_TYPE_STRING].pack('NN')
end
when :bytes
# Bytes / rpcstrings store the length in the first value, and the value in the body
[a[:value].length, UNIRPC_TYPE_BYTES].pack('NN')
when :float
# Floats ignore the first value, and the second value is the type
[a[:extra] || 0, UNIRPC_TYPE_FLOAT].pack('NN')
else
raise(UniRPCUsageError, "Tried to build UniRPC packet with unknown type: #{a[:type]}")
end
end.join
# Follow it with the 'oldschool_data' arg
body += oldschool_data
# Follow that data section with the args - this is the value of the args
body += args.map do |a|
case a[:type]
when :integer
[a[:value]].pack('N')
when :string
str = a[:value]
if a[:null_terminate].nil? || a[:null_terminate] == true
str += "\0"
end
# Align to multiple of 4, always adding at least one
str += "\0"
str += "\0" while (str.length % 4) != 0
str
when :bytes
str = a[:value]
# Alignment
str += "\0" while (str.length % 4) != 0
str
when :float
[a[:value]].pack('Q')
else
raise(UniRPCUsageError, "Tried to build UniRPC packet with unknown type: #{a[:type]}")
end
end.join
end
# "Encrypt" if we're supposed to
# We use the key "2", other options include "1"
if encrypt
body = body.bytes.map do |b|
(b ^ 2).chr
end.join
end
# Figure out the argcount
if argcount_override
argcount = argcount_override
else
argcount = args.length
# If we pass plaintext data, it actually counts as an extra arg
if oldschool_data != ''
argcount += 1
end
end
# Let the user to skip appending a header, if they choose
if skip_header
return body
end
# Pack the header
header = [
version_byte, # Has to be 0x6c
other_version_byte, # Can be 0x01 or 0x02
0x00, # Reserved (ignored)
0x00, # Reserved (ignored)
body_length_override || body.length, # Length of data (0x7FFFFFFF => heap overflow)
0x00000000, # Reserved (ignored)
2, # Encryption "key" - basically the XOR key (can only be 1 or 2)
0, # Do compression?
encrypt ? 1 : 0, # Encryption (0 = not encrypted, 1 = encrypted)
0x00, # Padding
0x00000000, # Unknown (reserved?) 0 unused, but has to be 0
argcount, # Argcount, which we compute earlier
oldschool_data.length # Data length
].pack('CCCCNNCCCCNnn')
return header + body
end
# Receive and parse a message from UniRPC server on the given socket
#
# Many RPC replies put a status / error code in the first argument. To
# check that argument and raise an error when the server returns an
# error, set first_result_is_status to true
def recv_unirpc_message(sock, first_result_is_status: false)
# Receive the header
header = sock.get_once(0x18)
# Make sure we received all of it
if header.nil?
raise(UniRPCCommunicationError, "Couldn't receive UniRPC packet header")
elsif header.length < 0x18
raise(UniRPCCommunicationError, "UniRPC packet header was truncated (expected 24 bytes, received #{header.length}) - this might not be a UniRPC server")
end
# Parse out the fields
(
version_byte,
other_version_byte,
_reserved1,
_reserved2,
body_length,
_reserved3,
encryption_key,
claim_compression,
claim_encryption,
_reserved4,
_reserved5,
argcount,
data_length,
) = header.unpack('CCCCNNCCCCNnn')
# Note that we don't attempt to decrypt / decompress here, because
# we've never seen a server actually enable encryption or compression
# (even if we start it)
results = {
header: header,
version_byte: version_byte,
other_version_byte: other_version_byte,
body_length: body_length,
encryption_key: encryption_key,
claim_compression: claim_compression,
claim_encryption: claim_encryption,
argcount: argcount,
data_length: data_length
}
# Receive the body
body = sock.get_once(body_length)
if body.length != body_length
raise(UniRPCCommunicationError, "UniRPC packet body was truncated (expected #{body_length} bytes, received #{body.length}) - this might not be a UniRPC server")
end
# Parse the argument metadata, data, and argument data
args, _data, extra_data = body.unpack("a#{argcount * 8}a#{data_length}a*")
# Parse the argument metadata + data
results[:args] = []
1.upto(argcount) do
arg, args = args.unpack('a8a*')
(value, type) = arg.unpack('NN')
case type
when UNIRPC_TYPE_INTEGER # 32-bit integer
(arg_data, extra_data) = extra_data.unpack('Na*')
results[:args] << {
type: :integer,
value: arg_data,
extra: value
}
when UNIRPC_TYPE_STRING # Null-able string
if value == 0
string_value = nil
else
(string, extra_data) = extra_data.unpack("a#{value}a*")
string_value = string
end
results[:args] << {
type: :string,
value: string_value,
extra: value
}
when UNIRPC_TYPE_BYTES # They call this "RPC String"
(string, extra_data) = extra_data.unpack("a#{value}a*")
string_value = string
results[:args] << {
type: :string,
value: string_value
}
else
raise(UniRPCUnexpectedResponseError, "Unidata: received unknown RPC type (#{type})!")
end
end
if first_result_is_status
if results&.dig(:args, 0, :type) != :integer
raise(UniRPCUnexpectedResponseError, 'UniRPC server returned a non-integer status code')
end
error_code = results[:args][0][:value]
if error_code != 0
raise(UniRPCUnexpectedResponseError, "UniRPC server returned an error code: #{@error_codes[error_code] || "Unknown error: #{error_code}"}")
end
end
return results
end
end
end
end
end