Module: Msf::Exploit::Remote::Unirpc

Defined in:
lib/msf/core/exploit/remote/unirpc.rb

Overview

Defined Under Namespace

Classes: UniRPCCommunicationError, UniRPCError, UniRPCUnexpectedResponseError, UniRPCUsageError

Constant Summary collapse

UNIRPC_TYPE_INTEGER =

Argument types

0
UNIRPC_TYPE_FLOAT =
1
UNIRPC_TYPE_STRING =
2
UNIRPC_TYPE_BYTES =
3
UNIRPC_MESSAGE_LOGIN =

Message types

0x0F
UNIRPC_MESSAGE_OSCOMMAND =
0x06

Instance Method Summary collapse

Instance Method Details

#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) ⇒ Object

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)



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/msf/core/exploit/remote/unirpc.rb', line 106

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

#initialize(info = {}) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/msf/core/exploit/remote/unirpc.rb', line 26

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

#recv_unirpc_message(sock, first_result_is_status: false) ⇒ Object

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



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/msf/core/exploit/remote/unirpc.rb', line 242

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

#unirpc_get_versionObject



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/msf/core/exploit/remote/unirpc.rb', line 38

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