123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569 |
- #!/usr/bin/env ruby
- ##
- # This module requires Metasploit: https://metasploit.com/download
- # Current source: https://github.com/rapid7/metasploit-framework
- ##
- #
- # This script will check multiple files against VirusTotal's public analysis service. You are
- # limited to at most 4 requests (of any nature in any given 1 minute time frame), because
- # VirusTotal says so. If you prefer your own API key, you may get one at virustotal.com
- #
- # VirusTotal Terms of Service:
- # https://www.virustotal.com/en/about/terms-of-service/
- #
- # Public API documentations can be found here:
- # https://www.virustotal.com/en/documentation/public-api/
- # https://api.vtapi.net/en/doc/
- #
- # WARNING:
- # When you upload or otherwise submit content, you give VirusTotal (and those we work with) a
- # worldwide, royalty free, irrevocable and transferable licence to use, edit, host, store,
- # reproduce, modify, create derivative works, communicate, publish, publicly perform, publicly
- # display and distribute such content.
- #
- # Author:
- # sinn3r <sinn3r[at]metasploit.com>
- #
- begin
- msfbase = __FILE__
- while File.symlink?(msfbase)
- msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
- end
- $:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))
- require 'msfenv'
- require 'rex'
- require 'digest/sha2'
- require 'optparse'
- require 'json'
- require 'timeout'
- #
- # Prints a status message
- #
- def print_status(msg='')
- $stdout.puts "[*] #{msg}"
- end
- #
- # Prints an error message
- #
- def print_error(msg='')
- $stdout.puts "[-] #{msg}"
- end
- module VirusTotalUtility
- class ToolConfig
- def initialize
- @config_file ||= Msf::Config.config_file
- @group_name ||= 'VirusTotal'
- end
- #
- # Saves the VirusTotal API key to Metasploit's config file
- # @param key [String] API key
- # @return [void]
- #
- def save_api_key(key)
- _set_setting('api_key', key)
- end
- #
- # Returns the VirusTotal API key from Metasploit's config file
- # @return [String] the API key
- #
- def load_api_key
- _get_setting('api_key') || ''
- end
- #
- # Sets the privacy waiver to true after the tool is run for the very first time
- # @return [void]
- #
- def save_privacy_waiver
- _set_setting('waiver', true)
- end
- #
- # Returns whether a waver is set or not
- # @return [Boolean]
- #
- def has_privacy_waiver?
- _get_setting('waiver') || false
- end
- private
- #
- # Sets a setting in Metasploit's config file
- # @param key_name [String] The Key to set
- # @param value [String] The value to set
- # @return [void]
- #
- def _set_setting(key_name, value)
- ini = Rex::Parser::Ini.new(@config_file)
- ini.add_group(@group_name) if ini[@group_name].nil?
- ini[@group_name][key_name] = value
- ini.to_file(@config_file)
- end
- #
- # Returns a setting from Metasploit's config file
- # @param key_name [String] The setting to get
- # @return [void]
- #
- def _get_setting(key_name)
- ini = Rex::Parser::Ini.new(@config_file)
- group = ini[@group_name]
- return nil if group.nil?
- return nil if group[key_name].nil?
- group[key_name]
- end
- end
- class VirusTotal < Msf::Auxiliary
- include Msf::Exploit::Remote::HttpClient
- def initialize(opts={})
- @api_key = opts['api_key']
- @sample_info = _load_sample(opts['sample'])
- # It should resolve to 74.125.34.46, and the HOST header (HTTP) must be www.virustotal.com, or
- # it will return a 404 instead.
- rhost = Rex::Socket.resolv_to_dotted("www.virustotal.com") rescue '74.125.34.46'
- # Need to configure HttpClient to enable SSL communication
- super(
- 'DefaultOptions' =>
- {
- 'SSL' => true,
- 'RHOST' => rhost,
- 'RPORT' => 443
- }
- )
- end
- #
- # Submits a malware sample for VirusTotal to scan
- # @param sample [String] Data to analyze
- # @return [Hash] JSON response
- #
- def scan_sample
- opts = {
- 'boundary' => 'THEREAREMANYLIKEITBUTTHISISMYDATA',
- 'api_key' => @api_key,
- 'filename' => @sample_info['filename'],
- 'data' => @sample_info['data']
- }
- _execute_request({
- 'uri' => '/vtapi/v2/file/scan',
- 'method' => 'POST',
- 'vhost' => 'www.virustotal.com',
- 'ctype' => "multipart/form-data; boundary=#{opts['boundary']}",
- 'data' => _create_upload_data(opts)
- })
- end
- #
- # Returns the report of a specific malware hash
- # @return [Hash] JSON response
- #
- def retrieve_report
- _execute_request({
- 'uri' => '/vtapi/v2/file/report',
- 'method' => 'POST',
- 'vhost' => 'www.virustotal.com',
- 'vars_post' => {
- 'apikey' => @api_key,
- 'resource' => @sample_info['sha256']
- }
- })
- end
- private
- #
- # Returns the JSON response of a HTTP request
- # @param opts [Hash] HTTP options
- # @return [Hash] JSON response
- #
- def _execute_request(opts)
- res = send_request_cgi(opts)
- return '' if res.nil?
- case res.code
- when 204
- raise RuntimeError, "You have hit the request limit."
- when 403
- raise RuntimeError, "No privilege to execute this request probably due to an invalye API key"
- end
- json_body = ''
- begin
- json_body = JSON.parse(res.body)
- rescue JSON::ParserError
- json_body = ''
- end
- json_body
- end
- #
- # Returns malware sample information
- # @param sample [String] The sample path to load
- # @return [Hash] Information about the sample (including the raw data, and SHA256 hash)
- #
- def _load_sample(sample)
- info = {
- 'filename' => '',
- 'data' => ''
- }
- File.open(sample, 'rb') do |f|
- info['data'] = f.read
- end
- info['filename'] = File.basename(sample)
- info['sha256'] = Digest::SHA256.hexdigest(info['data'])
- info
- end
- #
- # Creates a form-data message
- # @param opts [Hash] A hash that contains keys including boundary, api_key, filename, and data
- # @return [String] The POST request data
- #
- def _create_upload_data(opts={})
- boundary = opts['boundary']
- api_key = opts['api_key']
- filename = opts['filename']
- data = opts['data']
- # Can't use Rex::MIME::Message, or you WILL be increditably outraged, it messes with your data.
- # See VT report for example: 4212686e701286ab734d8a67b7b7527f279c2dadc27bd744abebecab91b70c82
- data = %Q|--#{boundary}
- Content-Disposition: form-data; name="apikey"
- #{api_key}
- --#{boundary}
- Content-Disposition: form-data; name="file"; filename="#{filename}"
- Content-Type: application/octet-stream
- #{data}
- --#{boundary}--
- |
- data
- end
- end
- class OptsConsole
- #
- # Return a hash describing the options.
- #
- def self.parse(args)
- options = {}
- opts = OptionParser.new do |opts|
- opts.banner = "Usage: #{__FILE__} [options]"
- opts.separator ""
- opts.separator "Specific options:"
- opts.on("-k", "-k <key>", "(Optional) Virusl API key to use") do |v|
- options['api_key'] = v
- end
- opts.on("-d", "-d <seconds>", "(Optional) Number of seconds to wait for the report") do |v|
- if v !~ /^\d+$/
- print_error("Invalid input for -d. It must be a number.")
- exit
- end
- options['delay'] = v.to_i
- end
- opts.on("-q", nil, "(Optional) Do a hash search without uploading the sample") do |v|
- options['quick'] = true
- end
- opts.on("-f", "-f <filenames>", "Files to scan") do |v|
- files = v.split.delete_if { |e| e.nil? }
- bad_files = []
- files.each do |f|
- unless ::File.exist?(f)
- bad_files << f
- end
- end
- unless bad_files.empty?
- print_error("Cannot find: #{bad_files * ' '}")
- exit
- end
- if files.length > 4
- print_error("Sorry, I can only allow 4 files at a time.")
- exit
- end
- options['samples'] = files
- end
- opts.separator ""
- opts.separator "Common options:"
- opts.on_tail("-h", "--help", "Show this message") do
- puts opts
- exit
- end
- end
- # Set default
- if options['samples'].nil?
- options['samples'] = []
- end
- if options['quick'].nil?
- options['quick'] = false
- end
- if options['delay'].nil?
- options['delay'] = 60
- end
- if options['api_key'].nil?
- # Default key is from Metasploit, see why this key can be shared:
- # http://blog.virustotal.com/2012/12/public-api-request-rate-limits-and-tool.html
- options['api_key'] = '501caf66349cc7357eb4398ac3298fdd03dec01a3e2f3ad576525aa7b57a1987'
- end
- begin
- opts.parse!(args)
- rescue OptionParser::InvalidOption
- print_error("Invalid option, try -h for usage")
- exit
- end
- if options.empty?
- print_error("No options specified, try -h for usage")
- exit
- end
- options
- end
- end
- class Driver
- attr_reader :opts
- def initialize
- opts = {}
- # Init arguments
- options = OptsConsole.parse(ARGV)
- # Init config manager
- config = ToolConfig.new
- # User must ack for research privacy before using this tool
- unless config.has_privacy_waiver?
- ack_privacy
- config.save_privacy_waiver
- end
- # Set the API key
- config.save_api_key(options['api_key']) unless options['api_key'].blank?
- api_key = config.load_api_key
- if api_key.blank?
- print_status("No API key found, using the default one. You may set it later with -k.")
- exit
- else
- print_status("Using API key: #{api_key}")
- opts['api_key'] = api_key
- end
- @opts = opts.merge(options)
- end
- #
- # Prompts the user about research privacy. They will not be able to get out until they enter 'Y'
- # @return [Boolean] True if ack
- #
- def ack_privacy
- print_status "WARNING: When you upload or otherwise submit content, you give VirusTotal"
- print_status "(and those we work with) a worldwide, royalty free, irrevocable and transferable"
- print_status "licence to use, edit, host, store, reproduce, modify, create derivative works,"
- print_status "communicate, publish, publicly perform, publicly display and distribute such"
- print_status "content. To read the complete Terms of Service for VirusTotal, please go to the"
- print_status "following link:"
- print_status "https://www.virustotal.com/en/about/terms-of-service/"
- print_status
- print_status "If you prefer your own API key, you may obtain one at VirusTotal."
- while true
- $stdout.print "[*] Enter 'Y' to acknowledge: "
- if $stdin.gets =~ /^y|yes$/i
- return true
- end
- end
- end
- #
- # Retrieves a report from VirusTotal
- # @param vt [VirusTotal] VirusTotal object
- # @param res [Hash] Last submission response
- # @param delay [Integer] Delay
- # @return [Hash] VirusTotal response that contains the report
- #
- def wait_report(vt, res, delay)
- sha256 = res['sha256']
- print_status("Requesting the report...")
- res = nil
- # 3600 seconds = 1 hour
- begin
- ::Timeout.timeout(3600) {
- while true
- res = vt.retrieve_report
- break if res['response_code'] == 1
- select(nil, nil, nil, delay)
- print_status("Received code #{res['response_code']}. Waiting for another #{delay.to_s} seconds...")
- end
- }
- rescue ::Timeout::Error
- print_error("No report collected. Please manually check the analysis link later.")
- return nil
- end
- res
- end
- #
- # Shows the scan report
- # @param res [Hash] VirusTotal response
- # @param sample [String] Malware name
- # @return [void]
- #
- def generate_report(res, sample)
- if res['response_code'] != 1
- print_status("VirusTotal: #{res['verbose_msg']}")
- return
- end
- short_filename = File.basename(sample)
- tbl = Rex::Text::Table.new(
- 'Header' => "Analysis Report: #{short_filename} (#{res['positives']} / #{res['total']}): #{res['sha256']}",
- 'Indent' => 1,
- 'Columns' => ['Antivirus', 'Detected', 'Version', 'Result', 'Update']
- )
- (res['scans'] || []).each do |result|
- product = result[0]
- detected = result[1]['detected'].to_s
- version = result[1]['version'] || ''
- sig_name = result[1]['result'] || ''
- timestamp = result[1]['update'] || ''
- tbl << [product, detected, version, sig_name, timestamp]
- end
- print_status tbl.to_s
- end
- #
- # Displays hashes
- #
- def show_hashes(res)
- print_status("Sample MD5 hash : #{res['md5']}") if res['md5']
- print_status("Sample SHA1 hash : #{res['sha1']}") if res['sha1']
- print_status("Sample SHA256 hash : #{res['sha256']}") if res['sha256']
- print_status("Analysis link: #{res['permalink']}") if res['permalink']
- end
- #
- # Executes a scan by uploading a sample and produces a report
- #
- def scan_by_upload
- @opts['samples'].each do |sample|
- vt = VirusTotal.new({'api_key' => @opts['api_key'], 'sample' => sample})
- print_status("Please wait while I upload #{sample}...")
- res = vt.scan_sample
- print_status("VirusTotal: #{res['verbose_msg']}")
- show_hashes(res)
- res = wait_report(vt, res, @opts['delay'])
- generate_report(res, sample) if res
- puts
- end
- end
- #
- # Executes a hash search and produces a report
- #
- def scan_by_hash
- @opts['samples'].each do |sample|
- vt = VirusTotal.new({'api_key' => @opts['api_key'], 'sample' => sample})
- print_status("Please wait I look for a report for #{sample}...")
- res = vt.retrieve_report
- show_hashes(res)
- generate_report(res, sample) if res
- puts
- end
- end
- end
- end # VirusTotalUtility
- #
- # main
- #
- if __FILE__ == $PROGRAM_NAME
- begin
- driver = VirusTotalUtility::Driver.new
- if driver.opts['quick']
- driver.scan_by_hash
- else
- driver.scan_by_upload
- end
- rescue Interrupt
- $stdout.puts
- $stdout.puts "Good bye"
- end
- end
- rescue SignalException => e
- puts("Aborted! #{e}")
- end
|