diff --git a/lib/rex/java/serialization.rb b/lib/rex/java/serialization.rb new file mode 100644 index 0000000000..9638ce4f45 --- /dev/null +++ b/lib/rex/java/serialization.rb @@ -0,0 +1,34 @@ +module Rex + module Java + # Include constants defining terminal and constant + # values expected in a stream. + module Serialization + STREAM_MAGIC = 0xaced + STREAM_VERSION = 5 + TC_NULL = 0x70 + TC_REFERENCE = 0x71 + TC_CLASSDESC = 0x72 + TC_OBJECT = 0x73 + TC_STRING = 0x74 + TC_ARRAY = 0x75 + TC_CLASS = 0x76 + TC_BLOCKDATA = 0x77 + TC_ENDBLOCKDATA = 0x78 + TC_RESET = 0x79 + TC_BLOCKDATALONG = 0x7A + TC_EXCEPTION = 0x7B + TC_LONGSTRING = 0x7C + TC_PROXYCLASSDESC = 0x7D + TC_ENUM = 0x7E + BASE_WIRE_HANDLE = 0x7E0000 + + SC_WRITE_METHOD = 0x01 # if SC_SERIALIZABLE + SC_BLOCK_DATA = 0x08 # if SC_EXTERNALIZABLE + SC_SERIALIZABLE = 0x02 + SC_EXTERNALIZABLE = 0x04 + SC_ENUM = 0x10 + end + end +end + +require 'rex/java/serialization/model' \ No newline at end of file diff --git a/lib/rex/java/serialization/model.rb b/lib/rex/java/serialization/model.rb new file mode 100644 index 0000000000..0504b50f66 --- /dev/null +++ b/lib/rex/java/serialization/model.rb @@ -0,0 +1,5 @@ +require 'rex/java/serialization/model/element' +require 'rex/java/serialization/model/end_block_data' +require 'rex/java/serialization/model/null_reference' +require 'rex/java/serialization/model/utf' +require 'rex/java/serialization/model/field' \ No newline at end of file diff --git a/lib/rex/java/serialization/model/element.rb b/lib/rex/java/serialization/model/element.rb new file mode 100644 index 0000000000..257b35636b --- /dev/null +++ b/lib/rex/java/serialization/model/element.rb @@ -0,0 +1,26 @@ +module Rex + module Java + module Serialization + module Model + class Element + def self.decode(io) + elem = self.new + elem.decode(io) + end + + def initialize + + end + + def decode(io) + self + end + + def encode + '' + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/java/serialization/model/end_block_data.rb b/lib/rex/java/serialization/model/end_block_data.rb new file mode 100644 index 0000000000..83cd6c919d --- /dev/null +++ b/lib/rex/java/serialization/model/end_block_data.rb @@ -0,0 +1,10 @@ +module Rex + module Java + module Serialization + module Model + class EndBlockData < Element + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/java/serialization/model/field.rb b/lib/rex/java/serialization/model/field.rb new file mode 100644 index 0000000000..5a1857f384 --- /dev/null +++ b/lib/rex/java/serialization/model/field.rb @@ -0,0 +1,167 @@ +module Rex + module Java + module Serialization + module Model + # This class provides a field description representation (fieldDesc). It's used for + # both primitive descriptions (primitiveDesc) and object descriptions (objectDesc). + class Field < Element + + PRIMITIVE_TYPE_CODES = { + 'B' => 'byte', + 'C' => 'char', + 'D' => 'double', + 'F' => 'float', + 'I' => 'integer', + 'J' => 'long', + 'S' => 'short', + 'Z' => 'boolean', + } + + OBJECT_TYPE_CODES = { + '[' => 'array', + 'L' => 'object' + } + + TYPE_CODES = PRIMITIVE_TYPE_CODES.merge(OBJECT_TYPE_CODES) + + # @!attribute type + # @return [String] The type of the field. + attr_accessor :type + # @!attribute name + # @return [Java::Serialization::Model::Utf] The name of the field. + attr_accessor :name + # @!attribute field_type + # @return [Java::Serialization::Model::Utf] The type of the field on object types. + attr_accessor :field_type + + # Unserializes a Java::Serialization::Field + # + # @param io [IO] the io to read from + # @return [Java::Serialization::Model::Field] if deserialization is possible + # @return [nil] if deserialization isn't possible + def self.decode(io) + elem = self.new + + elem.decode(io) + end + + def initialize + self.type = '' + self.name = nil + self.field_type = nil + end + + # Unserializes a Java::Serialization::Model::Field + # + # @param io [IO] the io to read from + # @return [self] if deserialization is possible + # @return [nil] if deserialization isn't possible + def decode(io) + code = io.read(1) + return nil unless code && is_valid?(code) + self.type = TYPE_CODES[code] + + self.name = Utf.decode(io) + return nil if name.nil? + + if is_object? + self.field_type = decode_field_type(io) + return nil if field_type.nil? + end + + self + end + + # Serializes the Java::Serialization::Model::Field + # + # @return [String] if serialization is possible + # @return [nil] if serialization isn't possible + def encode + unless is_type_valid? + return nil + end + + encoded = '' + encoded << TYPE_CODES.key(type) + encoded << name.encode + + if is_object? + encoded << encode_field_type + end + + encoded + end + + # Whether the field type is valid. + # + # @return [Boolean] + def is_type_valid? + if TYPE_CODES.values.include?(type) + return true + end + + false + end + + # Whether the field type is a primitive one. + # + # @return [Boolean] + def is_primitive? + if PRIMITIVE_TYPE_CODES.values.include?(type) + return true + end + + false + end + + # Whether the field type is an object one. + # + # @return [Boolean] + def is_object? + if OBJECT_TYPE_CODES.values.include?(type) + return true + end + + false + end + + private + + # Whether the type opcode is a valid one. + # + # @param code [String] A type opcode + # @return [Boolean] + def is_valid?(code) + if TYPE_CODES.keys.include?(code) + return true + end + + false + end + + # Serializes the `field_type` attribute. + # + # @return [String] + def encode_field_type + encoded = [Java::Serialization::TC_STRING].pack('C') + encoded << field_type.encode + + encoded + end + + # Unserializes the `field_type` value. + # + # @param io [IO] the io to read from + # @return [Java::Serialization::Model::Utf] + def decode_field_type(io) + opcode = io.read(1) + return nil unless opcode && opcode == [Java::Serialization::TC_STRING].pack('C') + type = Utf.decode(io) + + type + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/java/serialization/model/null_reference.rb b/lib/rex/java/serialization/model/null_reference.rb new file mode 100644 index 0000000000..ded373f116 --- /dev/null +++ b/lib/rex/java/serialization/model/null_reference.rb @@ -0,0 +1,10 @@ +module Rex + module Java + module Serialization + module Model + class NullReference < Element + end + end + end + end +end \ No newline at end of file diff --git a/lib/rex/java/serialization/model/utf.rb b/lib/rex/java/serialization/model/utf.rb new file mode 100644 index 0000000000..4ffba241f1 --- /dev/null +++ b/lib/rex/java/serialization/model/utf.rb @@ -0,0 +1,66 @@ +module Rex + module Java + module Serialization + module Model + # This class provides a Utf string representation + class Utf < Element + + # @!attribute length + # @return [Integer] the length of the string + attr_accessor :length + # @!attribute contents + # @return [String] the contents of the string + attr_accessor :contents + + # Userializes a Java::Serialization::Model::Utf + # + # @param io [IO] the io to read from + # @return [Java::Serialization::Model::Utf] if deserialization is possible + # @return [nil] if deserialization isn't possible + def self.decode(io) + elem = self.new + + elem.decode(io) + end + + # @param contents [String] the contents of the utf string + def initialize(contents = '') + self.contents = contents + self.length = contents.length + end + + # Userializes a Java::Serialization::Model::Utf + # + # @param io [IO] the io to read from + # @return [self] if deserialization is possible + # @return [nil] if deserialization isn't possible + def decode(io) + raw_length = io.read(2) + return nil if raw_length.nil? + self.length = raw_length.unpack('n')[0] + + if length == 0 + self.contents = '' + else + self.contents = io.read(length) + return nil if contents.nil? || contents.length != length + end + + self + end + + # Serializes the Java::Serialization::Model::Utf + # + # @return [String] if serialization is possible + # @return [nil] if serialization isn't possible + def encode + encoded = [length].pack('n') + encoded << contents + + encoded + end + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/rex/java/serialization/model/field_spec.rb b/spec/lib/rex/java/serialization/model/field_spec.rb new file mode 100644 index 0000000000..62cea048a6 --- /dev/null +++ b/spec/lib/rex/java/serialization/model/field_spec.rb @@ -0,0 +1,132 @@ +require 'rex/java' +require 'stringio' + +describe Rex::Java::Serialization::Model::Field do + subject(:field) do + described_class.new + end + + let(:sample_primitive) { "I\x00\x06number" } + let(:sample_primitive_io) { StringIO.new(sample_primitive) } + let(:sample_object) { "[\x00\x0atest_arrayt\x00\x0b[LEmployee;" } + let(:sample_object_io) { StringIO.new(sample_object) } + + describe ".new" do + it "Rex::Java::Serialization::Model::Field" do + expect(field).to be_a(Rex::Java::Serialization::Model::Field) + end + + it "initializes code with empty string" do + expect(field.type).to be_empty + end + + it "initializes name with nil" do + expect(field.name).to be_nil + end + + it "initializes field_type with nil" do + expect(field.field_type).to be_nil + end + end + + describe "#encode" do + context "when empty field" do + it { expect(field.encode).to be_nil } + end + + context "when primitive field" do + it do + field.type = 'integer' + field.name = Rex::Java::Serialization::Model::Utf.new('number') + expect(field.encode).to eq(sample_primitive) + end + end + + context "when object field" do + it do + field.type = 'array' + field.name = Rex::Java::Serialization::Model::Utf.new('test_array') + field.field_type = Rex::Java::Serialization::Model::Utf.new('[LEmployee;') + expect(field.encode).to eq(sample_object) + end + end + end + + describe "#decode" do + context "when stream contains a primitive field" do + it "returns a Rex::Java::Serialization::Model::Field" do + expect(field.decode(sample_primitive_io)).to be_a(Rex::Java::Serialization::Model::Field) + end + + it "decodes field type" do + field.decode(sample_primitive_io) + expect(field.type).to eq('integer') + end + + it "decodes field name as Utf" do + field.decode(sample_primitive_io) + expect(field.name.contents).to eq('number') + end + end + + context "when stream contains an object field" do + it "returns a Rex::Java::Serialization::Model::Field" do + expect(field.decode(sample_object_io)).to be_a(Rex::Java::Serialization::Model::Field) + end + + it "decodes field type" do + field.decode(sample_object_io) + expect(field.type).to eq('array') + end + + it "decodes field name" do + field.decode(sample_object_io) + expect(field.name.contents).to eq('test_array') + end + + it "decodes field_type string" do + field.decode(sample_object_io) + expect(field.field_type.contents).to eq('[LEmployee;') + end + end + end + + describe ".decode" do + context "when stream contains a primitive field" do + it "returns a Rex::Java::Serialization::Model::Field" do + expect(described_class.decode(sample_primitive_io)).to be_a(Rex::Java::Serialization::Model::Field) + end + + it "decodes field type" do + field = described_class.decode(sample_primitive_io) + expect(field.type).to eq('integer') + end + + it "decodes field name as Utf" do + field = described_class.decode(sample_primitive_io) + expect(field.name.contents).to eq('number') + end + end + + context "when stream contains an object field" do + it "returns a Rex::Java::Serialization::Model::Field" do + expect(described_class.decode(sample_object_io)).to be_a(Rex::Java::Serialization::Model::Field) + end + + it "decodes field type" do + field = described_class.decode(sample_object_io) + expect(field.type).to eq('array') + end + + it "decodes field name" do + field = described_class.decode(sample_object_io) + expect(field.name.contents).to eq('test_array') + end + + it "decodes field_type string" do + field = described_class.decode(sample_object_io) + expect(field.field_type.contents).to eq('[LEmployee;') + end + end + end +end \ No newline at end of file diff --git a/spec/lib/rex/java/serialization/model/utf_spec.rb b/spec/lib/rex/java/serialization/model/utf_spec.rb new file mode 100644 index 0000000000..b2b38737a7 --- /dev/null +++ b/spec/lib/rex/java/serialization/model/utf_spec.rb @@ -0,0 +1,137 @@ +require 'rex/java' +require 'stringio' + +describe Rex::Java::Serialization::Model::Utf do + subject(:utf) do + described_class.new + end + + let(:sample_utf) { "\x00\x10java.lang.Number" } + let(:sample_utf_io) { StringIO.new(sample_utf) } + let(:empty_utf) { "\x00\x00" } + let(:empty_utf_io) { StringIO.new(empty_utf) } + let(:incomplete_utf) { "\x00\x10java.lang.Numb" } + let(:incomplete_utf_io) { StringIO.new(incomplete_utf) } + let(:empty_io) { StringIO.new('') } + + describe ".new" do + it "Rex::Java::Serialization::Model::Utf" do + expect(utf).to be_a(Rex::Java::Serialization::Model::Utf) + end + + it "initializes length to 0" do + expect(utf.length).to eq(0) + end + + it "initializes contents with empty string" do + expect(utf.contents).to be_empty + end + end + + describe "#encode" do + context "when empty utf" do + it { expect(utf.encode).to eq(empty_utf) } + end + + context "when filled utf" do + it do + utf.length = 16 + utf.contents = 'java.lang.Number' + expect(utf.encode).to eq(sample_utf) + end + end + end + + describe "#decode" do + context "when stream contains empty string" do + it "returns nil" do + expect(utf.decode(empty_io)).to be_nil + end + end + + context "when stream contains empty utf" do + it "returns a Rex::Java::Serialization::Model::Utf" do + expect(utf.decode(empty_utf_io)).to be_a(Rex::Java::Serialization::Model::Utf) + end + + it "sets length to 0" do + utf.decode(empty_utf_io) + expect(utf.length).to eq(0) + end + + it "sets contents to empty string" do + utf.decode(empty_utf_io) + expect(utf.contents).to be_empty + end + end + + context "when stream contains incomplete utf" do + it "returns nil" do + expect(utf.decode(incomplete_utf_io)).to be_nil + end + end + + context "when stream contains correct utf" do + + it "returns a Rex::Java::Serialization::Model::Utf" do + expect(utf.decode(sample_utf_io)).to be_a(Rex::Java::Serialization::Model::Utf) + end + + it "sets length to 0" do + utf.decode(sample_utf_io) + expect(utf.length).to eq(16) + end + + it "sets contents to sample string" do + utf.decode(sample_utf_io) + expect(utf.contents).to eq('java.lang.Number') + end + end + end + + describe ".decode" do + context "when stream contains empty string" do + it "returns nil" do + expect(described_class.decode(empty_io)).to be_nil + end + end + + context "when stream contains empty utf" do + it "returns a Rex::Java::Serialization::Model::Utf" do + expect(described_class.decode(empty_utf_io)).to be_a(Rex::Java::Serialization::Model::Utf) + end + + it "sets length to 0" do + utf = described_class.decode(empty_utf_io) + expect(utf.length).to eq(0) + end + + it "sets contents to empty string" do + utf = described_class.decode(empty_utf_io) + expect(utf.contents).to be_empty + end + end + + context "when stream contains incomplete utf" do + it "returns nil" do + expect(described_class.decode(incomplete_utf_io)).to be_nil + end + end + + context "when stream contains correct utf" do + it "returns a Rex::Java::Serialization::Model::Utf" do + expect(described_class.decode(sample_utf_io)).to be_a(Rex::Java::Serialization::Model::Utf) + end + + it "sets length to 0" do + utf = described_class.decode(sample_utf_io) + expect(utf.length).to eq(16) + end + + it "sets contents to sample string" do + utf = described_class.decode(sample_utf_io) + expect(utf.contents).to eq('java.lang.Number') + end + end + end +end \ No newline at end of file