lib/msf/core/db_manager/host.rb
module Msf::DBManager::Host
# TODO: doesn't appear to have any callers. How is this used?
# Deletes a host and associated data matching this address/comm
def del_host(wspace, address, comm='')
::ApplicationRecord.connection_pool.with_connection {
address, scope = address.split('%', 2)
host = wspace.hosts.find_by_address_and_comm(address, comm)
host.destroy if host
}
end
# Deletes Host entries based on the IDs passed in.
#
# @param opts[:ids] [Array] Array containing Integers corresponding to the IDs of the Host entries to delete.
# @return [Array] Array containing the Mdm::Host objects that were successfully deleted.
def delete_host(opts)
raise ArgumentError.new("The following options are required: :ids") if opts[:ids].nil?
::ApplicationRecord.connection_pool.with_connection {
deleted = []
opts[:ids].each do |host_id|
host = Mdm::Host.find(host_id)
begin
deleted << host.destroy
rescue # refs suck
elog("Forcibly deleting #{host.address}")
deleted << host.delete
end
end
return deleted
}
end
#
# Iterates over the hosts table calling the supplied block with the host
# instance of each entry.
#
def each_host(wspace=framework.db.workspace, &block)
::ApplicationRecord.connection_pool.with_connection {
wspace.hosts.each do |host|
block.call(host)
end
}
end
# Exactly like report_host but waits for the database to create a host and returns it.
def find_or_create_host(opts)
host = get_host(opts.clone)
return host unless host.nil?
report_host(opts)
end
def find_host_by_address_or_id(opts, wspace)
# Find the host entry in the current workspace by searching on
# the ID if available. If this isn't possible then try search on
# the IP address of the host we are currently processing, then save the database
# entry into the "host" variable.
if opts[:id]
host = wspace.hosts.find(opts[:id])
elsif opts[:address]
host = wspace.hosts.find_by_address(opts[:address])
else
raise ::ArgumentError, 'opts hash did not contain an :id or :address entry!'
end
host
end
def add_host_tag(opts)
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
tag_name = opts[:tag_name] # This will be the string of the tag that we are using.
host = find_host_by_address_or_id(opts, wspace)
# If a host was found
if host
# Set host_id to the ID of the host entry in the database that was found.
host_id = host[:id]
# Then proceed to go ahead and find potential tags that might have been already
# created that match the one we are trying to add.
possible_tags = Mdm::Tag.joins(:hosts).where("hosts.workspace_id = ? and hosts.id = ? and tags.name = ?", wspace.id, host_id, tag_name).order("tags.id DESC").limit(1)
# If one exists, then use it, otherwise create a new Mdm::Tag, and update
# the data in the database if the entry was found to need updating (aka the tag
# hasn't already been applied).
# @type [Mdm::Tag]
tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first)
tag.name = tag_name
tag.hosts = [host]
tag.save! if tag.changed?
tag
end
end
#@todo This will have to be pulled out if tags are used for more than just hosts
# ATM it will delete the tag from the tag table, not the host<->tag link
def delete_host_tag(opts)
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
tag_name = opts[:tag_name]
tag_ids = []
# If the command line included an address or address range then use this.
# Otherwise delete all entries that match the given tag.
host = find_host_by_address_or_id(opts, wspace)
if host
found_tags = Mdm::Tag.joins(:hosts).where("hosts.workspace_id = ? and hosts.id = ? and tags.name = ?", wspace.id, host.id, tag_name)
found_tags.each do |t|
tag_ids << t.id
end
deleted_tags = []
tag_ids.each do |id|
tag = Mdm::Tag.find_by_id(id)
deleted_tags << tag
tag.destroy
end
return deleted_tags
end
end
def get_host_tags(opts)
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
host_id = opts[:id]
host = wspace.hosts.find(host_id)
if host
host.tags
end
end
#
# Find a host. Performs no database writes.
#
def get_host(opts)
if opts.kind_of? ::Mdm::Host
return opts
elsif opts.kind_of? String
raise RuntimeError, "This invocation of get_host is no longer supported: #{caller}"
else
address = opts[:addr] || opts[:address] || opts[:host] || return
return address if address.kind_of? ::Mdm::Host
end
::ApplicationRecord.connection_pool.with_connection {
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
address = Msf::Util::Host.normalize_host(address)
return wspace.hosts.find_by_address(address)
}
end
# Returns a list of all hosts in the database
def hosts(opts)
::ApplicationRecord.connection_pool.with_connection {
# If we have the ID, there is no point in creating a complex query.
if opts[:id] && !opts[:id].to_s.empty?
return Array.wrap(Mdm::Host.find(opts[:id]))
end
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
conditions = {}
conditions[:state] = [Msf::HostState::Alive, Msf::HostState::Unknown] if opts[:non_dead]
conditions[:address] = opts[:address] if opts[:address] && !opts[:address].empty?
if opts[:search_term] && !opts[:search_term].empty?
column_search_conditions = Msf::Util::DBManager.create_all_column_search_conditions(Mdm::Host, opts[:search_term])
tag_conditions = Arel::Nodes::Regexp.new(Mdm::Tag.arel_table[:name], Arel::Nodes.build_quoted("(?mi)#{opts[:search_term]}"))
search_conditions = column_search_conditions.or(tag_conditions)
wspace.hosts.where(conditions).where(search_conditions).includes(:tags).references(:tags).order(:address)
else
wspace.hosts.where(conditions).order(:address)
end
}
end
def host_state_changed(host, ostate)
begin
framework.events.on_db_host_state(host, ostate)
rescue ::Exception => e
wlog("Exception in on_db_host_state event handler: #{e.class}: #{e}")
wlog("Call Stack\n#{e.backtrace.join("\n")}")
end
end
#
# Report a host's attributes such as operating system and service pack
#
# The opts parameter MUST contain
# +:host+:: -- the host's ip address
#
# The opts parameter can contain:
# +:state+:: -- one of the Msf::HostState constants
# +:os_name+:: -- something like "Windows", "Linux", or "Mac OS X"
# +:os_flavor+:: -- something like "Enterprise", "Pro", or "Home"
# +:os_sp+:: -- something like "SP2"
# +:os_lang+:: -- something like "English", "French", or "en-US"
# +:arch+:: -- one of the ARCHITECTURES listed in metasploit_data_models/app/models/mdm/host.rb
# +:mac+:: -- the host's MAC address
# +:scope+:: -- interface identifier for link-local IPv6
# +:virtual_host+:: -- the name of the virtualization software, eg "VMWare", "QEMU", "Xen", "Docker", etc.
#
def report_host(opts)
return if !active
addr = opts.delete(:host) || return
# Sometimes a host setup through a pivot will see the address as "Remote Pipe"
if addr.eql? "Remote Pipe"
return
end
::ApplicationRecord.connection_pool.with_connection {
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework)
opts = opts.clone
opts.delete(:workspace)
begin
retry_attempts ||= 0
if !addr.kind_of? ::Mdm::Host
original_addr = addr
addr = Msf::Util::Host.normalize_host(original_addr)
unless ipv46_validator(addr)
raise ::ArgumentError, "Invalid IP address in report_host(): #{original_addr}"
end
conditions = {address: addr}
conditions[:comm] = opts[:comm] if !opts[:comm].nil? && opts[:comm].length > 0
host = wspace.hosts.where(conditions).first_or_initialize
else
host = addr
end
ostate = host.state
# Truncate the info field at the maximum field length
if opts[:info]
opts[:info] = opts[:info][0,65535]
end
# Truncate the name field at the maximum field length
if opts[:name]
opts[:name] = opts[:name][0,255]
end
opts.each do |k,v|
if host.attribute_names.include?(k.to_s)
unless host.attribute_locked?(k.to_s)
host[k] = v.to_s.gsub(/[\x00-\x1f]/n, '')
end
elsif !v.blank?
dlog("Unknown attribute for ::Mdm::Host: #{k}")
end
end
host.info = host.info[0,::Mdm::Host.columns_hash["info"].limit] if host.info
# Set default fields if needed
host.state = Msf::HostState::Alive if host.state.nil? || host.state.empty?
host.comm = '' unless host.comm
host.workspace = wspace unless host.workspace
begin
framework.events.on_db_host(host) if host.new_record?
rescue => e
wlog("Exception in on_db_host event handler: #{e.class}: #{e}")
wlog("Call Stack\n#{e.backtrace.join("\n")}")
end
host_state_changed(host, ostate) if host.state != ostate
if host.changed?
msf_import_timestamps(opts, host)
host.save!
end
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
# two concurrent report requests for a new host could result in a RecordNotUnique or
# RecordInvalid exception, simply retry the report once more as an optimistic approach
retry if (retry_attempts+=1) <= 1
raise
end
if opts[:task]
Mdm::TaskHost.create(
:task => opts[:task],
:host => host
)
end
host
}
end
def update_host(opts)
::ApplicationRecord.connection_pool.with_connection {
# process workspace string for update if included in opts
wspace = Msf::Util::DBManager.process_opts_workspace(opts, framework, false)
opts = opts.clone()
opts[:workspace] = wspace if wspace
id = opts.delete(:id)
host = Mdm::Host.find(id)
host.update!(opts)
return host
}
end
end