582 lines
17 KiB
Ruby
582 lines
17 KiB
Ruby
# -*- coding: binary -*-
|
|
module Msf
|
|
|
|
###
|
|
#
|
|
# This module provides methods for reporting data to the DB
|
|
#
|
|
###
|
|
|
|
module Auxiliary::Report
|
|
extend Metasploit::Framework::Require
|
|
|
|
optionally_include_metasploit_credential_creation
|
|
|
|
def db_warning_given?
|
|
if @warning_issued
|
|
true
|
|
else
|
|
@warning_issued = true
|
|
false
|
|
end
|
|
end
|
|
|
|
def create_cracked_credential(opts={})
|
|
if active_db?
|
|
opts = { :task_id => mytask.id }.merge(opts) if mytask
|
|
framework.db.create_cracked_credential(opts)
|
|
elsif !db_warning_given?
|
|
vprint_warning('No active DB -- Credential data will not be saved!')
|
|
end
|
|
end
|
|
|
|
def create_credential(opts={})
|
|
if active_db?
|
|
opts = { :task_id => mytask.id }.merge(opts) if mytask
|
|
framework.db.create_credential(opts)
|
|
elsif !db_warning_given?
|
|
vprint_warning('No active DB -- Credential data will not be saved!')
|
|
end
|
|
end
|
|
|
|
def create_credential_login(opts={})
|
|
if active_db?
|
|
opts = { :task_id => mytask.id }.merge(opts) if mytask
|
|
framework.db.create_credential_login(opts)
|
|
elsif !db_warning_given?
|
|
vprint_warning('No active DB -- Credential data will not be saved!')
|
|
end
|
|
end
|
|
|
|
def create_credential_and_login(opts={})
|
|
if active_db?
|
|
opts = { :task_id => mytask.id }.merge(opts) if mytask
|
|
framework.db.create_credential_and_login(opts)
|
|
elsif !db_warning_given?
|
|
vprint_warning('No active DB -- Credential data will not be saved!')
|
|
end
|
|
end
|
|
|
|
def invalidate_login(opts={})
|
|
if active_db?
|
|
opts = { :task_id => mytask.id }.merge(opts) if mytask
|
|
framework.db.invalidate_login(opts)
|
|
elsif !db_warning_given?
|
|
vprint_warning('No active DB -- Credential data will not be saved!')
|
|
end
|
|
end
|
|
|
|
# This method overrides the method from Metasploit::Credential to check for an active db
|
|
def active_db?
|
|
framework.db.active
|
|
end
|
|
|
|
# Shortcut method for detecting when the DB is active
|
|
def db
|
|
framework.db.active
|
|
end
|
|
|
|
def myworkspace
|
|
@myworkspace = framework.db.find_workspace(self.workspace)
|
|
end
|
|
|
|
# This method safely get the workspace ID. It handles if the db is not active
|
|
#
|
|
# @return [NilClass] if there is no DB connection
|
|
# @return [Integer] the ID of the current Mdm::Workspace
|
|
def myworkspace_id
|
|
if framework.db.active
|
|
myworkspace.id
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def mytask
|
|
if self.respond_to?(:[]) && self[:task]
|
|
return self[:task].record
|
|
elsif @task && @task.class == Mdm::Task
|
|
return @task
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def inside_workspace_boundary?(ip)
|
|
return true if not framework.db.active
|
|
allowed = myworkspace.allow_actions_on?(ip)
|
|
return allowed
|
|
end
|
|
|
|
|
|
|
|
#
|
|
# Report a host's liveness and attributes such as operating system and service pack
|
|
#
|
|
# opts must contain :host, which is an IP address identifying the host
|
|
# you're reporting about
|
|
#
|
|
# See data/sql/*.sql and lib/msf/core/db.rb for more info
|
|
#
|
|
def report_host(opts)
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_host(opts)
|
|
end
|
|
|
|
def get_host(opts)
|
|
return if not db
|
|
opts = {:workspace => myworkspace}.merge(opts)
|
|
framework.db.get_host(opts)
|
|
end
|
|
|
|
#
|
|
# Report a client connection
|
|
# @param opts [Hash] report client information based on user-agent
|
|
# @option opts [String] :host the address of the client connecting
|
|
# @option opts [String] :ua_string a string that uniquely identifies this client
|
|
# @option opts [String] :ua_name a brief identifier for the client, e.g. "Firefox"
|
|
# @option opts [String] :ua_ver the version number of the client, e.g. "3.0.11"
|
|
#
|
|
def report_client(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_client(opts)
|
|
end
|
|
|
|
def get_client(opts={})
|
|
return if not db
|
|
opts = {:workspace => myworkspace}.merge(opts)
|
|
framework.db.get_client(opts)
|
|
end
|
|
|
|
#
|
|
# Report detection of a service
|
|
#
|
|
def report_service(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_service(opts)
|
|
end
|
|
|
|
def report_note(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_note(opts)
|
|
end
|
|
|
|
# This Legacy method is responsible for creating credentials from data supplied
|
|
# by a module. This method is deprecated and the new Metasploit::Credential methods
|
|
# should be used directly instead.
|
|
#
|
|
# @param opts [Hash] the option hash
|
|
# @option opts [String] :host the address of the host (also takes a Mdm::Host)
|
|
# @option opts [Integer] :port the port of the connected service
|
|
# @option opts [Mdm::Service] :service an optional Service object to build the cred for
|
|
# @option opts [String] :type What type of private credential this is (e.g. "password", "hash", "ssh_key")
|
|
# @option opts [String] :proto Which transport protocol the service uses
|
|
# @option opts [String] :sname The 'name' of the service
|
|
# @option opts [String] :user The username for the cred
|
|
# @option opts [String] :pass The private part of the credential (e.g. password)
|
|
def report_auth_info(opts={})
|
|
print_warning("*** #{self.fullname} is still calling the deprecated report_auth_info method! This needs to be updated!")
|
|
print_warning('*** For detailed information about LoginScanners and the Credentials objects see:')
|
|
print_warning(' https://github.com/rapid7/metasploit-framework/wiki/Creating-Metasploit-Framework-LoginScanners')
|
|
print_warning(' https://github.com/rapid7/metasploit-framework/wiki/How-to-write-a-HTTP-LoginScanner-Module')
|
|
print_warning('*** For examples of modules converted to just report credentials without report_auth_info, see:')
|
|
print_warning(' https://github.com/rapid7/metasploit-framework/pull/5376')
|
|
print_warning(' https://github.com/rapid7/metasploit-framework/pull/5377')
|
|
return unless db
|
|
raise ArgumentError.new("Missing required option :host") if opts[:host].nil?
|
|
raise ArgumentError.new("Missing required option :port") if (opts[:port].nil? and opts[:service].nil?)
|
|
|
|
if opts[:host].kind_of?(::Mdm::Host)
|
|
host = opts[:host].address
|
|
else
|
|
host = opts[:host]
|
|
end
|
|
|
|
type = :password
|
|
case opts[:type]
|
|
when "password"
|
|
type = :password
|
|
when "hash"
|
|
type = :nonreplayable_hash
|
|
when "ssh_key"
|
|
type = :ssh_key
|
|
end
|
|
|
|
case opts[:proto]
|
|
when "tcp"
|
|
proto = "tcp"
|
|
when "udp"
|
|
proto = "udp"
|
|
else
|
|
proto = "tcp"
|
|
end
|
|
|
|
if opts[:service] && opts[:service].kind_of?(Mdm::Service)
|
|
port = opts[:service].port
|
|
proto = opts[:service].proto
|
|
service_name = opts[:service].name
|
|
host = opts[:service].host.address
|
|
else
|
|
port = opts.fetch(:port)
|
|
service_name = opts.fetch(:sname, nil)
|
|
end
|
|
|
|
username = opts.fetch(:user, nil)
|
|
private = opts.fetch(:pass, nil)
|
|
|
|
service_data = {
|
|
address: host,
|
|
port: port,
|
|
service_name: service_name,
|
|
protocol: proto,
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
if self.type == "post"
|
|
credential_data = {
|
|
origin_type: :session,
|
|
session_id: session_db_id,
|
|
post_reference_name: self.refname
|
|
}
|
|
else
|
|
credential_data = {
|
|
origin_type: :service,
|
|
module_fullname: self.fullname
|
|
}
|
|
credential_data.merge!(service_data)
|
|
end
|
|
|
|
unless private.nil?
|
|
credential_data[:private_type] = type
|
|
credential_data[:private_data] = private
|
|
end
|
|
|
|
unless username.nil?
|
|
credential_data[:username] = username
|
|
end
|
|
|
|
credential_core = create_credential(credential_data)
|
|
|
|
login_data ={
|
|
core: credential_core,
|
|
status: Metasploit::Model::Login::Status::UNTRIED
|
|
}
|
|
login_data.merge!(service_data)
|
|
create_credential_login(login_data)
|
|
end
|
|
|
|
def report_vuln(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
vuln = framework.db.report_vuln(opts)
|
|
|
|
# add vuln attempt audit details here during report
|
|
|
|
timestamp = opts[:timestamp]
|
|
username = opts[:username]
|
|
mname = self.fullname # use module name when reporting attempt for correlation
|
|
|
|
# report_vuln is only called in an identified case, consider setting value reported here
|
|
attempt_info = {
|
|
:vuln_id => vuln.id,
|
|
:attempted_at => timestamp || Time.now.utc,
|
|
:exploited => false,
|
|
:fail_detail => 'vulnerability identified',
|
|
:fail_reason => 'Untried', # Mdm::VulnAttempt::Status::UNTRIED, avoiding direct dependency on Mdm, used elsewhere in this module
|
|
:module => mname,
|
|
:username => username || "unknown",
|
|
}
|
|
|
|
# TODO: figure out what opts are required and why the above logic doesn't match that of the db_manager method
|
|
framework.db.report_vuln_attempt(vuln, attempt_info)
|
|
|
|
vuln
|
|
end
|
|
|
|
# This will simply log a deprecation warning, since report_exploit()
|
|
# is no longer implemented.
|
|
def report_exploit(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_exploit(opts)
|
|
end
|
|
|
|
def report_loot(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_loot(opts)
|
|
end
|
|
|
|
def report_web_site(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_web_site(opts)
|
|
end
|
|
|
|
def report_web_page(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_web_page(opts)
|
|
end
|
|
|
|
def report_web_form(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_web_form(opts)
|
|
end
|
|
|
|
def report_web_vuln(opts={})
|
|
return if not db
|
|
opts = {
|
|
:workspace => myworkspace,
|
|
:task => mytask
|
|
}.merge(opts)
|
|
framework.db.report_web_vuln(opts)
|
|
end
|
|
|
|
#
|
|
# Store some data stolen from a session as a file
|
|
#
|
|
# Also stores metadata about the file in the database when available
|
|
# +ltype+ is an OID-style loot type, e.g. "cisco.ios.config". Ignored when
|
|
# no database is connected.
|
|
#
|
|
# +ctype+ is the Content-Type, e.g. "text/plain". Affects the extension
|
|
# the file will be saved with.
|
|
#
|
|
# +host+ can be an String address or a Session object
|
|
#
|
|
# +data+ is the actual contents of the file
|
|
#
|
|
# +filename+ and +info+ are only stored as metadata, and therefore both are
|
|
# ignored if there is no database
|
|
#
|
|
def store_loot(ltype, ctype, host, data, filename=nil, info=nil, service=nil)
|
|
if ! ::File.directory?(Msf::Config.loot_directory)
|
|
FileUtils.mkdir_p(Msf::Config.loot_directory)
|
|
end
|
|
|
|
ext = 'bin'
|
|
if filename
|
|
parts = filename.to_s.split('.')
|
|
if parts.length > 1 and parts[-1].length < 4
|
|
ext = parts[-1]
|
|
end
|
|
end
|
|
|
|
case ctype
|
|
when /^text\/[\w\.]+$/
|
|
ext = "txt"
|
|
end
|
|
# This method is available even if there is no database, don't bother checking
|
|
host = Msf::Util::Host.normalize_host(host)
|
|
|
|
ws = (db ? myworkspace.name[0,16] : 'default')
|
|
name =
|
|
Time.now.strftime("%Y%m%d%H%M%S") + "_" + ws + "_" +
|
|
(host || 'unknown') + '_' + ltype[0,16] + '_' +
|
|
Rex::Text.rand_text_numeric(6) + '.' + ext
|
|
|
|
name.gsub!(/[^a-z0-9\.\_]+/i, '')
|
|
|
|
path = File.join(Msf::Config.loot_directory, name)
|
|
full_path = ::File.expand_path(path)
|
|
File.open(full_path, "wb") do |fd|
|
|
fd.write(data)
|
|
end
|
|
|
|
if (db)
|
|
# If we have a database we need to store it with all the available
|
|
# metadata.
|
|
conf = {}
|
|
conf[:host] = host if host
|
|
conf[:type] = ltype
|
|
conf[:content_type] = ctype
|
|
conf[:path] = full_path
|
|
conf[:workspace] = myworkspace
|
|
conf[:name] = filename if filename
|
|
conf[:info] = info if info
|
|
conf[:data] = data if data
|
|
|
|
if service and service.kind_of?(::Mdm::Service)
|
|
conf[:service] = service if service
|
|
end
|
|
|
|
framework.db.report_loot(conf)
|
|
end
|
|
|
|
return full_path.dup
|
|
end
|
|
|
|
#
|
|
# Store some locally-generated data as a file, similiar to store_loot.
|
|
# Sometimes useful for keeping artifacts of an exploit or auxiliary
|
|
# module, such as files from fileformat exploits. (TODO: actually
|
|
# implement this on file format modules.)
|
|
#
|
|
# +filename+ is the local file name.
|
|
#
|
|
# +data+ is the actual contents of the file
|
|
#
|
|
# Also stores metadata about the file in the database when available.
|
|
# +ltype+ is an OID-style loot type, e.g. "cisco.ios.config". Ignored when
|
|
# no database is connected.
|
|
#
|
|
# +ctype+ is the Content-Type, e.g. "text/plain". Ignored when no database
|
|
# is connected.
|
|
#
|
|
def store_local(ltype=nil, ctype=nil, data=nil, filename=nil)
|
|
if ! ::File.directory?(Msf::Config.local_directory)
|
|
FileUtils.mkdir_p(Msf::Config.local_directory)
|
|
end
|
|
|
|
# Split by fname an extension
|
|
if filename and not filename.empty?
|
|
if filename =~ /(.*)\.(.*)/
|
|
ext = $2
|
|
fname = $1
|
|
else
|
|
fname = filename
|
|
end
|
|
else
|
|
fname = ctype || "local_#{Time.now.utc.to_i}"
|
|
end
|
|
|
|
# Split by path separator
|
|
fname = ::File.split(fname).last
|
|
|
|
case ctype # Probably could use more cases
|
|
when "text/plain"
|
|
ext ||= "txt"
|
|
when "text/xml"
|
|
ext ||= "xml"
|
|
when "text/html"
|
|
ext ||= "html"
|
|
when "application/pdf"
|
|
ext ||= "pdf"
|
|
else
|
|
ext ||= "bin"
|
|
end
|
|
|
|
fname.gsub!(/[^a-z0-9\.\_\-]+/i, '')
|
|
fname << ".#{ext}"
|
|
|
|
ltype.gsub!(/[^a-z0-9\.\_\-]+/i, '')
|
|
|
|
path = File.join(Msf::Config.local_directory, fname)
|
|
full_path = ::File.expand_path(path)
|
|
File.open(full_path, "wb") { |fd| fd.write(data) }
|
|
|
|
# This will probably evolve into a new database table
|
|
report_note(
|
|
:data => full_path.dup,
|
|
:type => "#{ltype}.localpath"
|
|
)
|
|
|
|
return full_path.dup
|
|
end
|
|
|
|
# Takes a credential from a script (shell or meterpreter), and
|
|
# sources it correctly to the originating user account or
|
|
# session. Note that the passed-in session ID should be the
|
|
# Session.local_id, which will be correlated with the Session.id
|
|
def store_cred(opts={})
|
|
if [opts[:port],opts[:sname]].compact.empty?
|
|
raise ArgumentError, "Missing option: :sname or :port"
|
|
end
|
|
cred_opts = opts
|
|
cred_opts = opts.merge(:workspace => myworkspace)
|
|
cred_opts = { :task_id => mytask.id }.merge(cred_opts) if mytask
|
|
cred_host = myworkspace.hosts.find_by_address(cred_opts[:host])
|
|
unless opts[:port]
|
|
possible_services = myworkspace.services.where(host_id: cred_host[:id], name: cred_opts[:sname])
|
|
case possible_services.size
|
|
when 0
|
|
case cred_opts[:sname].downcase
|
|
when "smb"
|
|
cred_opts[:port] = 445
|
|
when "ssh"
|
|
cred_opts[:port] = 22
|
|
when "telnet"
|
|
cred_opts[:port] = 23
|
|
when "snmp"
|
|
cred_opts[:port] = 161
|
|
cred_opts[:proto] = "udp"
|
|
else
|
|
raise ArgumentError, "No matching :sname found to store this cred."
|
|
end
|
|
when 1
|
|
cred_opts[:port] = possible_services.first[:port]
|
|
else # SMB should prefer 445. Everyone else, just take the first hit.
|
|
if (cred_opts[:sname].downcase == "smb") && possible_services.map {|x| x[:port]}.include?(445)
|
|
cred_opts[:port] = 445
|
|
elsif (cred_opts[:sname].downcase == "ssh") && possible_services.map {|x| x[:port]}.include?(22)
|
|
cred_opts[:port] = 22
|
|
else
|
|
cred_opts[:port] = possible_services.first[:port]
|
|
end
|
|
end
|
|
end
|
|
if opts[:collect_user]
|
|
cred_service = cred_host.services.find_by_host_id(cred_host[:id])
|
|
myworkspace.creds.sort {|a,b| a.created_at.to_f}.each do |cred|
|
|
if(cred.user.downcase == opts[:collect_user].downcase &&
|
|
cred.pass == opts[:collect_pass]
|
|
)
|
|
cred_opts[:source_id] ||= cred.id
|
|
cred_opts[:source_type] ||= cred_opts[:collect_type]
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if opts[:collect_session]
|
|
session = myworkspace.sessions.where(local_id: opts[:collect_session]).last
|
|
if !session.nil?
|
|
cred_opts[:source_id] = session.id
|
|
cred_opts[:source_type] = "exploit"
|
|
end
|
|
end
|
|
print_status "Collecting #{cred_opts[:user]}:#{cred_opts[:pass]}"
|
|
framework.db.report_auth_info(cred_opts)
|
|
end
|
|
|
|
|
|
|
|
end
|
|
end
|
|
|