358 lines
13 KiB
Ruby
358 lines
13 KiB
Ruby
# 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
|