Land #2851, @wchen-r7's virustotal integration
commit
bd91e36e06
|
@ -0,0 +1,129 @@
|
|||
##
|
||||
# This module requires Metasploit: http//metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
class Metasploit3 < Msf::Post
|
||||
|
||||
include Msf::Post::File
|
||||
|
||||
def initialize(info={})
|
||||
super( update_info( info,
|
||||
'Name' => 'Multi Gather Malware Verifier',
|
||||
'Description' => %q{
|
||||
This module will check a file for malware on VirusTotal based on the checksum.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'sinn3r'],
|
||||
'Platform' => [ 'osx', 'win', 'linux' ],
|
||||
'SessionTypes' => [ "shell", "meterpreter" ]
|
||||
))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('APIKEY', [true, "VirusTotal API key", '501caf66349cc7357eb4398ac3298fdd03dec01a3e2f3ad576525aa7b57a1987']),
|
||||
OptString.new('REMOTEFILE', [true, "A file to check from the remote machine"])
|
||||
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def rhost
|
||||
session.session_host
|
||||
end
|
||||
|
||||
def get_report(api_key, checksum)
|
||||
#
|
||||
# We have to use Net::HTTP instead of HttpClient because of the following error:
|
||||
# The supplied module name is ambiguous: undefined method `register_autofilter_ports'
|
||||
#
|
||||
url = URI.parse("https://www.virustotal.com/vtapi/v2/file/report")
|
||||
req = Net::HTTP::Post.new(url.path, initheader={'Host'=>'www.virustotal.com'})
|
||||
req.set_form_data({'apikey'=>api_key, 'resource'=>checksum})
|
||||
http = Net::HTTP::new(url.host, url.port)
|
||||
http.use_ssl = true
|
||||
res = http.start {|http| http.request(req)}
|
||||
|
||||
unless res
|
||||
print_error("#{rhost} - Connection timed out")
|
||||
return ''
|
||||
end
|
||||
|
||||
case res.code
|
||||
when 204
|
||||
print_error("#{rhost} - You have reached the request limit, please wait for one minute to try again")
|
||||
return ''
|
||||
when 403
|
||||
print_error("#{rhost} - No privilege to execute this request probably due to an invalye API key")
|
||||
return ''
|
||||
end
|
||||
|
||||
body = ''
|
||||
begin
|
||||
body = JSON.parse(res.body)
|
||||
rescue JSON::ParserError
|
||||
print_error("#{rhost} - Unable to parse the response")
|
||||
return body
|
||||
end
|
||||
|
||||
body
|
||||
end
|
||||
|
||||
def show_report(res, filename)
|
||||
md5 = res['md5'] || ''
|
||||
sha1 = res['sha1'] || ''
|
||||
sha256 = res['sha256'] || ''
|
||||
|
||||
print_status("#{rhost} - MD5: #{md5}") unless md5.blank?
|
||||
print_status("#{rhost} - SHA1: #{sha1}") unless sha1.blank?
|
||||
print_status("#{rhost} - SHA256: #{sha256}") unless sha256.blank?
|
||||
|
||||
tbl = Rex::Ui::Text::Table.new(
|
||||
'Header' => "Analysis Report: #{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
|
||||
|
||||
report_note({
|
||||
:host => session,
|
||||
:type => 'malware.sample',
|
||||
:data => tbl.to_csv
|
||||
})
|
||||
print_status tbl.to_s
|
||||
end
|
||||
|
||||
def run
|
||||
filename = datastore['REMOTEFILE']
|
||||
api_key = datastore['APIKEY']
|
||||
|
||||
unless file?(filename)
|
||||
print_error("#{rhost} - File not found: #{filename}")
|
||||
return
|
||||
end
|
||||
|
||||
checksum = file_remote_digestsha1(filename)
|
||||
print_status("#{rhost} - Checking: #{filename}...")
|
||||
report = get_report(api_key, checksum)
|
||||
|
||||
return if report.blank?
|
||||
|
||||
print_status("#{rhost} - VirusTotal message: #{report['verbose_msg']}")
|
||||
if report['response_code'] == 1
|
||||
show_report(report, File.basename(filename))
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,264 @@
|
|||
require 'spec_helper'
|
||||
|
||||
load Metasploit::Framework.root.join('tools/virustotal.rb').to_path
|
||||
|
||||
require 'fastlib'
|
||||
require 'msfenv'
|
||||
require 'msf/base'
|
||||
require 'digest/sha2'
|
||||
|
||||
describe "virustotal.rb" do
|
||||
|
||||
context "Classes" do
|
||||
let(:api_key) do
|
||||
'FAKE_API_KEY'
|
||||
end
|
||||
|
||||
let(:filename) do
|
||||
'MALWARE.EXE'
|
||||
end
|
||||
|
||||
let(:malware_data) do
|
||||
'DATA'
|
||||
end
|
||||
|
||||
describe ToolConfig do
|
||||
context "Class methods" do
|
||||
|
||||
let(:tool_config) do
|
||||
ToolConfig.new
|
||||
end
|
||||
|
||||
context ".Initializer" do
|
||||
it "should init the config file path as Metasploit's default config path" do
|
||||
tool_config.instance_variable_get(:@config_file).should eq(Msf::Config.config_file)
|
||||
end
|
||||
|
||||
it "should init the group name as 'VirusTotal'" do
|
||||
tool_config.instance_variable_get(:@group_name).should eq('VirusTotal')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe VirusTotal do
|
||||
context "Class methods" do
|
||||
|
||||
let(:malware_sha256) do
|
||||
Digest::SHA256.hexdigest(malware_data)
|
||||
end
|
||||
|
||||
let(:sample) do
|
||||
{
|
||||
'filename' => filename,
|
||||
'data' => malware_data,
|
||||
'sha256' => malware_sha256
|
||||
}
|
||||
end
|
||||
|
||||
let(:boundary) do
|
||||
'THEREAREMANYLIKEITBUTTHISISMYDATA'
|
||||
end
|
||||
|
||||
let(:scan_sample_opts) do
|
||||
opts = {
|
||||
'boundary' => boundary,
|
||||
'api_key' => api_key,
|
||||
'filename' => filename,
|
||||
'data' => malware_data
|
||||
}
|
||||
|
||||
return opts
|
||||
end
|
||||
|
||||
let(:retrieve_report_opts) do
|
||||
opts = {
|
||||
'uri' => '/vtapi/v2/file/report',
|
||||
'method' => 'POST',
|
||||
'vhost' => 'www.virustotal.com',
|
||||
'vars_post' => {
|
||||
'apikey' => api_key,
|
||||
'resource' => malware_sha256
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
end
|
||||
|
||||
let(:vt) do
|
||||
file = double(File, read: malware_data)
|
||||
File.stub(:open).with(filename, 'rb') {|&block| block.yield file}
|
||||
VirusTotal.new({'api_key'=>api_key, 'sample'=>filename})
|
||||
end
|
||||
|
||||
context ".Initializer" do
|
||||
it "should have an API key" do
|
||||
vt.instance_variable_get(:@api_key).should eq(api_key)
|
||||
end
|
||||
|
||||
it "should have a checksum for the malware sample" do
|
||||
vt.instance_variable_get(:@sample_info)['sha256'].should eq(malware_sha256)
|
||||
end
|
||||
end
|
||||
|
||||
context "._load_sample" do
|
||||
it "should contain sample info including data, filename, and sha256" do
|
||||
vt.send(:_load_sample, filename).should eq(sample)
|
||||
end
|
||||
end
|
||||
|
||||
context ".scan_sample" do
|
||||
it "should return with data" do
|
||||
vt.stub(:_execute_request).and_return('')
|
||||
vt.scan_sample.should eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context ".retrieve_report" do
|
||||
it "should return with data" do
|
||||
vt.stub(:_execute_request).and_return('')
|
||||
vt.retrieve_report.should eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context "._execute_request" do
|
||||
it "should return status code 204" do
|
||||
res = double(Rex::Proto::Http::Response)
|
||||
res.stub(:code).and_return(204)
|
||||
vt.stub(:send_request_cgi).with(scan_sample_opts).and_return(res)
|
||||
expect { vt.send(:_execute_request, scan_sample_opts) }.to raise_error(RuntimeError)
|
||||
end
|
||||
|
||||
it "should return status code 403" do
|
||||
res = double(Rex::Proto::Http::Response)
|
||||
res.stub(:code).and_return(403)
|
||||
vt.stub(:send_request_cgi).with(scan_sample_opts).and_return(res)
|
||||
expect { vt.send(:_execute_request, scan_sample_opts) }.to raise_error(RuntimeError)
|
||||
end
|
||||
end
|
||||
|
||||
context "._create_upload_data" do
|
||||
|
||||
let(:form_opts) do
|
||||
{
|
||||
'boundary' => boundary,
|
||||
'api_key' => api_key,
|
||||
'filename' => filename,
|
||||
'data' => malware_data
|
||||
}
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
@upload_data = vt.send(:_create_upload_data, form_opts)
|
||||
end
|
||||
|
||||
it "should create form-data with a boundary" do
|
||||
@upload_data.should match(/#{boundary}/)
|
||||
end
|
||||
|
||||
it "should create form-data with the API key" do
|
||||
@upload_data.should match(/#{api_key}/)
|
||||
end
|
||||
|
||||
it "should create form-data with the malware filename" do
|
||||
@upload_data.should match(/#{filename}/)
|
||||
end
|
||||
|
||||
it "should create form-data with the malware data" do
|
||||
@upload_data.should match(/#{malware_data}/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe Driver do
|
||||
# Get stdout:
|
||||
# http://stackoverflow.com/questions/11349270/test-output-to-command-line-with-rspec
|
||||
def get_stdout(&block)
|
||||
out = $stdout
|
||||
$stdout = fake = StringIO.new
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
$stdout = out
|
||||
end
|
||||
fake.string
|
||||
end
|
||||
|
||||
before do
|
||||
$stdin = StringIO.new("Y\n")
|
||||
end
|
||||
|
||||
after do
|
||||
$stdin = STDIN
|
||||
end
|
||||
|
||||
let(:driver) do
|
||||
argv = "-k #{api_key} -f #{filename}".split
|
||||
options = {
|
||||
'samples' => filename,
|
||||
'api_key' => api_key,
|
||||
'delay' => 60
|
||||
}
|
||||
|
||||
OptsConsole.stub(:parse).with(anything).and_return(options)
|
||||
|
||||
|
||||
tool_config = double("tool_config")
|
||||
ToolConfig.stub(:new).and_return(tool_config)
|
||||
tool_config.stub(:has_privacy_waiver?).and_return(true)
|
||||
tool_config.stub(:load_api_key).and_return(api_key)
|
||||
tool_config.stub(:save_privacy_waiver)
|
||||
tool_config.stub(:save_api_key).with(anything)
|
||||
|
||||
d = nil
|
||||
|
||||
out = get_stdout {
|
||||
d = Driver.new
|
||||
}
|
||||
|
||||
d
|
||||
end
|
||||
|
||||
context ".Class methods" do
|
||||
|
||||
context ".initialize" do
|
||||
it "should return a Driver object" do
|
||||
driver.class.should eq(Driver)
|
||||
end
|
||||
end
|
||||
|
||||
context ".ask_privacy" do
|
||||
it "should have a link of VirusTotal's terms of service" do
|
||||
tos = 'https://www.virustotal.com/en/about/terms-of-service'
|
||||
out = get_stdout { driver.ack_privacy }
|
||||
out.should match(/#{tos}/)
|
||||
end
|
||||
end
|
||||
|
||||
context ".upload_sample" do
|
||||
it "should upload a sample" do
|
||||
vt = double(VirusTotal)
|
||||
vt.stub(:scan_sample).and_return({})
|
||||
out = get_stdout { driver.upload_sample(vt, filename) }
|
||||
out.should match(/Please wait while I upload/)
|
||||
end
|
||||
end
|
||||
|
||||
context ".generate_report" do
|
||||
it "should show a report" do
|
||||
res = {
|
||||
"scans" => {
|
||||
"Bkav" => { "detected" => false, "version" => "1.3.0.4613", "result" => nil, "update" => "20140107" }
|
||||
}
|
||||
}
|
||||
|
||||
out = get_stdout { driver.generate_report(res, filename) }
|
||||
out.should match(/#{res['scans']['Bkav']['version']}/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,529 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
##
|
||||
# This module requires Metasploit: http//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>
|
||||
#
|
||||
|
||||
|
||||
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 'fastlib'
|
||||
require 'msfenv'
|
||||
require 'rex'
|
||||
require 'msf/core'
|
||||
require 'digest/sha2'
|
||||
require 'optparse'
|
||||
require 'json'
|
||||
require 'timeout'
|
||||
|
||||
|
||||
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 checksum
|
||||
# @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 checksum)
|
||||
#
|
||||
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 DriverBase
|
||||
#
|
||||
# Prints a status message
|
||||
#
|
||||
def print_status(msg='')
|
||||
$stdout.puts "[*] #{msg}"
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Prints an error message
|
||||
#
|
||||
def print_error(msg='')
|
||||
$stdout.puts "[-] #{msg}"
|
||||
end
|
||||
end
|
||||
|
||||
class OptsConsole < DriverBase
|
||||
#
|
||||
# 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", "-delay <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("-f", "-files <filenames>", "Files to scan") do |v|
|
||||
files = v.split.delete_if { |e| e.nil? }
|
||||
bad_files = []
|
||||
files.each do |f|
|
||||
unless ::File.exists?(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['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 < DriverBase
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
# Submits a malware sample to VirusTotal
|
||||
# @param vt [VirusTotal] VirusTotal object
|
||||
# @param sample [String] The malware sample name
|
||||
# @return [Hash] VirusTotal response of the upload
|
||||
#
|
||||
def upload_sample(vt, sample)
|
||||
print_status("Please wait while I upload #{sample}...")
|
||||
res = vt.scan_sample
|
||||
print_status("VirusTotal: #{res['verbose_msg']}")
|
||||
print_status("Sample MD5 checksum: #{res['md5']}")
|
||||
print_status("Sample SHA256 checksum: #{res['sha256']}")
|
||||
print_status("Analysis link: #{res['permalink']}")
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Retrieves a report from VirusTotal
|
||||
# @param vt [VirusTotal] VirusTotal object
|
||||
# @param res [Hash] Last submission response
|
||||
# @param delay [Fixnum] 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)
|
||||
tbl = Rex::Ui::Text::Table.new(
|
||||
'Header' => "Analysis Report: #{sample} (#{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
|
||||
|
||||
|
||||
#
|
||||
# Executes a scan and produces a report
|
||||
#
|
||||
def scan
|
||||
@opts['samples'].each do |sample|
|
||||
vt = VirusTotal.new({'api_key' => @opts['api_key'], 'sample' => sample})
|
||||
res = upload_sample(vt, sample)
|
||||
res = wait_report(vt, res, @opts['delay'])
|
||||
generate_report(res, sample) if res
|
||||
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# main
|
||||
#
|
||||
if __FILE__ == $PROGRAM_NAME
|
||||
begin
|
||||
driver = Driver.new
|
||||
driver.scan
|
||||
rescue Interrupt
|
||||
$stdout.puts
|
||||
$stdout.puts "Good bye"
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue