Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/tools/exploit/virustotal.rb
Views: 11767
#!/usr/bin/env ruby12##3# This module requires Metasploit: https://metasploit.com/download4# Current source: https://github.com/rapid7/metasploit-framework5##67#8# This script will check multiple files against VirusTotal's public analysis service. You are9# limited to at most 4 requests (of any nature in any given 1 minute time frame), because10# VirusTotal says so. If you prefer your own API key, you may get one at virustotal.com11#12# VirusTotal Terms of Service:13# https://www.virustotal.com/en/about/terms-of-service/14#15# Public API documentations can be found here:16# https://www.virustotal.com/en/documentation/public-api/17# https://api.vtapi.net/en/doc/18#19# WARNING:20# When you upload or otherwise submit content, you give VirusTotal (and those we work with) a21# worldwide, royalty free, irrevocable and transferable licence to use, edit, host, store,22# reproduce, modify, create derivative works, communicate, publish, publicly perform, publicly23# display and distribute such content.24#25# Author:26# sinn3r <sinn3r[at]metasploit.com>27#28begin29msfbase = __FILE__30while File.symlink?(msfbase)31msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))32end3334$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))35require 'msfenv'36require 'rex'37require 'digest/sha2'38require 'optparse'39require 'json'40require 'timeout'4142#43# Prints a status message44#45def print_status(msg='')46$stdout.puts "[*] #{msg}"47end484950#51# Prints an error message52#53def print_error(msg='')54$stdout.puts "[-] #{msg}"55end565758module VirusTotalUtility5960class ToolConfig6162def initialize63@config_file ||= Msf::Config.config_file64@group_name ||= 'VirusTotal'65end6667#68# Saves the VirusTotal API key to Metasploit's config file69# @param key [String] API key70# @return [void]71#72def save_api_key(key)73_set_setting('api_key', key)74end757677#78# Returns the VirusTotal API key from Metasploit's config file79# @return [String] the API key80#81def load_api_key82_get_setting('api_key') || ''83end848586#87# Sets the privacy waiver to true after the tool is run for the very first time88# @return [void]89#90def save_privacy_waiver91_set_setting('waiver', true)92end939495#96# Returns whether a waver is set or not97# @return [Boolean]98#99def has_privacy_waiver?100_get_setting('waiver') || false101end102103104private105106107#108# Sets a setting in Metasploit's config file109# @param key_name [String] The Key to set110# @param value [String] The value to set111# @return [void]112#113def _set_setting(key_name, value)114ini = Rex::Parser::Ini.new(@config_file)115ini.add_group(@group_name) if ini[@group_name].nil?116ini[@group_name][key_name] = value117ini.to_file(@config_file)118end119120121#122# Returns a setting from Metasploit's config file123# @param key_name [String] The setting to get124# @return [void]125#126def _get_setting(key_name)127ini = Rex::Parser::Ini.new(@config_file)128group = ini[@group_name]129return nil if group.nil?130return nil if group[key_name].nil?131132group[key_name]133end134135end136137138class VirusTotal < Msf::Auxiliary139140include Msf::Exploit::Remote::HttpClient141142def initialize(opts={})143@api_key = opts['api_key']144@sample_info = _load_sample(opts['sample'])145146# It should resolve to 74.125.34.46, and the HOST header (HTTP) must be www.virustotal.com, or147# it will return a 404 instead.148rhost = Rex::Socket.resolv_to_dotted("www.virustotal.com") rescue '74.125.34.46'149150# Need to configure HttpClient to enable SSL communication151super(152'DefaultOptions' =>153{154'SSL' => true,155'RHOST' => rhost,156'RPORT' => 443157}158)159end160161162#163# Submits a malware sample for VirusTotal to scan164# @param sample [String] Data to analyze165# @return [Hash] JSON response166#167def scan_sample168opts = {169'boundary' => 'THEREAREMANYLIKEITBUTTHISISMYDATA',170'api_key' => @api_key,171'filename' => @sample_info['filename'],172'data' => @sample_info['data']173}174175_execute_request({176'uri' => '/vtapi/v2/file/scan',177'method' => 'POST',178'vhost' => 'www.virustotal.com',179'ctype' => "multipart/form-data; boundary=#{opts['boundary']}",180'data' => _create_upload_data(opts)181})182end183184185#186# Returns the report of a specific malware hash187# @return [Hash] JSON response188#189def retrieve_report190_execute_request({191'uri' => '/vtapi/v2/file/report',192'method' => 'POST',193'vhost' => 'www.virustotal.com',194'vars_post' => {195'apikey' => @api_key,196'resource' => @sample_info['sha256']197}198})199end200201private202203#204# Returns the JSON response of a HTTP request205# @param opts [Hash] HTTP options206# @return [Hash] JSON response207#208def _execute_request(opts)209res = send_request_cgi(opts)210211return '' if res.nil?212case res.code213when 204214raise RuntimeError, "You have hit the request limit."215when 403216raise RuntimeError, "No privilege to execute this request probably due to an invalye API key"217end218219json_body = ''220221begin222json_body = JSON.parse(res.body)223rescue JSON::ParserError224json_body = ''225end226227json_body228end229230#231# Returns malware sample information232# @param sample [String] The sample path to load233# @return [Hash] Information about the sample (including the raw data, and SHA256 hash)234#235def _load_sample(sample)236info = {237'filename' => '',238'data' => ''239}240241File.open(sample, 'rb') do |f|242info['data'] = f.read243end244245info['filename'] = File.basename(sample)246info['sha256'] = Digest::SHA256.hexdigest(info['data'])247248info249end250251252#253# Creates a form-data message254# @param opts [Hash] A hash that contains keys including boundary, api_key, filename, and data255# @return [String] The POST request data256#257def _create_upload_data(opts={})258boundary = opts['boundary']259api_key = opts['api_key']260filename = opts['filename']261data = opts['data']262263# Can't use Rex::MIME::Message, or you WILL be increditably outraged, it messes with your data.264# See VT report for example: 4212686e701286ab734d8a67b7b7527f279c2dadc27bd744abebecab91b70c82265data = %Q|--#{boundary}266Content-Disposition: form-data; name="apikey"267268#{api_key}269--#{boundary}270Content-Disposition: form-data; name="file"; filename="#{filename}"271Content-Type: application/octet-stream272273#{data}274--#{boundary}--275|276277data278end279280end281282class OptsConsole283#284# Return a hash describing the options.285#286def self.parse(args)287options = {}288289opts = OptionParser.new do |opts|290opts.banner = "Usage: #{__FILE__} [options]"291292opts.separator ""293opts.separator "Specific options:"294295opts.on("-k", "-k <key>", "(Optional) Virusl API key to use") do |v|296options['api_key'] = v297end298299opts.on("-d", "-d <seconds>", "(Optional) Number of seconds to wait for the report") do |v|300if v !~ /^\d+$/301print_error("Invalid input for -d. It must be a number.")302exit303end304305options['delay'] = v.to_i306end307308opts.on("-q", nil, "(Optional) Do a hash search without uploading the sample") do |v|309options['quick'] = true310end311312opts.on("-f", "-f <filenames>", "Files to scan") do |v|313files = v.split.delete_if { |e| e.nil? }314bad_files = []315files.each do |f|316unless ::File.exist?(f)317bad_files << f318end319end320321unless bad_files.empty?322print_error("Cannot find: #{bad_files * ' '}")323exit324end325326if files.length > 4327print_error("Sorry, I can only allow 4 files at a time.")328exit329end330331options['samples'] = files332end333334opts.separator ""335opts.separator "Common options:"336337opts.on_tail("-h", "--help", "Show this message") do338puts opts339exit340end341end342343# Set default344if options['samples'].nil?345options['samples'] = []346end347348if options['quick'].nil?349options['quick'] = false350end351352if options['delay'].nil?353options['delay'] = 60354end355356if options['api_key'].nil?357# Default key is from Metasploit, see why this key can be shared:358# http://blog.virustotal.com/2012/12/public-api-request-rate-limits-and-tool.html359options['api_key'] = '501caf66349cc7357eb4398ac3298fdd03dec01a3e2f3ad576525aa7b57a1987'360end361362begin363opts.parse!(args)364rescue OptionParser::InvalidOption365print_error("Invalid option, try -h for usage")366exit367end368369if options.empty?370print_error("No options specified, try -h for usage")371exit372end373374options375end376end377378class Driver379380attr_reader :opts381382def initialize383opts = {}384385# Init arguments386options = OptsConsole.parse(ARGV)387388# Init config manager389config = ToolConfig.new390391# User must ack for research privacy before using this tool392unless config.has_privacy_waiver?393ack_privacy394config.save_privacy_waiver395end396397# Set the API key398config.save_api_key(options['api_key']) unless options['api_key'].blank?399api_key = config.load_api_key400if api_key.blank?401print_status("No API key found, using the default one. You may set it later with -k.")402exit403else404print_status("Using API key: #{api_key}")405opts['api_key'] = api_key406end407408@opts = opts.merge(options)409end410411412#413# Prompts the user about research privacy. They will not be able to get out until they enter 'Y'414# @return [Boolean] True if ack415#416def ack_privacy417print_status "WARNING: When you upload or otherwise submit content, you give VirusTotal"418print_status "(and those we work with) a worldwide, royalty free, irrevocable and transferable"419print_status "licence to use, edit, host, store, reproduce, modify, create derivative works,"420print_status "communicate, publish, publicly perform, publicly display and distribute such"421print_status "content. To read the complete Terms of Service for VirusTotal, please go to the"422print_status "following link:"423print_status "https://www.virustotal.com/en/about/terms-of-service/"424print_status425print_status "If you prefer your own API key, you may obtain one at VirusTotal."426427while true428$stdout.print "[*] Enter 'Y' to acknowledge: "429if $stdin.gets =~ /^y|yes$/i430return true431end432end433end434435436#437# Retrieves a report from VirusTotal438# @param vt [VirusTotal] VirusTotal object439# @param res [Hash] Last submission response440# @param delay [Integer] Delay441# @return [Hash] VirusTotal response that contains the report442#443def wait_report(vt, res, delay)444sha256 = res['sha256']445print_status("Requesting the report...")446res = nil447448# 3600 seconds = 1 hour449begin450::Timeout.timeout(3600) {451while true452res = vt.retrieve_report453break if res['response_code'] == 1454select(nil, nil, nil, delay)455print_status("Received code #{res['response_code']}. Waiting for another #{delay.to_s} seconds...")456end457}458rescue ::Timeout::Error459print_error("No report collected. Please manually check the analysis link later.")460return nil461end462463res464end465466467#468# Shows the scan report469# @param res [Hash] VirusTotal response470# @param sample [String] Malware name471# @return [void]472#473def generate_report(res, sample)474if res['response_code'] != 1475print_status("VirusTotal: #{res['verbose_msg']}")476return477end478479short_filename = File.basename(sample)480tbl = Rex::Text::Table.new(481'Header' => "Analysis Report: #{short_filename} (#{res['positives']} / #{res['total']}): #{res['sha256']}",482'Indent' => 1,483'Columns' => ['Antivirus', 'Detected', 'Version', 'Result', 'Update']484)485486(res['scans'] || []).each do |result|487product = result[0]488detected = result[1]['detected'].to_s489version = result[1]['version'] || ''490sig_name = result[1]['result'] || ''491timestamp = result[1]['update'] || ''492493tbl << [product, detected, version, sig_name, timestamp]494end495496print_status tbl.to_s497end498499500#501# Displays hashes502#503def show_hashes(res)504print_status("Sample MD5 hash : #{res['md5']}") if res['md5']505print_status("Sample SHA1 hash : #{res['sha1']}") if res['sha1']506print_status("Sample SHA256 hash : #{res['sha256']}") if res['sha256']507print_status("Analysis link: #{res['permalink']}") if res['permalink']508end509510511#512# Executes a scan by uploading a sample and produces a report513#514def scan_by_upload515@opts['samples'].each do |sample|516vt = VirusTotal.new({'api_key' => @opts['api_key'], 'sample' => sample})517print_status("Please wait while I upload #{sample}...")518res = vt.scan_sample519print_status("VirusTotal: #{res['verbose_msg']}")520show_hashes(res)521res = wait_report(vt, res, @opts['delay'])522generate_report(res, sample) if res523524puts525end526end527528529#530# Executes a hash search and produces a report531#532def scan_by_hash533@opts['samples'].each do |sample|534vt = VirusTotal.new({'api_key' => @opts['api_key'], 'sample' => sample})535print_status("Please wait I look for a report for #{sample}...")536res = vt.retrieve_report537show_hashes(res)538generate_report(res, sample) if res539540puts541end542end543544end545546end # VirusTotalUtility547548549#550# main551#552if __FILE__ == $PROGRAM_NAME553begin554driver = VirusTotalUtility::Driver.new555if driver.opts['quick']556driver.scan_by_hash557else558driver.scan_by_upload559end560rescue Interrupt561$stdout.puts562$stdout.puts "Good bye"563end564end565rescue SignalException => e566puts("Aborted! #{e}")567end568569570