2313 lines
66 KiB
Ruby
2313 lines
66 KiB
Ruby
#
|
|
# Web assessment for the Metasploit Framework
|
|
# Efrain Torres - et[ ] metasploit.com 2012
|
|
#
|
|
|
|
require 'English'
|
|
require 'rabal/tree'
|
|
|
|
module Msf
|
|
class Plugin::Wmap < Msf::Plugin
|
|
class WmapCommandDispatcher
|
|
|
|
# @!attribute wmapmodules
|
|
# @return [Array] Enabled WMAP modules
|
|
# @!attribute targets
|
|
# @return [Hash] WMAP targets
|
|
# @!attribute lastsites
|
|
# @return [Array] Temp location of previously obtained sites
|
|
# @!attribute rpcarr
|
|
# @return [Array] Array or rpc connections
|
|
# @!attribute njobs
|
|
# @return [Integer] Max number of jobs
|
|
# @!attribute nmaxdisplay
|
|
# @return [Boolean] Flag to stop displaying the same message
|
|
# @!attribute runlocal
|
|
# @return [Boolean] Flag to run local modules only
|
|
# @!attribute masstop
|
|
# @return [Boolean] Flag to stop everything
|
|
# @!attribute killwhenstop
|
|
# @return [Boolean] Kill process when exiting
|
|
attr_accessor :wmapmodules, :targets, :lastsites, :rpcarr, :njobs, :nmaxdisplay, :runlocal, :masstop, :killwhenstop
|
|
|
|
include Msf::Ui::Console::CommandDispatcher
|
|
|
|
def name
|
|
'wmap'
|
|
end
|
|
|
|
#
|
|
# The initial command set
|
|
#
|
|
def commands
|
|
{
|
|
'wmap_targets' => 'Manage targets',
|
|
'wmap_sites' => 'Manage sites',
|
|
'wmap_nodes' => 'Manage nodes',
|
|
'wmap_run' => 'Test targets',
|
|
'wmap_modules' => 'Manage wmap modules',
|
|
'wmap_vulns' => 'Display web vulns'
|
|
}
|
|
end
|
|
|
|
def cmd_wmap_vulns(*args)
|
|
args.push('-h') if args.empty?
|
|
|
|
while (arg = args.shift)
|
|
case arg
|
|
when '-l'
|
|
view_vulns
|
|
when '-h'
|
|
print_status('Usage: wmap_vulns [options]')
|
|
print_line("\t-h Display this help text")
|
|
print_line("\t-l Display web vulns table")
|
|
|
|
print_line('')
|
|
else
|
|
print_error('Unknown flag.')
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
def cmd_wmap_modules(*args)
|
|
args.push('-h') if args.empty?
|
|
|
|
while (arg = args.shift)
|
|
case arg
|
|
when '-l'
|
|
view_modules
|
|
when '-r'
|
|
load_wmap_modules(true)
|
|
when '-h'
|
|
print_status('Usage: wmap_modules [options]')
|
|
print_line("\t-h Display this help text")
|
|
print_line("\t-l List all wmap enabled modules")
|
|
print_line("\t-r Reload wmap modules")
|
|
|
|
print_line('')
|
|
else
|
|
print_error('Unknown flag.')
|
|
end
|
|
return
|
|
end
|
|
end
|
|
|
|
def cmd_wmap_targets(*args)
|
|
args.push('-h') if args.empty?
|
|
|
|
while (arg = args.shift)
|
|
case arg
|
|
when '-c'
|
|
self.targets = Hash.new
|
|
when '-l'
|
|
view_targets
|
|
return
|
|
when '-t'
|
|
process_urls(args.shift)
|
|
when '-d'
|
|
process_ids(args.shift)
|
|
when '-h'
|
|
print_status('Usage: wmap_targets [options]')
|
|
print_line("\t-h Display this help text")
|
|
print_line("\t-t [urls] Define target sites (vhost1,url[space]vhost2,url) ")
|
|
print_line("\t-d [ids] Define target sites (id1, id2, id3 ...)")
|
|
print_line("\t-c Clean target sites list")
|
|
print_line("\t-l List all target sites")
|
|
|
|
print_line('')
|
|
return
|
|
else
|
|
print_error('Unknown flag.')
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
def cmd_wmap_sites(*args)
|
|
args.push('-h') if args.empty?
|
|
|
|
while (arg = args.shift)
|
|
case arg
|
|
when '-a'
|
|
site = args.shift
|
|
if site
|
|
s = add_web_site(site)
|
|
if s
|
|
print_status('Site created.')
|
|
else
|
|
print_error('Unable to create site')
|
|
end
|
|
else
|
|
print_error('No site provided.')
|
|
end
|
|
when '-d'
|
|
del_idx = args
|
|
if !del_idx.empty?
|
|
delete_sites(del_idx.select { |d| d =~ /^[0-9]*$/ }.map(&:to_i).uniq)
|
|
return
|
|
else
|
|
print_error('No index provided.')
|
|
end
|
|
when '-l'
|
|
view_sites
|
|
return
|
|
when '-s'
|
|
u = args.shift
|
|
l = args.shift
|
|
o = args.shift
|
|
|
|
return unless u
|
|
|
|
if l.nil? || l.empty?
|
|
l = 200
|
|
o = 'true'
|
|
elsif (l == 'true') || (l == 'false')
|
|
# Add check if unicode parameters is the second one
|
|
o = l
|
|
l = 200
|
|
else
|
|
l = l.to_i
|
|
end
|
|
|
|
o = (o == 'true')
|
|
|
|
if u.include? 'http'
|
|
# Parameters are in url form
|
|
view_site_tree(u, l, o)
|
|
else
|
|
# Parameters are digits
|
|
if !lastsites || lastsites.empty?
|
|
view_sites
|
|
print_status('Web sites ids. referenced from previous table.')
|
|
end
|
|
|
|
target_whitelist = []
|
|
ids = u.to_s.split(/,/)
|
|
|
|
ids.each do |id|
|
|
next if id.to_s.strip.empty?
|
|
|
|
if id.to_i > lastsites.length
|
|
print_error("Skipping id #{id}...")
|
|
else
|
|
target_whitelist << lastsites[id.to_i]
|
|
# print_status("Loading #{self.lastsites[id.to_i]}.")
|
|
end
|
|
end
|
|
|
|
# Skip the DB entirely if no matches
|
|
return if target_whitelist.empty?
|
|
|
|
unless targets
|
|
self.targets = Hash.new
|
|
end
|
|
|
|
target_whitelist.each do |ent|
|
|
view_site_tree(ent, l, o)
|
|
end
|
|
end
|
|
return
|
|
when '-h'
|
|
print_status('Usage: wmap_sites [options]')
|
|
print_line("\t-h Display this help text")
|
|
print_line("\t-a [url] Add site (vhost,url)")
|
|
print_line("\t-d [ids] Delete sites (separate ids with space)")
|
|
print_line("\t-l List all available sites")
|
|
print_line("\t-s [id] Display site structure (vhost,url|ids) (level) (unicode output true/false)")
|
|
print_line('')
|
|
return
|
|
else
|
|
print_error('Unknown flag.')
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
def cmd_wmap_nodes(*args)
|
|
if !rpcarr
|
|
self.rpcarr = Hash.new
|
|
end
|
|
|
|
args.push('-h') if args.empty?
|
|
|
|
while (arg = args.shift)
|
|
case arg
|
|
when '-a'
|
|
h = args.shift
|
|
r = args.shift
|
|
s = args.shift
|
|
u = args.shift
|
|
p = args.shift
|
|
|
|
res = rpc_add_node(h, r, s, u, p, false)
|
|
if res
|
|
print_status('Node created.')
|
|
else
|
|
print_error('Unable to create node')
|
|
end
|
|
when '-c'
|
|
idref = args.shift
|
|
|
|
if !idref
|
|
print_error('No id defined')
|
|
return
|
|
end
|
|
if idref.upcase == 'ALL'
|
|
print_status('All nodes removed')
|
|
self.rpcarr = Hash.new
|
|
else
|
|
idx = 0
|
|
rpcarr.each do |k, _v|
|
|
if idx == idref.to_i
|
|
rpcarr.delete(k)
|
|
print_status("Node deleted #{k}")
|
|
end
|
|
idx += 1
|
|
end
|
|
end
|
|
when '-d'
|
|
host = args.shift
|
|
port = args.shift
|
|
user = args.shift
|
|
pass = args.shift
|
|
dbname = args.shift
|
|
|
|
res = rpc_db_nodes(host, port, user, pass, dbname)
|
|
if res
|
|
print_status('OK.')
|
|
else
|
|
print_error('Error')
|
|
end
|
|
when '-l'
|
|
rpc_list_nodes
|
|
return
|
|
when '-j'
|
|
rpc_view_jobs
|
|
return
|
|
when '-k'
|
|
node = args.shift
|
|
jid = args.shift
|
|
rpc_kill_node(node, jid)
|
|
return
|
|
when '-h'
|
|
print_status('Usage: wmap_nodes [options]')
|
|
print_line("\t-h Display this help text")
|
|
print_line("\t-c id Remove id node (Use ALL for ALL nodes")
|
|
print_line("\t-a host port ssl user pass Add node")
|
|
print_line("\t-d host port user pass db Force all nodes to connect to db")
|
|
print_line("\t-j View detailed jobs")
|
|
print_line("\t-k ALL|id ALL|job_id Kill jobs on node")
|
|
print_line("\t-l List all current nodes")
|
|
|
|
print_line('')
|
|
return
|
|
else
|
|
print_error('Unknown flag.')
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
def cmd_wmap_run(*args)
|
|
# Stop everything
|
|
self.masstop = false
|
|
self.killwhenstop = true
|
|
|
|
trap('INT') do
|
|
print_error('Stopping execution...')
|
|
self.masstop = true
|
|
if killwhenstop
|
|
rpc_kill_node('ALL', 'ALL')
|
|
end
|
|
end
|
|
|
|
# Max numbers of concurrent jobs per node
|
|
self.njobs = 25
|
|
self.nmaxdisplay = false
|
|
self.runlocal = false
|
|
|
|
# Formatting
|
|
sizeline = 60
|
|
|
|
wmap_show = 2**0
|
|
wmap_expl = 2**1
|
|
|
|
# Exclude files can be modified by setting datastore['WMAP_EXCLUDE']
|
|
wmap_exclude_files = '.*\.(gif|jpg|png*)$'
|
|
|
|
run_wmap_ssl = true
|
|
run_wmap_server = true
|
|
run_wmap_dir_file = true
|
|
run_wmap_query = true
|
|
run_wmap_unique_query = true
|
|
run_wmap_generic = true
|
|
|
|
# If module supports datastore['VERBOSE']
|
|
moduleverbose = false
|
|
|
|
showprogress = false
|
|
|
|
if !rpcarr
|
|
self.rpcarr = Hash.new
|
|
end
|
|
|
|
if !run_wmap_ssl
|
|
print_status('Loading of wmap ssl modules disabled.')
|
|
end
|
|
if !run_wmap_server
|
|
print_status('Loading of wmap server modules disabled.')
|
|
end
|
|
if !run_wmap_dir_file
|
|
print_status('Loading of wmap dir and file modules disabled.')
|
|
end
|
|
if !run_wmap_query
|
|
print_status('Loading of wmap query modules disabled.')
|
|
end
|
|
if !run_wmap_unique_query
|
|
print_status('Loading of wmap unique query modules disabled.')
|
|
end
|
|
if !run_wmap_generic
|
|
print_status('Loading of wmap generic modules disabled.')
|
|
end
|
|
|
|
stamp = Time.now.to_f
|
|
mode = 0
|
|
|
|
eprofile = []
|
|
using_p = false
|
|
using_m = false
|
|
usinginipath = false
|
|
|
|
mname = ''
|
|
inipathname = '/'
|
|
|
|
args.push('-h') if args.empty?
|
|
|
|
while (arg = args.shift)
|
|
case arg
|
|
when '-t'
|
|
mode |= wmap_show
|
|
when '-e'
|
|
mode |= wmap_expl
|
|
|
|
profile = args.shift
|
|
|
|
if profile
|
|
print_status("Using profile #{profile}.")
|
|
|
|
begin
|
|
File.open(profile).each do |str|
|
|
if !str.include? '#'
|
|
# Not a comment
|
|
modname = str.strip
|
|
if !modname.empty?
|
|
eprofile << modname
|
|
end
|
|
end
|
|
using_p = true
|
|
end
|
|
rescue StandardError
|
|
print_error('Profile not found or invalid.')
|
|
return
|
|
end
|
|
else
|
|
print_status('Using ALL wmap enabled modules.')
|
|
end
|
|
when '-m'
|
|
mode |= wmap_expl
|
|
|
|
mname = args.shift
|
|
|
|
if mname
|
|
print_status("Using module #{mname}.")
|
|
end
|
|
using_m = true
|
|
when '-p'
|
|
mode |= wmap_expl
|
|
|
|
inipathname = args.shift
|
|
|
|
if inipathname
|
|
print_status("Using initial path #{inipathname}.")
|
|
end
|
|
usinginipath = true
|
|
|
|
when '-h'
|
|
print_status('Usage: wmap_run [options]')
|
|
print_line("\t-h Display this help text")
|
|
print_line("\t-t Show all enabled modules")
|
|
print_line("\t-m [regex] Launch only modules that name match provided regex.")
|
|
print_line("\t-p [regex] Only test path defined by regex.")
|
|
print_line("\t-e [/path/to/profile] Launch profile modules against all matched targets.")
|
|
print_line("\t (No profile file runs all enabled modules.)")
|
|
print_line('')
|
|
return
|
|
else
|
|
print_error('Unknown flag')
|
|
return
|
|
end
|
|
end
|
|
|
|
if rpcarr.empty? && (mode & wmap_show == 0)
|
|
print_error('NO WMAP NODES DEFINED. Executing local modules')
|
|
self.runlocal = true
|
|
end
|
|
|
|
if targets.nil?
|
|
print_error('Targets have not been selected.')
|
|
return
|
|
end
|
|
|
|
if targets.keys.empty?
|
|
print_error('Targets have not been selected.')
|
|
return
|
|
end
|
|
|
|
execmod = true
|
|
if (mode & wmap_show != 0)
|
|
execmod = false
|
|
end
|
|
|
|
targets.each_with_index do |t, idx|
|
|
selected_host = t[1][:host]
|
|
selected_port = t[1][:port]
|
|
selected_ssl = t[1][:ssl]
|
|
selected_vhost = t[1][:vhost]
|
|
|
|
print_status('Testing target:')
|
|
print_status("\tSite: #{selected_vhost} (#{selected_host})")
|
|
print_status("\tPort: #{selected_port} SSL: #{selected_ssl}")
|
|
print_line '=' * sizeline
|
|
print_status("Testing started. #{Time.now}")
|
|
|
|
if !selected_ssl
|
|
run_wmap_ssl = false
|
|
# print_status ("Target is not SSL. SSL modules disabled.")
|
|
end
|
|
|
|
# wmap_dir, wmap_file
|
|
matches = Hash.new
|
|
|
|
# wmap_server
|
|
matches1 = Hash.new
|
|
|
|
# wmap_query
|
|
matches2 = Hash.new
|
|
|
|
# wmap_ssl
|
|
matches3 = Hash.new
|
|
|
|
# wmap_unique_query
|
|
matches5 = Hash.new
|
|
|
|
# wmap_generic
|
|
matches10 = Hash.new
|
|
|
|
# OPTIONS
|
|
jobify = false
|
|
|
|
# This will be clean later
|
|
load_wmap_modules(false)
|
|
|
|
wmapmodules.each do |w|
|
|
case w[2]
|
|
when :wmap_server
|
|
if run_wmap_server
|
|
matches1[w] = true
|
|
end
|
|
when :wmap_query
|
|
if run_wmap_query
|
|
matches2[w] = true
|
|
end
|
|
when :wmap_unique_query
|
|
if run_wmap_unique_query
|
|
matches5[w] = true
|
|
end
|
|
when :wmap_generic
|
|
if run_wmap_generic
|
|
matches10[w] = true
|
|
end
|
|
when :wmap_dir, :wmap_file
|
|
if run_wmap_dir_file
|
|
matches[w] = true
|
|
end
|
|
when :wmap_ssl
|
|
if run_wmap_ssl
|
|
matches3[w] = true
|
|
end
|
|
else
|
|
# Black Hole
|
|
end
|
|
end
|
|
|
|
# Execution order (orderid)
|
|
matches = sort_by_orderid(matches)
|
|
matches1 = sort_by_orderid(matches1)
|
|
matches2 = sort_by_orderid(matches2)
|
|
matches3 = sort_by_orderid(matches3)
|
|
matches5 = sort_by_orderid(matches5)
|
|
matches10 = sort_by_orderid(matches10)
|
|
|
|
#
|
|
# Handle modules that need to be run before all tests IF SERVER is SSL, once usually again the SSL web server.
|
|
# :wmap_ssl
|
|
#
|
|
|
|
print_status "\n=[ SSL testing ]="
|
|
print_line '=' * sizeline
|
|
|
|
if !selected_ssl
|
|
print_status('Target is not SSL. SSL modules disabled.')
|
|
end
|
|
|
|
idx = 0
|
|
matches3.each_key do |xref|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
# Module not part of profile or not match
|
|
next unless (using_p && eprofile.include?(xref[0].split('/').last)) || (using_m && xref[0].to_s.match(mname)) || (!using_m && !using_p)
|
|
|
|
idx += 1
|
|
|
|
begin
|
|
# Module options hash
|
|
modopts = Hash.new
|
|
|
|
#
|
|
# The code is just a proof-of-concept and will be expanded in the future
|
|
#
|
|
print_status "Module #{xref[0]}"
|
|
|
|
if (mode & wmap_expl != 0)
|
|
|
|
#
|
|
# For modules to have access to the global datastore
|
|
# i.e. set -g DOMAIN test.com
|
|
#
|
|
framework.datastore.each do |gkey, gval|
|
|
modopts[gkey] = gval
|
|
end
|
|
|
|
#
|
|
# Parameters passed in hash xref
|
|
#
|
|
modopts['RHOST'] = selected_host
|
|
modopts['RHOSTS'] = selected_host
|
|
modopts['RPORT'] = selected_port.to_s
|
|
modopts['SSL'] = selected_ssl
|
|
modopts['VHOST'] = selected_vhost.to_s
|
|
modopts['VERBOSE'] = moduleverbose
|
|
modopts['ShowProgress'] = showprogress
|
|
modopts['RunAsJob'] = jobify
|
|
|
|
begin
|
|
if execmod
|
|
rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
|
|
#
|
|
# Handle modules that need to be run before all tests, once usually again the web server.
|
|
# :wmap_server
|
|
#
|
|
print_status "\n=[ Web Server testing ]="
|
|
print_line '=' * sizeline
|
|
|
|
idx = 0
|
|
matches1.each_key do |xref|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
# Module not part of profile or not match
|
|
next unless (using_p && eprofile.include?(xref[0].split('/').last)) || (using_m && xref[0].to_s.match(mname)) || (!using_m && !using_p)
|
|
|
|
idx += 1
|
|
|
|
begin
|
|
# Module options hash
|
|
modopts = Hash.new
|
|
|
|
#
|
|
# The code is just a proof-of-concept and will be expanded in the future
|
|
#
|
|
|
|
print_status "Module #{xref[0]}"
|
|
|
|
if (mode & wmap_expl != 0)
|
|
|
|
#
|
|
# For modules to have access to the global datastore
|
|
# i.e. set -g DOMAIN test.com
|
|
#
|
|
framework.datastore.each do |gkey, gval|
|
|
modopts[gkey] = gval
|
|
end
|
|
|
|
#
|
|
# Parameters passed in hash xref
|
|
#
|
|
modopts['RHOST'] = selected_host
|
|
modopts['RHOSTS'] = selected_host
|
|
modopts['RPORT'] = selected_port.to_s
|
|
modopts['SSL'] = selected_ssl
|
|
modopts['VHOST'] = selected_vhost.to_s
|
|
modopts['VERBOSE'] = moduleverbose
|
|
modopts['ShowProgress'] = showprogress
|
|
modopts['RunAsJob'] = jobify
|
|
|
|
begin
|
|
if execmod
|
|
rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
|
|
#
|
|
# Handle modules to be run at every path/file
|
|
# wmap_dir, wmap_file
|
|
#
|
|
print_status "\n=[ File/Dir testing ]="
|
|
print_line '=' * sizeline
|
|
|
|
idx = 0
|
|
matches.each_key do |xref|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
# Module not part of profile or not match
|
|
next unless (using_p && eprofile.include?(xref[0].split('/').last)) || (using_m && xref[0].to_s.match(mname)) || (!using_m && !using_p)
|
|
|
|
idx += 1
|
|
|
|
begin
|
|
# Module options hash
|
|
modopts = Hash.new
|
|
|
|
#
|
|
# The code is just a proof-of-concept and will be expanded in the future
|
|
#
|
|
|
|
print_status "Module #{xref[0]}"
|
|
|
|
if (mode & wmap_expl != 0)
|
|
#
|
|
# For modules to have access to the global datastore
|
|
# i.e. set -g DOMAIN test.com
|
|
#
|
|
framework.datastore.each do |gkey, gval|
|
|
modopts[gkey] = gval
|
|
end
|
|
|
|
#
|
|
# Parameters passed in hash xref
|
|
#
|
|
modopts['RHOST'] = selected_host
|
|
modopts['RHOSTS'] = selected_host
|
|
modopts['RPORT'] = selected_port.to_s
|
|
modopts['SSL'] = selected_ssl
|
|
modopts['VHOST'] = selected_vhost.to_s
|
|
modopts['VERBOSE'] = moduleverbose
|
|
modopts['ShowProgress'] = showprogress
|
|
modopts['RunAsJob'] = jobify
|
|
|
|
#
|
|
# Run the plugins that only need to be
|
|
# launched once.
|
|
#
|
|
|
|
wtype = xref[2]
|
|
|
|
h = framework.db.workspace.hosts.find_by_address(selected_host)
|
|
s = h.services.find_by_port(selected_port)
|
|
w = s.web_sites.find_by_vhost(selected_vhost)
|
|
|
|
test_tree = load_tree(w)
|
|
test_tree.each do |node|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
p = node.current_path
|
|
testpath = Pathname.new(p)
|
|
strpath = testpath.cleanpath(false).to_s
|
|
|
|
#
|
|
# Fixing paths
|
|
#
|
|
|
|
if node.is_leaf? && !node.is_root?
|
|
#
|
|
# Later we can add here more checks to see if its a file
|
|
#
|
|
elsif node.is_root?
|
|
strpath = '/'
|
|
else
|
|
strpath = strpath.chomp + '/'
|
|
end
|
|
|
|
strpath = strpath.gsub('//', '/')
|
|
# print_status("Testing path: #{strpath}")
|
|
|
|
#
|
|
# Launch plugin depending module type.
|
|
# Module type depends on main input type.
|
|
# Code may be the same but it depend on final
|
|
# versions of plugins
|
|
#
|
|
|
|
case wtype
|
|
when :wmap_file
|
|
if node.is_leaf? && !node.is_root?
|
|
#
|
|
# Check if an exclusion regex has been defined
|
|
#
|
|
excludefilestr = framework.datastore['WMAP_EXCLUDE'] || wmap_exclude_files
|
|
|
|
if !(strpath.match(excludefilestr) && (!usinginipath || (usinginipath && strpath.match(inipathname))))
|
|
modopts['PATH'] = strpath
|
|
print_status("Path: #{strpath}")
|
|
|
|
begin
|
|
if execmod
|
|
rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
end
|
|
when :wmap_dir
|
|
if ((node.is_leaf? && !strpath.include?('.')) || node.is_root? || !node.is_leaf?) && (!usinginipath || (usinginipath && strpath.match(inipathname)))
|
|
|
|
modopts['PATH'] = strpath
|
|
print_status("Path: #{strpath}")
|
|
|
|
begin
|
|
if execmod
|
|
rpcnode = rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
|
|
#
|
|
# Run modules for each request to play with URI with UNIQUE query parameters.
|
|
# wmap_unique_query
|
|
#
|
|
print_status "\n=[ Unique Query testing ]="
|
|
print_line '=' * sizeline
|
|
|
|
idx = 0
|
|
matches5.each_key do |xref|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
# Module not part of profile or not match
|
|
next unless (using_p && eprofile.include?(xref[0].split('/').last)) || (using_m && xref[0].to_s.match(mname)) || (!using_m && !using_p)
|
|
|
|
idx += 1
|
|
|
|
begin
|
|
# Module options hash
|
|
modopts = Hash.new
|
|
|
|
#
|
|
# The code is just a proof-of-concept and will be expanded in the future
|
|
#
|
|
|
|
print_status "Module #{xref[0]}"
|
|
|
|
if (mode & wmap_expl != 0)
|
|
#
|
|
# For modules to have access to the global datastore
|
|
# i.e. set -g DOMAIN test.com
|
|
#
|
|
framework.datastore.each do |gkey, gval|
|
|
modopts[gkey] = gval
|
|
end
|
|
|
|
#
|
|
# Parameters passed in hash xref
|
|
#
|
|
|
|
modopts['RHOST'] = selected_host
|
|
modopts['RHOSTS'] = selected_host
|
|
modopts['RPORT'] = selected_port.to_s
|
|
modopts['SSL'] = selected_ssl
|
|
modopts['VHOST'] = selected_vhost.to_s
|
|
modopts['VERBOSE'] = moduleverbose
|
|
modopts['ShowProgress'] = showprogress
|
|
modopts['RunAsJob'] = jobify
|
|
|
|
#
|
|
# Run the plugins for each request that have a distinct
|
|
# GET/POST URI QUERY string.
|
|
#
|
|
|
|
utest_query = Hash.new
|
|
|
|
h = framework.db.workspace.hosts.find_by_address(selected_host)
|
|
s = h.services.find_by_port(selected_port)
|
|
w = s.web_sites.find_by_vhost(selected_vhost)
|
|
|
|
w.web_forms.each do |form|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
#
|
|
# Only test unique query strings by comparing signature to previous tested signatures 'path,p1,p2,pn'
|
|
#
|
|
|
|
datastr = ''
|
|
typestr = ''
|
|
|
|
temparr = []
|
|
|
|
# print_status "---------"
|
|
# print_status form.params
|
|
# print_status "+++++++++"
|
|
|
|
form.params.each do |p|
|
|
pn, pv, _pt = p
|
|
if pn
|
|
if !pn.empty?
|
|
if !pv || pv.empty?
|
|
# TODO: add value based on param name
|
|
pv = 'aaa'
|
|
end
|
|
|
|
# temparr << pn.to_s + "=" + Rex::Text.uri_encode(pv.to_s)
|
|
temparr << pn.to_s + '=' + pv.to_s
|
|
end
|
|
else
|
|
print_error("Blank parameter name. Form #{form.path}")
|
|
end
|
|
end
|
|
|
|
datastr = temparr.join('&') if (temparr && !temparr.empty?)
|
|
|
|
if (utest_query.key?(signature(form.path, datastr)) == false)
|
|
|
|
modopts['METHOD'] = form.method.upcase
|
|
modopts['PATH'] = form.path
|
|
modopts['QUERY'] = form.query
|
|
if form.method.upcase == 'GET'
|
|
modopts['QUERY'] = datastr
|
|
modopts['DATA'] = ''
|
|
end
|
|
if form.method.upcase == 'POST'
|
|
modopts['DATA'] = datastr
|
|
end
|
|
modopts['TYPES'] = typestr
|
|
|
|
#
|
|
# TODO: Add headers, etc.
|
|
#
|
|
if !usinginipath || (usinginipath && form.path.match(inipathname))
|
|
print_status "Path #{form.path}"
|
|
|
|
# print_status("Unique PATH #{modopts['PATH']}")
|
|
# print_status("Unique GET #{modopts['QUERY']}")
|
|
# print_status("Unique POST #{modopts['DATA']}")
|
|
# print_status("MODOPTS: #{modopts}")
|
|
|
|
begin
|
|
if execmod
|
|
rpcnode = rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
utest_query[signature(form.path, datastr)] = 1
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
|
|
#
|
|
# Run modules for each request to play with URI query parameters.
|
|
# This approach will reduce the complexity of the Tree used before
|
|
# and will make this shotgun implementation much simple.
|
|
# wmap_query
|
|
#
|
|
print_status "\n=[ Query testing ]="
|
|
print_line '=' * sizeline
|
|
|
|
idx = 0
|
|
matches2.each_key do |xref|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
# Module not part of profile or not match
|
|
next unless !(using_p && eprofile.include?(xref[0].split('/').last)) || (using_m && xref[0].to_s.match(mname)) || (!using_m && !using_p)
|
|
|
|
idx += 1
|
|
|
|
begin
|
|
# Module options hash
|
|
modopts = Hash.new
|
|
|
|
#
|
|
# The code is just a proof-of-concept and will be expanded in the future
|
|
#
|
|
|
|
print_status "Module #{xref[0]}"
|
|
|
|
if (mode & wmap_expl != 0)
|
|
|
|
#
|
|
# For modules to have access to the global datastore
|
|
# i.e. set -g DOMAIN test.com
|
|
#
|
|
framework.datastore.each do |gkey, gval|
|
|
modopts[gkey] = gval
|
|
end
|
|
|
|
#
|
|
# Parameters passed in hash xref
|
|
#
|
|
|
|
modopts['RHOST'] = selected_host
|
|
modopts['RHOSTS'] = selected_host
|
|
modopts['RPORT'] = selected_port.to_s
|
|
modopts['SSL'] = selected_ssl
|
|
modopts['VHOST'] = selected_vhost.to_s
|
|
modopts['VERBOSE'] = moduleverbose
|
|
modopts['ShowProgress'] = showprogress
|
|
modopts['RunAsJob'] = jobify
|
|
|
|
#
|
|
# Run the plugins for each request that have a distinct
|
|
# GET/POST URI QUERY string.
|
|
#
|
|
|
|
h = framework.db.workspace.hosts.find_by_address(selected_host)
|
|
s = h.services.find_by_port(selected_port)
|
|
w = s.web_sites.find_by_vhost(selected_vhost)
|
|
|
|
w.web_forms.each do |req|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
datastr = ''
|
|
typestr = ''
|
|
|
|
temparr = []
|
|
|
|
req.params.each do |p|
|
|
pn, pv, _pt = p
|
|
if pn
|
|
if !pn.empty?
|
|
if !pv || pv.empty?
|
|
# TODO: add value based on param name
|
|
pv = 'aaa'
|
|
end
|
|
# temparr << pn.to_s + "=" + Rex::Text.uri_encode(pv.to_s)
|
|
temparr << pn.to_s + '=' + pv.to_s
|
|
end
|
|
else
|
|
print_error("Blank parameter name. Form #{req.path}")
|
|
end
|
|
end
|
|
|
|
datastr = temparr.join('&') if (temparr && !temparr.empty?)
|
|
|
|
modopts['METHOD'] = req.method.upcase
|
|
modopts['PATH'] = req.path
|
|
if req.method.upcase == 'GET'
|
|
modopts['QUERY'] = datastr
|
|
modopts['DATA'] = ''
|
|
end
|
|
modopts['DATA'] = datastr if req.method.upcase == 'POST'
|
|
modopts['TYPES'] = typestr
|
|
|
|
#
|
|
# TODO: Add method, headers, etc.
|
|
#
|
|
if !usinginipath || (usinginipath && req.path.match(inipathname))
|
|
print_status "Path #{req.path}"
|
|
|
|
# print_status("Query PATH #{modopts['PATH']}")
|
|
# print_status("Query GET #{modopts['QUERY']}")
|
|
# print_status("Query POST #{modopts['DATA']}")
|
|
# print_status("Query TYPES #{typestr}")
|
|
|
|
begin
|
|
if execmod
|
|
rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
|
|
#
|
|
# Handle modules that need to be after all tests, once.
|
|
# Good place to have modules that analyze the test results and/or
|
|
# launch exploits.
|
|
# :wmap_generic
|
|
#
|
|
print_status "\n=[ General testing ]="
|
|
print_line '=' * sizeline
|
|
|
|
idx = 0
|
|
matches10.each_key do |xref|
|
|
if masstop
|
|
print_error('STOPPED.')
|
|
return
|
|
end
|
|
|
|
# Module not part of profile or not match
|
|
next unless !(using_p && eprofile.include?(xref[0].split('/').last)) || (using_m && xref[0].to_s.match(mname)) || (!using_m && !using_p)
|
|
|
|
idx += 1
|
|
|
|
begin
|
|
# Module options hash
|
|
modopts = Hash.new
|
|
|
|
#
|
|
# The code is just a proof-of-concept and will be expanded in the future
|
|
#
|
|
|
|
print_status "Module #{xref[0]}"
|
|
|
|
if (mode & wmap_expl != 0)
|
|
|
|
#
|
|
# For modules to have access to the global datastore
|
|
# i.e. set -g DOMAIN test.com
|
|
#
|
|
framework.datastore.each do |gkey, gval|
|
|
modopts[gkey] = gval
|
|
end
|
|
|
|
#
|
|
# Parameters passed in hash xref
|
|
#
|
|
|
|
modopts['RHOST'] = selected_host
|
|
modopts['RHOSTS'] = selected_host
|
|
modopts['RPORT'] = selected_port.to_s
|
|
modopts['SSL'] = selected_ssl
|
|
modopts['VHOST'] = selected_vhost.to_s
|
|
modopts['VERBOSE'] = moduleverbose
|
|
modopts['ShowProgress'] = showprogress
|
|
modopts['RunAsJob'] = jobify
|
|
|
|
#
|
|
# Run the plugins that only need to be
|
|
# launched once.
|
|
#
|
|
|
|
begin
|
|
if execmod
|
|
rpc_round_exec(xref[0], xref[1], modopts, njobs)
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception during launch from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
rescue ::Exception
|
|
print_status(" >> Exception from #{xref[0]}: #{$ERROR_INFO}")
|
|
end
|
|
end
|
|
|
|
if (mode & wmap_expl != 0)
|
|
print_line '+' * sizeline
|
|
|
|
if !(runlocal && execmod)
|
|
rpc_list_nodes
|
|
print_status('Note: Use wmap_nodes -l to list node status for completion')
|
|
end
|
|
|
|
print_line("Launch completed in #{Time.now.to_f - stamp} seconds.")
|
|
print_line '+' * sizeline
|
|
end
|
|
|
|
print_status('Done.')
|
|
end
|
|
|
|
# EOM
|
|
end
|
|
|
|
def view_targets
|
|
if targets.nil? || targets.keys.empty?
|
|
print_status 'No targets have been defined'
|
|
return
|
|
end
|
|
|
|
indent = ' '
|
|
|
|
tbl = Rex::Text::Table.new(
|
|
'Indent' => indent.length,
|
|
'Header' => 'Defined targets',
|
|
'Columns' =>
|
|
[
|
|
'Id',
|
|
'Vhost',
|
|
'Host',
|
|
'Port',
|
|
'SSL',
|
|
'Path',
|
|
]
|
|
)
|
|
|
|
targets.each_with_index do |t, idx|
|
|
tbl << [ idx.to_s, t[1][:vhost], t[1][:host], t[1][:port], t[1][:ssl], "\t" + t[1][:path].to_s ]
|
|
end
|
|
|
|
print_status tbl.to_s + "\n"
|
|
end
|
|
|
|
def delete_sites(wmap_index)
|
|
idx = 0
|
|
to_del = {}
|
|
# Rebuild the index from wmap_sites -l
|
|
framework.db.hosts.each do |bdhost|
|
|
bdhost.services.each do |serv|
|
|
serv.web_sites.each do |web|
|
|
# If the index of this site matches any deletion index,
|
|
# add to our hash, saving the index for later output
|
|
to_del[idx] = web if wmap_index.any? { |w| w.to_i == idx }
|
|
idx += 1
|
|
end
|
|
end
|
|
end
|
|
to_del.each do |widx, wsite|
|
|
if wsite.delete
|
|
print_status("Deleted #{wsite.vhost} on #{wsite.service.host.address} at index #{widx}")
|
|
else
|
|
print_error("Could note delete {wsite.vhost} on #{wsite.service.host.address} at index #{widx}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def view_sites
|
|
# Clean temporary sites list
|
|
self.lastsites = []
|
|
|
|
indent = ' '
|
|
|
|
tbl = Rex::Text::Table.new(
|
|
'Indent' => indent.length,
|
|
'Header' => 'Available sites',
|
|
'Columns' =>
|
|
[
|
|
'Id',
|
|
'Host',
|
|
'Vhost',
|
|
'Port',
|
|
'Proto',
|
|
'# Pages',
|
|
'# Forms',
|
|
]
|
|
)
|
|
|
|
idx = 0
|
|
framework.db.hosts.each do |bdhost|
|
|
bdhost.services.each do |serv|
|
|
serv.web_sites.each do |web|
|
|
c = web.web_pages.count
|
|
f = web.web_forms.count
|
|
tbl << [ idx.to_s, bdhost.address, web.vhost, serv.port, serv.name, c.to_s, f.to_s ]
|
|
idx += 1
|
|
|
|
turl = web.vhost + ',' + serv.name + '://' + bdhost.address.to_s + ':' + serv.port.to_s + '/'
|
|
lastsites << turl
|
|
end
|
|
end
|
|
end
|
|
|
|
print_status tbl.to_s + "\n"
|
|
end
|
|
|
|
# Reusing code from hdmoore
|
|
#
|
|
# Allow the URL to be supplied as VHOST,URL if a custom VHOST
|
|
# should be used. This allows for things like:
|
|
# localhost,http://192.168.0.2/admin/
|
|
|
|
def add_web_site(url)
|
|
vhost = nil
|
|
|
|
# Allow the URL to be supplied as VHOST,URL if a custom VHOST
|
|
# should be used. This allows for things like:
|
|
# localhost,http://192.168.0.2/admin/
|
|
|
|
if url !~ /^http/
|
|
vhost, url = url.split(',', 2)
|
|
if url.to_s.empty?
|
|
url = vhost
|
|
vhost = nil
|
|
end
|
|
end
|
|
|
|
# Prefix http:// when the URL has no specified parameter
|
|
if url !~ %r{^[a-z0-9A-Z]+://}
|
|
url = 'http://' + url
|
|
end
|
|
|
|
uri = begin
|
|
URI.parse(url)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
if !uri
|
|
print_error("Could not understand URL: #{url}")
|
|
return
|
|
end
|
|
|
|
vhost = uri.hostname if vhost.nil?
|
|
|
|
if uri.scheme !~ /^https?/
|
|
print_error("Only http and https URLs are accepted: #{url}")
|
|
return
|
|
end
|
|
|
|
ssl = false
|
|
if uri.scheme == 'https'
|
|
ssl = true
|
|
end
|
|
|
|
site = begin
|
|
framework.db.report_web_site(wait: true, host: uri.host, port: uri.port, vhost: vhost, ssl: ssl, workspace: framework.db.workspace)
|
|
rescue SocketError => e
|
|
elog("Could not get address for #{uri.host}", 'wmap', error: e)
|
|
print_status("Could not get address for #{uri.host}.")
|
|
nil
|
|
end
|
|
|
|
return site
|
|
end
|
|
|
|
# Code by hdm. Modified two lines by et
|
|
#
|
|
def process_urls(urlstr)
|
|
target_whitelist = []
|
|
|
|
urls = urlstr.to_s.split(/\s+/)
|
|
|
|
urls.each do |url|
|
|
next if url.to_s.strip.empty?
|
|
|
|
vhost = nil
|
|
|
|
# Allow the URL to be supplied as VHOST,URL if a custom VHOST
|
|
# should be used. This allows for things like:
|
|
# localhost,http://192.168.0.2/admin/
|
|
|
|
if url !~ /^http/
|
|
vhost, url = url.split(',', 2)
|
|
if url.to_s.empty?
|
|
url = vhost
|
|
vhost = nil
|
|
end
|
|
end
|
|
|
|
# Prefix http:// when the URL has no specified parameter
|
|
if url !~ %r{^[a-z0-9A-Z]+://}
|
|
url = 'http://' + url
|
|
end
|
|
|
|
uri = begin
|
|
URI.parse(url)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
if !uri
|
|
print_error("Could not understand URL: #{url}")
|
|
next
|
|
end
|
|
|
|
if uri.scheme !~ /^https?/
|
|
print_error("Only http and https URLs are accepted: #{url}")
|
|
next
|
|
end
|
|
|
|
target_whitelist << [vhost || uri.host, uri]
|
|
end
|
|
|
|
# Skip the DB entirely if no matches
|
|
return if target_whitelist.empty?
|
|
|
|
if !targets
|
|
# First time targets are defined
|
|
self.targets = Hash.new
|
|
end
|
|
|
|
target_whitelist.each do |ent|
|
|
vhost, target = ent
|
|
|
|
begin
|
|
address = Rex::Socket.getaddress(target.host, true)
|
|
rescue SocketError => e
|
|
elog("Could not get address for #{target.host}", 'wmap', error: e)
|
|
print_status("Could not get address for #{target.host}. Skipping.")
|
|
next
|
|
end
|
|
|
|
host = framework.db.workspace.hosts.find_by_address(address)
|
|
if !host
|
|
print_error("No matching host for #{target.host}")
|
|
next
|
|
end
|
|
serv = host.services.find_by_port_and_proto(target.port, 'tcp')
|
|
if !serv
|
|
print_error("No matching service for #{target.host}:#{target.port}")
|
|
next
|
|
end
|
|
|
|
sites = serv.web_sites.where('vhost = ? and service_id = ?', vhost, serv.id)
|
|
|
|
sites.each do |site|
|
|
# Initial default path
|
|
inipath = target.path
|
|
if target.path.empty?
|
|
inipath = '/'
|
|
end
|
|
|
|
# site.web_forms.where(path: target.path).each do |form|
|
|
ckey = [ site.vhost, host.address, serv.port, inipath].join('|')
|
|
|
|
if !targets[ckey]
|
|
targets[ckey] = WebTarget.new
|
|
targets[ckey].merge!({
|
|
vhost: site.vhost,
|
|
host: host.address,
|
|
port: serv.port,
|
|
ssl: (serv.name == 'https'),
|
|
path: inipath
|
|
})
|
|
# self.targets[ckey][inipath] = []
|
|
else
|
|
print_status('Target already set in targets list.')
|
|
end
|
|
|
|
# Store the form object in the hash for this path
|
|
# self.targets[ckey][inipath] << inipath
|
|
# end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Code by hdm. Modified two lines by et
|
|
# lastsites contains a temporary array with vhost,url strings so the id can be
|
|
# referenced in the array and prevent new sites added in the db to corrupt previous id list.
|
|
def process_ids(idsstr)
|
|
if !lastsites || lastsites.empty?
|
|
view_sites
|
|
print_status('Web sites ids. referenced from previous table.')
|
|
end
|
|
|
|
target_whitelist = []
|
|
ids = idsstr.to_s.split(/,/)
|
|
|
|
ids.each do |id|
|
|
next if id.to_s.strip.empty?
|
|
|
|
if id.to_i > lastsites.length
|
|
print_error("Skipping id #{id}...")
|
|
else
|
|
target_whitelist << lastsites[id.to_i]
|
|
print_status("Loading #{lastsites[id.to_i]}.")
|
|
end
|
|
end
|
|
|
|
# Skip the DB entirely if no matches
|
|
return if target_whitelist.empty?
|
|
|
|
if !targets
|
|
self.targets = Hash.new
|
|
end
|
|
|
|
target_whitelist.each do |ent|
|
|
process_urls(ent)
|
|
end
|
|
end
|
|
|
|
def view_site_tree(urlstr, md, ld)
|
|
if !urlstr
|
|
return
|
|
end
|
|
|
|
site_whitelist = []
|
|
|
|
urls = urlstr.to_s.split(/\s+/)
|
|
|
|
urls.each do |url|
|
|
next if url.to_s.strip.empty?
|
|
|
|
vhost = nil
|
|
|
|
# Allow the URL to be supplied as VHOST,URL if a custom VHOST
|
|
# should be used. This allows for things like:
|
|
# localhost,http://192.168.0.2/admin/
|
|
|
|
if url !~ /^http/
|
|
vhost, url = url.split(',', 2)
|
|
|
|
if url.to_s.empty?
|
|
url = vhost
|
|
vhost = nil
|
|
end
|
|
end
|
|
|
|
# Prefix http:// when the URL has no specified parameter
|
|
if url !~ %r{^[a-z0-9A-Z]+://}
|
|
url = 'http://' + url
|
|
end
|
|
|
|
uri = begin
|
|
URI.parse(url)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
if !uri
|
|
print_error("Could not understand URL: #{url}")
|
|
next
|
|
end
|
|
|
|
if uri.scheme !~ /^https?/
|
|
print_error("Only http and https URLs are accepted: #{url}")
|
|
next
|
|
end
|
|
|
|
site_whitelist << [vhost || uri.host, uri]
|
|
end
|
|
|
|
# Skip the DB entirely if no matches
|
|
return if site_whitelist.empty?
|
|
|
|
site_whitelist.each do |ent|
|
|
vhost, target = ent
|
|
|
|
host = framework.db.workspace.hosts.find_by_address(target.host)
|
|
unless host
|
|
print_error("No matching host for #{target.host}")
|
|
next
|
|
end
|
|
serv = host.services.find_by_port_and_proto(target.port, 'tcp')
|
|
unless serv
|
|
print_error("No matching service for #{target.host}:#{target.port}")
|
|
next
|
|
end
|
|
|
|
sites = serv.web_sites.where('vhost = ? and service_id = ?', vhost, serv.id)
|
|
|
|
sites.each do |site|
|
|
t = load_tree(site)
|
|
print_tree(t, target.host, md, ld)
|
|
print_line("\n")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Private function to avoid duplicate code
|
|
def load_tree_core(req, wtree)
|
|
pathchr = '/'
|
|
tarray = req.path.to_s.split(pathchr)
|
|
tarray.delete('')
|
|
tpath = Pathname.new(pathchr)
|
|
tarray.each do |df|
|
|
wtree.add_at_path(tpath.to_s, df)
|
|
tpath += Pathname.new(df.to_s)
|
|
end
|
|
end
|
|
|
|
#
|
|
# Load website structure into a tree
|
|
#
|
|
def load_tree(s)
|
|
wtree = Tree.new(s.vhost)
|
|
|
|
# Load site pages
|
|
s.web_pages.order('path asc').each do |req|
|
|
if req.code != 404
|
|
load_tree_core(req, wtree)
|
|
end
|
|
end
|
|
|
|
# Load site forms
|
|
s.web_forms.each do |req|
|
|
load_tree_core(req, wtree)
|
|
end
|
|
|
|
wtree
|
|
end
|
|
|
|
def print_file(filename)
|
|
ext = File.extname(filename)
|
|
if %w[.txt .md].include? ext
|
|
print '%bld%red'
|
|
elsif %w[.css .js].include? ext
|
|
print '%grn'
|
|
end
|
|
|
|
print_line("#{filename}%clr")
|
|
end
|
|
|
|
#
|
|
# Recursive function for printing the tree structure
|
|
#
|
|
def print_tree_recursive(tree, max_level, indent, prefix, is_last, unicode)
|
|
if !tree.nil? && (tree.depth <= max_level)
|
|
print(' ' * indent)
|
|
|
|
# Prefix serve to print the superior hierarchy
|
|
prefix.each do |bool|
|
|
if unicode
|
|
print (bool ? ' ' : '│') + (' ' * 3)
|
|
else
|
|
print (bool ? ' ' : '|') + (' ' * 3)
|
|
end
|
|
end
|
|
if unicode
|
|
# The last children is special
|
|
print (is_last ? '└' : '├') + ('─' * 2) + ' '
|
|
else
|
|
print (is_last ? '`' : '|') + ('-' * 2) + ' '
|
|
end
|
|
|
|
c = tree.children.count
|
|
|
|
if c > 0
|
|
print_line "%bld%blu#{tree.name}%clr (#{c})"
|
|
else
|
|
print_file tree.name
|
|
end
|
|
|
|
i = 1
|
|
new_prefix = prefix + [is_last]
|
|
tree.children.each_pair do |_, child|
|
|
is_last = i >= c
|
|
print_tree_recursive(child, max_level, indent, new_prefix, is_last, unicode)
|
|
i += 1
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Print Tree structure. Less ugly
|
|
# Modified by Jon P.
|
|
#
|
|
def print_tree(tree, ip, max_level, unicode)
|
|
indent = 4
|
|
if !tree.nil? && (tree.depth <= max_level)
|
|
if tree.depth == 0
|
|
print_line "\n" + (' ' * indent) + "%cya[#{tree.name}] (#{ip})%clr"
|
|
end
|
|
|
|
i = 1
|
|
c = tree.children.count
|
|
tree.children.each_pair do |_, child|
|
|
print_tree_recursive(child, max_level, indent, [], i >= c, unicode)
|
|
i += 1
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
#
|
|
# Signature of the form ',p1,p2,pn' then to be appended to path: path,p1,p2,pn
|
|
#
|
|
def signature(fpath, fquery)
|
|
hsig = queryparse(fquery)
|
|
fpath + ',' + hsig.map { |p| p[0].to_s }.join(',')
|
|
end
|
|
|
|
def queryparse(query)
|
|
params = Hash.new
|
|
|
|
query.split(/[&;]/n).each do |pairs|
|
|
key, value = pairs.split('=', 2)
|
|
if params.key?(key)
|
|
# Error
|
|
else
|
|
params[key] = value
|
|
end
|
|
end
|
|
params
|
|
end
|
|
|
|
def rpc_add_node(host, port, ssl, user, pass, bypass_exist)
|
|
if !rpcarr
|
|
self.rpcarr = Hash.new
|
|
end
|
|
|
|
istr = "#{host}|#{port}|#{ssl}|#{user}|#{pass}"
|
|
|
|
if rpcarr.key?(istr) && !bypass_exist && !rpcarr[istr].nil?
|
|
print_error("Connection already exists #{istr}")
|
|
return
|
|
end
|
|
|
|
begin
|
|
temprpc = ::Msf::RPC::Client.new(
|
|
host: host,
|
|
port: port,
|
|
ssl: ssl
|
|
)
|
|
rescue StandardError
|
|
print_error 'Unable to connect'
|
|
# raise ConnectionError
|
|
return
|
|
end
|
|
|
|
res = temprpc.login(user, pass)
|
|
|
|
if !res
|
|
print_error("Unable to authenticate to #{host}:#{port}.")
|
|
return
|
|
end
|
|
|
|
res = temprpc.call('core.version')
|
|
print_status("Connected to #{host}:#{port} [#{res['version']}].")
|
|
rpcarr[istr] = temprpc
|
|
rescue StandardError
|
|
print_error('Unable to connect')
|
|
end
|
|
|
|
def local_module_exec(mod, mtype, opts, _nmaxjobs)
|
|
jobify = false
|
|
|
|
modinst = framework.modules.create(mod)
|
|
|
|
if !modinst
|
|
print_error('Unknown module')
|
|
return
|
|
end
|
|
|
|
sess = nil
|
|
|
|
case mtype
|
|
when 'auxiliary'
|
|
Msf::Simple::Auxiliary.run_simple(modinst, {
|
|
'Action' => opts['ACTION'],
|
|
'LocalOutput' => driver.output,
|
|
'RunAsJob' => jobify,
|
|
'Options' => opts
|
|
})
|
|
when 'exploit'
|
|
if !(opts['PAYLOAD'])
|
|
opts['PAYLOAD'] = WmapCommandDispatcher::Exploit.choose_payload(modinst, opts['TARGET'])
|
|
end
|
|
|
|
sess = Msf::Simple::Exploit.exploit_simple(modinst, {
|
|
'Payload' => opts['PAYLOAD'],
|
|
'Target' => opts['TARGET'],
|
|
'LocalOutput' => driver.output,
|
|
'RunAsJob' => jobify,
|
|
'Options' => opts
|
|
})
|
|
else
|
|
print_error('Wrong mtype.')
|
|
end
|
|
|
|
if sess
|
|
if ((jobify == false) && sess.interactive?)
|
|
print_line
|
|
driver.run_single("sessions -q -i #{sess.sid}")
|
|
else
|
|
print_status("Session #{sess.sid} created in the background.")
|
|
end
|
|
end
|
|
end
|
|
|
|
def rpc_round_exec(mod, mtype, opts, nmaxjobs)
|
|
res = nil
|
|
idx = 0
|
|
|
|
if active_rpc_nodes == 0
|
|
if !runlocal
|
|
print_error('All active nodes not working or removed')
|
|
return
|
|
end
|
|
res = true
|
|
else
|
|
rpc_reconnect_nodes
|
|
end
|
|
|
|
if masstop
|
|
return
|
|
end
|
|
|
|
until res
|
|
if active_rpc_nodes == 0
|
|
print_error('All active nodes not working or removed')
|
|
return
|
|
end
|
|
|
|
# find the node with less jobs load.
|
|
minjobs = nmaxjobs
|
|
minconn = nil
|
|
nid = 0
|
|
rpcarr.each do |k, rpccon|
|
|
if !rpccon
|
|
print_error("Skipping inactive node #{nid} #{k}")
|
|
nid += 1
|
|
end
|
|
|
|
begin
|
|
currentjobs = rpccon.call('job.list').length
|
|
|
|
if currentjobs < minjobs
|
|
minconn = rpccon
|
|
minjobs = currentjobs
|
|
end
|
|
|
|
if currentjobs == nmaxjobs && (nmaxdisplay == false)
|
|
print_error("Node #{nid} reached max number of jobs #{nmaxjobs}")
|
|
print_error('Waiting for available node/slot...')
|
|
self.nmaxdisplay = true
|
|
end
|
|
# print_status("Node #{nid} #currentjobs #{currentjobs} #min #{minjobs}")
|
|
rescue StandardError
|
|
print_error("Unable to connect. Node #{tarr[0]}:#{tarr[1]}")
|
|
rpcarr[k] = nil
|
|
|
|
if active_rpc_nodes == 0
|
|
print_error('All active nodes, not working or removed')
|
|
return
|
|
else
|
|
print_error('Sending job to next node')
|
|
next
|
|
end
|
|
end
|
|
|
|
nid += 1
|
|
end
|
|
|
|
if minjobs < nmaxjobs
|
|
res = minconn.call('module.execute', mtype, mod, opts)
|
|
self.nmaxdisplay = false
|
|
# print_status(">>>#{res} #{mod}")
|
|
|
|
if res
|
|
if res.key?('job_id')
|
|
return
|
|
else
|
|
print_error("Unable to execute module in node #{k} #{res}")
|
|
end
|
|
end
|
|
end
|
|
|
|
# print_status("Max number of jobs #{nmaxjobs} reached in node #{k}") if minjobs >= nmaxjobs
|
|
|
|
idx += 1
|
|
end
|
|
|
|
if runlocal && !masstop
|
|
local_module_exec(mod, mtype, opts, nmaxjobs)
|
|
end
|
|
end
|
|
|
|
def rpc_db_nodes(host, port, user, pass, name)
|
|
rpc_reconnect_nodes
|
|
|
|
if active_rpc_nodes == 0
|
|
print_error('No active nodes at this time')
|
|
return
|
|
end
|
|
|
|
rpcarr.each do |k, v|
|
|
if v
|
|
v.call('db.driver', { driver: 'postgresql' })
|
|
v.call('db.connect', { database: name, host: host, port: port, username: user, password: pass })
|
|
|
|
res = v.call('db.status')
|
|
|
|
if res['db'] == name
|
|
print_status("db_connect #{res} #{host}:#{port} OK")
|
|
else
|
|
print_error("Error db_connect #{res} #{host}:#{port}")
|
|
end
|
|
else
|
|
print_error("No connection to node #{k}")
|
|
end
|
|
end
|
|
end
|
|
|
|
def rpc_reconnect_nodes
|
|
# Sucky 5 mins token timeout.
|
|
|
|
idx = nil
|
|
rpcarr.each do |k, rpccon|
|
|
next unless rpccon
|
|
|
|
idx = k
|
|
begin
|
|
rpccon.call('job.list').length
|
|
rescue StandardError
|
|
tarr = k.split('|')
|
|
|
|
res = rpccon.login(tarr[3], tarr[4])
|
|
|
|
raise ConnectionError unless res
|
|
|
|
print_error("Reauth to node #{tarr[0]}:#{tarr[1]}")
|
|
break
|
|
end
|
|
end
|
|
rescue StandardError
|
|
print_error("ERROR CONNECTING TO NODE. Disabling #{idx} use wmap_nodes -a to reconnect")
|
|
rpcarr[idx] = nil
|
|
if active_rpc_nodes == 0
|
|
print_error('No active nodes')
|
|
self.masstop = true
|
|
end
|
|
end
|
|
|
|
def rpc_kill_node(i, j)
|
|
if !i
|
|
print_error('Nodes not defined')
|
|
return
|
|
end
|
|
|
|
if !j
|
|
print_error('Node jobs defined')
|
|
return
|
|
end
|
|
|
|
rpc_reconnect_nodes
|
|
|
|
if active_rpc_nodes == 0
|
|
print_error('No active nodes at this time')
|
|
return
|
|
end
|
|
|
|
idx = 0
|
|
rpcarr.each do |_k, rpccon|
|
|
if (idx == i.to_i) || (i.upcase == 'ALL')
|
|
# begin
|
|
if !rpccon
|
|
print_error("No connection to node #{idx}")
|
|
else
|
|
n = rpccon.call('job.list')
|
|
n.each do |id, name|
|
|
if (j == id.to_s) || (j.upcase == 'ALL')
|
|
rpccon.call('job.stop', id)
|
|
print_status("Node #{idx} Killed job id #{id} #{name}")
|
|
end
|
|
end
|
|
end
|
|
# rescue
|
|
# print_error("No connection")
|
|
# end
|
|
end
|
|
idx += 1
|
|
end
|
|
end
|
|
|
|
def rpc_view_jobs
|
|
indent = ' '
|
|
|
|
rpc_reconnect_nodes
|
|
|
|
if active_rpc_nodes == 0
|
|
print_error('No active nodes at this time')
|
|
return
|
|
end
|
|
|
|
idx = 0
|
|
rpcarr.each do |k, rpccon|
|
|
if !rpccon
|
|
print_status("[Node ##{idx}: #{k} DISABLED/NO CONNECTION]")
|
|
else
|
|
|
|
arrk = k.split('|')
|
|
print_status("[Node ##{idx}: #{arrk[0]} Port:#{arrk[1]} SSL:#{arrk[2]} User:#{arrk[3]}]")
|
|
|
|
begin
|
|
n = rpccon.call('job.list')
|
|
|
|
tbl = Rex::Text::Table.new(
|
|
'Indent' => indent.length,
|
|
'Header' => 'Jobs',
|
|
'Columns' =>
|
|
[
|
|
'Id',
|
|
'Job name',
|
|
'Target',
|
|
'PATH',
|
|
]
|
|
)
|
|
|
|
n.each do |id, name|
|
|
jinfo = rpccon.call('job.info', id)
|
|
dstore = jinfo['datastore']
|
|
tbl << [ id.to_s, name, dstore['VHOST'] + ':' + dstore['RPORT'], dstore['PATH']]
|
|
end
|
|
|
|
print_status tbl.to_s + "\n"
|
|
rescue StandardError
|
|
print_status("[Node ##{idx} #{k} DISABLED/NO CONNECTION]")
|
|
end
|
|
end
|
|
idx += 1
|
|
end
|
|
end
|
|
|
|
# Modified from http://stackoverflow.com/questions/946738/detect-key-press-non-blocking-w-o-getc-gets-in-ruby
|
|
def quit?
|
|
while (c = driver.input.read_nonblock(1))
|
|
print_status('Quited')
|
|
return true if c == 'Q'
|
|
end
|
|
false
|
|
rescue Errno::EINTR
|
|
false
|
|
rescue Errno::EAGAIN
|
|
false
|
|
rescue EOFError
|
|
true
|
|
end
|
|
|
|
def rpc_mon_nodes
|
|
# Pretty monitor
|
|
|
|
color = begin
|
|
opts['ConsoleDriver'].output.supports_color?
|
|
rescue StandardError
|
|
false
|
|
end
|
|
|
|
colors = [
|
|
'%grn',
|
|
'%blu',
|
|
'%yel',
|
|
'%whi'
|
|
]
|
|
|
|
# begin
|
|
loop do
|
|
rpc_reconnect_nodes
|
|
|
|
idx = 0
|
|
rpcarr.each do |_k, rpccon|
|
|
v = 'NOCONN'
|
|
n = 1
|
|
c = '%red'
|
|
|
|
if !rpccon
|
|
v = 'NOCONN'
|
|
n = 1
|
|
c = '%red'
|
|
else
|
|
begin
|
|
v = ''
|
|
c = '%blu'
|
|
rescue StandardError
|
|
v = 'ERROR'
|
|
c = '%red'
|
|
end
|
|
|
|
begin
|
|
n = rpccon.call('job.list').length
|
|
c = '%blu'
|
|
rescue StandardError
|
|
n = 1
|
|
v = 'NOCONN'
|
|
c = '%red'
|
|
end
|
|
end
|
|
|
|
# begin
|
|
if !@stdio
|
|
@stdio = Rex::Ui::Text::Output::Stdio.new
|
|
end
|
|
|
|
if color == true
|
|
@stdio.auto_color
|
|
else
|
|
@stdio.disable_color
|
|
end
|
|
msg = "[#{idx}] #{"%bld#{c}||%clr" * n} #{n} #{v}\n"
|
|
@stdio.print_raw(@stdio.substitute_colors(msg))
|
|
|
|
# rescue
|
|
# blah
|
|
# end
|
|
sleep(2)
|
|
idx += 1
|
|
end
|
|
end
|
|
# rescue
|
|
# print_status("End.")
|
|
# end
|
|
end
|
|
|
|
def rpc_list_nodes
|
|
indent = ' '
|
|
|
|
tbl = Rex::Text::Table.new(
|
|
'Indent' => indent.length,
|
|
'Header' => 'Nodes',
|
|
'Columns' =>
|
|
[
|
|
'Id',
|
|
'Host',
|
|
'Port',
|
|
'SSL',
|
|
'User',
|
|
'Pass',
|
|
'Status',
|
|
'#jobs',
|
|
]
|
|
)
|
|
|
|
idx = 0
|
|
|
|
rpc_reconnect_nodes
|
|
|
|
rpcarr.each do |k, rpccon|
|
|
arrk = k.split('|')
|
|
|
|
if !rpccon
|
|
v = 'NOCONN'
|
|
n = ''
|
|
else
|
|
begin
|
|
v = rpccon.call('core.version')['version']
|
|
rescue StandardError
|
|
v = 'ERROR'
|
|
end
|
|
|
|
begin
|
|
n = rpccon.call('job.list').length
|
|
rescue StandardError
|
|
n = ''
|
|
end
|
|
end
|
|
|
|
tbl << [ idx.to_s, arrk[0], arrk[1], arrk[2], arrk[3], arrk[4], v, n]
|
|
idx += 1
|
|
end
|
|
|
|
print_status tbl.to_s + "\n"
|
|
end
|
|
|
|
def active_rpc_nodes
|
|
return 0 if rpcarr.empty?
|
|
|
|
idx = 0
|
|
rpcarr.each do |_k, conn|
|
|
if conn
|
|
idx += 1
|
|
end
|
|
end
|
|
|
|
idx
|
|
end
|
|
|
|
def view_modules
|
|
indent = ' '
|
|
|
|
wmaptype = %i[
|
|
wmap_ssl
|
|
wmap_server
|
|
wmap_dir
|
|
wmap_file
|
|
wmap_unique_query
|
|
wmap_query
|
|
wmap_generic
|
|
]
|
|
|
|
if !wmapmodules
|
|
load_wmap_modules(true)
|
|
end
|
|
|
|
wmaptype.each do |modt|
|
|
tbl = Rex::Text::Table.new(
|
|
'Indent' => indent.length,
|
|
'Header' => modt.to_s,
|
|
'Columns' =>
|
|
[
|
|
'Name',
|
|
'OrderID',
|
|
]
|
|
)
|
|
|
|
idx = 0
|
|
wmapmodules.each do |w|
|
|
oid = w[3]
|
|
if w[3] == 0xFFFFFF
|
|
oid = ':last'
|
|
end
|
|
|
|
if w[2] == modt
|
|
tbl << [w[0], oid]
|
|
idx += 1
|
|
end
|
|
end
|
|
|
|
print_status tbl.to_s + "\n"
|
|
end
|
|
end
|
|
|
|
# Sort hash by orderid
|
|
# Yes sorting hashes dont make sense but actually it does when you are enumerating one. And
|
|
# sort_by of a hash returns an array so this is the reason for this ugly piece of code
|
|
def sort_by_orderid(matches)
|
|
temphash = Hash.new
|
|
|
|
temparr = matches.sort_by do |xref, _v|
|
|
xref[3]
|
|
end
|
|
|
|
temparr.each do |b|
|
|
temphash[b[0]] = b[1]
|
|
end
|
|
|
|
temphash
|
|
end
|
|
|
|
# Load all wmap modules
|
|
def load_wmap_modules(reload)
|
|
if reload || !wmapmodules
|
|
print_status('Loading wmap modules...')
|
|
|
|
self.wmapmodules = []
|
|
|
|
idx = 0
|
|
[ [ framework.auxiliary, 'auxiliary' ], [framework.exploits, 'exploit' ] ].each do |mtype|
|
|
# Scan all exploit modules for matching references
|
|
mtype[0].each_module do |n, m|
|
|
e = m.new
|
|
|
|
# Only include wmap_enabled plugins
|
|
next unless e.respond_to?('wmap_enabled')
|
|
|
|
penabled = e.wmap_enabled
|
|
|
|
if penabled
|
|
wmapmodules << [mtype[1] + '/' + n, mtype[1], e.wmap_type, e.orderid]
|
|
idx += 1
|
|
end
|
|
end
|
|
end
|
|
print_status("#{idx} wmap enabled modules loaded.")
|
|
end
|
|
end
|
|
|
|
def view_vulns
|
|
framework.db.hosts.each do |host|
|
|
host.services.each do |serv|
|
|
serv.web_sites.each do |site|
|
|
site.web_vulns.each do |wv|
|
|
print_status("+ [#{host.address}] (#{site.vhost}): #{wv.category} #{wv.path}")
|
|
print_status("\t#{wv.name} #{wv.description}")
|
|
print_status("\t#{wv.method} #{wv.proof}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class WebTarget < ::Hash
|
|
def to_url
|
|
proto = self[:ssl] ? 'https' : 'http'
|
|
"#{proto}://#{self[:host]}:#{self[:port]}#{self[:path]}"
|
|
end
|
|
end
|
|
|
|
def initialize(framework, opts)
|
|
super
|
|
|
|
if framework.db.active == false
|
|
raise 'Database not connected (try db_connect)'
|
|
end
|
|
|
|
color = begin
|
|
self.opts['ConsoleDriver'].output.supports_color?
|
|
rescue StandardError
|
|
false
|
|
end
|
|
|
|
wmapversion = '1.5.1'
|
|
|
|
wmapbanner = "%red\n.-.-.-..-.-.-..---..---.%clr\n"
|
|
wmapbanner += "%red| | | || | | || | || |-'%clr\n"
|
|
wmapbanner += "%red`-----'`-'-'-'`-^-'`-'%clr\n"
|
|
wmapbanner += "[WMAP #{wmapversion}] === et [ ] metasploit.com 2012\n"
|
|
|
|
if !@stdio
|
|
@stdio = Rex::Ui::Text::Output::Stdio.new
|
|
end
|
|
|
|
if color == true
|
|
@stdio.auto_color
|
|
else
|
|
@stdio.disable_color
|
|
end
|
|
|
|
@stdio.print_raw(@stdio.substitute_colors(wmapbanner))
|
|
|
|
add_console_dispatcher(WmapCommandDispatcher)
|
|
# print_status("#{wmapbanner}")
|
|
end
|
|
|
|
def cleanup
|
|
remove_console_dispatcher('wmap')
|
|
end
|
|
|
|
def name
|
|
'wmap'
|
|
end
|
|
|
|
def desc
|
|
'Web assessment plugin'
|
|
end
|
|
|
|
end
|
|
end
|