lib/msf/core/modules/metadata/search.rb
# -*- coding: binary -*-
#
# Provides search operations on the module metadata cache.
#
module Msf::Modules::Metadata::Search
VALID_PARAMS =
%w[
action
adapter
aka
arch
author
authors
bid
check
cve
date
description
disclosure_date
edb
fullname
mod_time
name
os
path
platform
port
rank
ref
ref_name
reference
references
rport
session_type
stage
stager
target
targets
text
type
]
#
# Module Type Shorthands
#
MODULE_TYPE_SHORTHANDS = {
"aux" => Msf::MODULE_AUX
}
module SearchMode
INCLUDE = 0
EXCLUDE = 1
end
#
# Parses command line search string into a hash. A param prefixed with '-' indicates "not", and will omit results
# matching that keyword. This hash can be used with the find command.
#
# Resulting Hash Example:
# {"platform"=>[["android"], []]} will match modules targeting the android platform
# {"platform"=>[[], ["android"]]} will exclude modules targeting the android platform
#
def self.parse_search_string(search_string)
search_string ||= ''
search_string += ' '
# Split search terms by space, but allow quoted strings
terms = search_string.split(/\"/).collect{|term| term.strip==term ? term : term.split(' ')}.flatten
terms.delete('')
# All terms are either included or excluded
res = {}
terms.each do |term|
# Split it on the `:`, with the part before the first `:` going into keyword, the part after first `:`
# but before any later instances of `:` going into search_term, and the characters after the second
# `:` or later in the string going into _excess to be ignored.
#
# Example is `use exploit/linux/local/nested_namespace_idmap_limit_priv_esc::a`
# which would make keyword become `exploit/linux/local/nested_namespace_idmap_limit_priv_esc`,
# search_term become blank, and _excess become "a".
keyword, search_term, _excess = term.split(":", 3)
if search_term.blank?
search_term = keyword
keyword = 'text'
end
next if search_term.length == 0
keyword.downcase!
search_term.downcase!
if keyword == "type"
search_term = MODULE_TYPE_SHORTHANDS[search_term] if MODULE_TYPE_SHORTHANDS.key?(search_term)
end
res[keyword] ||=[ [], [] ]
if search_term[0,1] == "-"
next if search_term.length == 1
res[keyword][SearchMode::EXCLUDE] << search_term[1,search_term.length-1]
else
res[keyword][SearchMode::INCLUDE] << search_term
end
end
res
end
#
# Searches the module metadata using the passed hash of search params
#
def find(params, fields={})
raise ArgumentError if params.any? && VALID_PARAMS.none? { |k| params.key?(k) }
search_results = []
get_metadata.each { |module_metadata|
if is_match(params, module_metadata)
unless fields.empty?
module_metadata = get_fields(module_metadata, fields)
end
search_results << module_metadata
end
}
return search_results
end
#######
private
#######
def is_match(params, module_metadata)
return true if params.empty?
param_hash = params
[SearchMode::INCLUDE, SearchMode::EXCLUDE].each do |mode|
match = false
param_hash.keys.each do |keyword|
next if param_hash[keyword][mode].length == 0
# free form text search will honor 'and' semantics, i.e. 'metasploit pro' will only match modules that contain both
# words, and will return false when only one word is matched
if keyword == 'text'
module_actions = (module_metadata.actions || []).flat_map { |action| action.values.map(&:to_s) }
text_segments = [module_metadata.name, module_metadata.fullname, module_metadata.description] + module_metadata.references + module_metadata.author + (module_metadata.notes['AKA'] || []) + module_actions
if module_metadata.targets
text_segments = text_segments + module_metadata.targets
end
param_hash[keyword][mode].each do |search_term|
has_match = text_segments.any? { |text_segment| text_segment =~ as_regex(search_term) }
match = [keyword, search_term] if has_match
if mode == SearchMode::INCLUDE && !has_match
return false
end
if mode == SearchMode::EXCLUDE && has_match
return false
end
end
next
end
# The remaining keywords honor 'or' semantics, i.e. the following param_hash will match either osx, or linux
# {"platform"=>[["osx", "linux"], []]}
param_hash[keyword][mode].each do |search_term|
# Reset the match flag for each keyword for inclusive search
match = false if mode == SearchMode::INCLUDE
regex = as_regex(search_term)
case keyword
when 'action'
match = [keyword, search_term] if (module_metadata&.actions || []).any? { |action| action.any? { |k, v| k =~ regex || v =~ regex } }
when 'aka'
match = [keyword, search_term] if (module_metadata.notes['AKA'] || []).any? { |aka| aka =~ regex }
when 'author', 'authors'
match = [keyword, search_term] if module_metadata.author.any? { |author| author =~ regex }
when 'arch'
match = [keyword, search_term] if module_metadata.arch =~ regex
when 'cve'
match = [keyword, search_term] if module_metadata.references.any? { |ref| ref =~ /^cve\-/i and ref =~ regex }
when 'bid'
match = [keyword, search_term] if module_metadata.references.any? { |ref| ref =~ /^bid\-/i and ref =~ regex }
when 'edb'
match = [keyword, search_term] if module_metadata.references.any? { |ref| ref =~ /^edb\-/i and ref =~ regex }
when 'check'
if module_metadata.check
matches_check = %w(true yes).any? { |val| val =~ regex}
else
matches_check = %w(false no).any? { |val| val =~ regex}
end
match = [keyword, search_term] if matches_check
when 'date', 'disclosure_date'
match = [keyword, search_term] if module_metadata.disclosure_date.to_s =~ regex
when 'description'
match = [keyword, search_term] if module_metadata.description =~ regex
when 'fullname'
match = [keyword, search_term] if module_metadata.fullname =~ regex
when 'mod_time'
match = [keyword, search_term] if module_metadata.mod_time.to_s =~ regex
when 'name'
match = [keyword, search_term] if module_metadata.name =~ regex
when 'os', 'platform'
match = [keyword, search_term] if module_metadata.platform =~ regex or module_metadata.arch =~ regex
if module_metadata.targets
match = [keyword, search_term] if module_metadata.targets.any? { |target| target =~ regex }
end
when 'path'
match = [keyword, search_term] if module_metadata.fullname =~ regex
when 'stage'
match = [keyword, search_term] if module_metadata.stage_refname =~ regex
when 'stager'
match = [keyword, search_term] if module_metadata.stager_refname =~ regex
when 'adapter'
match = [keyword, search_term] if module_metadata.adapter_refname =~ regex
when 'session_type'
match = [keyword, search_term] if module_metadata.session_types && module_metadata.session_types.any? { |session_type| session_type =~ regex }
when 'port', 'rport'
match = [keyword, search_term] if module_metadata.rport.to_s =~ regex
when 'rank'
# Determine if param was prepended with gt, lt, gte, lte, or eq
# Ex: "lte300" should match all ranks <= 300
query_rank = search_term.dup
operator = query_rank[0,3].tr('0-9', '')
valid_operators = %w[eq gt lt gte lte]
matches_rank = module_metadata.rank == search_term.to_i
if valid_operators.include? operator
query_rank.slice! operator
query_rank = query_rank.to_i
case operator
when 'gt'
matches_rank = module_metadata.rank.to_i > query_rank
when 'lt'
matches_rank = module_metadata.rank.to_i < query_rank
when 'gte'
matches_rank = module_metadata.rank.to_i >= query_rank
when 'lte'
matches_rank = module_metadata.rank.to_i <= query_rank
when 'eq'
matches_rank = module_metadata.rank.to_i == query_rank
end
elsif query_rank =~ /^\d+$/
matches_rank = module_metadata.rank.to_i == query_rank.to_i
else
matches_rank = module_metadata.rank.to_i == Msf::RankingName.key(query_rank)
end
match = [keyword, search_term] if matches_rank
when 'ref', 'ref_name'
match = [keyword, search_term] if module_metadata.ref_name =~ regex
when 'reference', 'references'
match = [keyword, search_term] if module_metadata.references.any? { |ref| ref =~ regex }
when 'target', 'targets'
match = [keyword, search_term] if module_metadata.targets.any? { |target| target =~ regex }
when 'type'
match = [keyword, search_term] if Msf::MODULE_TYPES.any? { |module_type| search_term == module_type and module_metadata.type == module_type }
else
# Ignore extraneous/invalid keywords
match = [keyword, search_term]
end
break if match
end
# Filter this module if no matches for a given keyword type
if mode == SearchMode::INCLUDE and not match
return false
end
end
# Filter this module if we matched an exclusion keyword (-value)
if mode == SearchMode::EXCLUDE and match
return false
end
end
true
end
def as_regex(search_term)
# Convert into a case-insensitive regex
utf8_buf = search_term.dup.force_encoding('UTF-8')
if utf8_buf.valid_encoding?
Regexp.new(Regexp.escape(utf8_buf), Regexp::IGNORECASE)
else
# If the encoding is invalid, default to a regex that matches anything
//
end
end
def get_fields(module_metadata, fields)
selected_fields = {}
aliases = {
:cve => 'references',
:edb => 'references',
:bid => 'references',
:os => 'platform',
:port => 'rport',
:reference => 'references',
:ref => 'ref_name',
:target => 'targets',
:authors => 'author'
}
fields.each do | field |
field = aliases[field.to_sym] if aliases[field.to_sym]
if module_metadata.respond_to?(field)
selected_fields[field] = module_metadata.send(field)
end
end
selected_fields
end
end