# # Web assessment for the metasploit framework # Efrain Torres - et[ ] metasploit.com 2012 # require 'rabal/tree' require 'msf/core/rpc/v10/client' module Msf class Plugin::Wmap < Msf::Plugin class WmapCommandDispatcher attr_accessor :wmapmodules # Enabled Wmap modules attr_accessor :targets # Targets attr_accessor :lastsites # Temp location of previously obtained sites attr_accessor :rpcarr # Array or rpc connections attr_accessor :njobs # Max number of jobs attr_accessor :nmaxdisplay # Flag to stop displaying the same mesg attr_accessor :runlocal # Flag to run local modules only attr_accessor :masstop # Flag to stop everything attr_accessor :killwhenstop # Kill process when exiting 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.length == 0 while (arg = args.shift) case arg when '-l' view_vulns return 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("") return else print_error("Unknown flag.") return end end end def cmd_wmap_modules(*args) args.push("-h") if args.length == 0 while (arg = args.shift) case arg when '-l' view_modules return when '-r' load_wmap_modules(true) return 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("") return else print_error("Unknown flag.") return end end end def cmd_wmap_targets(*args) args.push("-h") if args.length == 0 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.length == 0 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 or l.empty? l = 200 o = 'true' else # Add check if unicode parameters is the second one if l == 'true' or l == 'false' o = l l = 200 else l = l.to_i end end o = (o == 'true') if u.include? 'http' # Parameters are in url form view_site_tree(u,l,o) else # Parameters are digits if !self.lastsites or self.lastsites.length == 0 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 > self.lastsites.length print_error("Skipping id #{id}...") else target_whitelist << self.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.length == 0 unless self.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 not self.rpcarr self.rpcarr=Hash.new() end args.push("-h") if args.length == 0 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 not idref print_error("No id defined") return end if idref.upcase == 'ALL' print_status("All nodes removed") self.rpcarr = Hash.new() else idx=0 self.rpcarr.each do |k,v| if idx == idref.to_i self.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") { print_error("Stopping execution...") self.masstop = true if self.killwhenstop rpc_kill_node('ALL','ALL') end } # Max numbers of concurrent jobs per node self.njobs = 25 self.nmaxdisplay = false self.runlocal = false # Formating 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 not self.rpcarr self.rpcarr = Hash.new() end if not run_wmap_ssl print_status("Loading of wmap ssl modules disabled.") end if not run_wmap_server print_status("Loading of wmap server modules disabled.") end if not run_wmap_dir_file print_status("Loading of wmap dir and file modules disabled.") end if not run_wmap_query print_status("Loading of wmap query modules disabled.") end if not run_wmap_unique_query print_status("Loading of wmap unique query modules disabled.") end if not 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.length == 0 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 not str.include? '#' # Not a comment modname = str.strip if not modname.empty? eprofile << modname end end using_p = true end rescue 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 (self.rpcarr.length == 0) and (mode & wmap_show == 0) print_error("NO WMAP NODES DEFINED. Executing local modules") self.runlocal = true end if self.targets == nil print_error("Targets have not been selected.") return end if self.targets.keys.length == 0 print_error("Targets have not been selected.") return end execmod = true if (mode & wmap_show != 0) execmod = false end self.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 not 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 opt_str = nil jobify = false # This will be clean later load_wmap_modules(false) self.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 not selected_ssl print_status ("Target is not SSL. SSL modules disabled.") end idx = 0 matches3.each_key do |xref| if self.masstop print_error("STOPPED.") return end # Module not part of profile or not match if ( using_p and eprofile.include? xref[0].split('/').last ) or (using_m and xref[0].to_s.match(mname)) or (not using_m and not 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 # self.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 rpcnode = rpc_round_exec(xref[0],xref[1], modopts, self.njobs) end rescue ::Exception print_status(" >> Exception during launch from #{xref[0]}: #{$!}") end end rescue ::Exception print_status(" >> Exception from #{xref[0]}: #{$!}") end 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 self.masstop print_error("STOPPED.") return end # Module not part of profile or not match if ( using_p and eprofile.include? xref[0].split('/').last ) or (using_m and xref[0].to_s.match(mname)) or (not using_m and not 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 # self.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 rpcnode = rpc_round_exec(xref[0],xref[1], modopts, self.njobs) end rescue ::Exception print_status(" >> Exception during launch from #{xref[0]}: #{$!}") end end rescue ::Exception print_status(" >> Exception from #{xref[0]}: #{$!}") end 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 self.masstop print_error("STOPPED.") return end # Module not part of profile or not match if ( using_p and eprofile.include? xref[0].split('/').last ) or (using_m and xref[0].to_s.match(mname)) or (not using_m and not 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 # self.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 = self.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 self.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? and not node.is_root? # # Later we can add here more checks to see if its a file # else if node.is_root? strpath = "/" else strpath = strpath.chomp + "/" end 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? and not node.is_root? # # Check if an exclusion regex has been defined # if self.framework.datastore['WMAP_EXCLUDE'] excludefilestr = self.framework.datastore['WMAP_EXCLUDE'] else excludefilestr = wmap_exclude_files end if not strpath.match(excludefilestr) if (not usinginipath) or (usinginipath and strpath.match(inipathname)) modopts['PATH'] = strpath print_status("Path: #{strpath}") begin if execmod rpcnode = rpc_round_exec(xref[0],xref[1], modopts, self.njobs) end rescue ::Exception print_status(" >> Exception during launch from #{xref[0]}: #{$!}") end end end end when :wmap_dir if (node.is_leaf? and not strpath.include? ".") or node.is_root? or not node.is_leaf? if (not usinginipath) or (usinginipath and 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]}: #{$!}") end end end end end end rescue ::Exception print_status(" >> Exception from #{xref[0]}: #{$!}") end 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 self.masstop print_error("STOPPED.") return end # Module not part of profile or not match if ( using_p and eprofile.include? xref[0].split('/').last ) or (using_m and xref[0].to_s.match(mname)) or (not using_m and not 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 # self.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 = self.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 self.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 not pn.empty? if not pv or 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 and not temparr.empty?) if (utest_query.has_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 (not usinginipath) or (usinginipath and 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, self.njobs) end utest_query[signature(form.path,datastr)]=1 rescue ::Exception print_status(" >> Exception during launch from #{xref[0]}: #{$!}") end end else #print_status("Already tested") end end end rescue ::Exception print_status(" >> Exception from #{xref[0]}: #{$!}") end 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 self.masstop print_error("STOPPED.") return end # Module not part of profile or not match if not ( using_p and eprofile.include? xref[0].split('/').last ) or (using_m and xref[0].to_s.match(mname)) or (not using_m and not 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 # self.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 = self.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 self.masstop print_error("STOPPED.") return end datastr = "" typestr = "" temparr = [] req.params.each do |p| pn, pv, pt = p if pn if not pn.empty? if not pv or 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 and not 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 (not usinginipath) or (usinginipath and 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 rpcnode = rpc_round_exec(xref[0],xref[1], modopts, self.njobs) end rescue ::Exception print_status(" >> Exception during launch from #{xref[0]}: #{$!}") end end end end rescue ::Exception print_status(" >> Exception from #{xref[0]}: #{$!}") end end end # # Handle modules that need to be after all tests, once. # Good place to have modules that analize 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 self.masstop print_error("STOPPED.") return end # Module not part of profile or not match if not ( using_p and eprofile.include? xref[0].split('/').last ) or (using_m and xref[0].to_s.match(mname)) or (not using_m and not 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 # self.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 rpcnode = rpc_round_exec(xref[0],xref[1], modopts, self.njobs) end rescue ::Exception print_status(" >> Exception during launch from #{xref[0]}: #{$!}") end end rescue ::Exception print_status(" >> Exception from #{xref[0]}: #{$!}") end end end if (mode & wmap_expl != 0) print_line "+" * sizeline if not self.runlocal if execmod rpc_list_nodes() print_status("Note: Use wmap_nodes -l to list node status for completion") end 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 self.targets == nil or self.targets.keys.length == 0 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', ]) self.targets.each_with_index { |t, idx| tbl << [ idx.to_s, t[1][:vhost], t[1][:host], t[1][:port], t[1][:ssl], "\t"+t[1][:path].to_s ] } print_status tbl.to_s + "\n" end def delete_sites(wmap_index) idx = 0 to_del = {} # Rebuild the index from wmap_sites -l self.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 self.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 + "/" self.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 !~ /^[a-z0-9A-Z]+:\/\// url = "http://" + url end uri = URI.parse(url) rescue nil if not uri print_error("Could not understand URL: #{url}") return end 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 = self.framework.db.report_web_site(:wait => true, :host => uri.host, :port => uri.port, :vhost => vhost, :ssl => ssl) 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 !~ /^[a-z0-9A-Z]+:\/\// url = "http://" + url end uri = URI.parse(url) rescue nil if not 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.length == 0 if not self.targets # First time targets are defined self.targets = Hash.new() end target_whitelist.each do |ent| vhost,target = ent host = self.framework.db.workspace.hosts.find_by_address(target.host) if not host print_error("No matching host for #{target.host}") next end serv = host.services.find_by_port_and_proto(target.port, 'tcp') if not serv print_error("No matching service for #{target.host}:#{target.port}") next end #print_status "aaa" #print_status framework.db.workspace.name sites = serv.web_sites.where('vhost = ? and service_id = ?', vhost, serv.id) sites.each do |site| # Initial defaul 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 not self.targets[ckey] self.targets[ckey] = WebTarget.new self.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 !self.lastsites or self.lastsites.length == 0 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 > self.lastsites.length print_error("Skipping id #{id}...") else target_whitelist << self.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.length == 0 if not self.targets self.targets = Hash.new() end target_whitelist.each do |ent| process_urls(ent) end end def view_site_tree(urlstr, md, ld) if not 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 !~ /^[a-z0-9A-Z]+:\/\// url = "http://" + url end uri = URI.parse(url) rescue nil if not 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.length == 0 site_whitelist.each do |ent| vhost,target = ent host = self.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 = 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 and tree.depth <= max_level print (' ' * indent) # Prefix serve to print the superior hierarchy prefix.each { |bool| if unicode print (bool ? ' ' : '│') + (' ' * 3) else print (bool ? ' ' : '|') + (' ' * 3) 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.to_s })" else print_file tree.name end i = 1 new_prefix = prefix + [is_last] tree.children.each_pair { |_,child| is_last = !(i < c) print_tree_recursive(child, max_level, indent, new_prefix, is_last, unicode) i += 1 } end end # # Print Tree structure. Less ugly # Modified by Jon P. # def print_tree(tree, ip, max_level, unicode) indent = 4 if tree != nil and 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 def signature(fpath,fquery) hsig = Hash.new() hsig = queryparse(fquery) # # Signature of the form ',p1,p2,pn' then to be appended to path: path,p1,p2,pn # sigstr = 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.has_key?(key) #Error else params[key] = value end end params end def rpc_add_node(host,port,ssl,user,pass,bypass_exist) if not self.rpcarr self.rpcarr = Hash.new() end begin istr = "#{host}|#{port}|#{ssl}|#{user}|#{pass}" if self.rpcarr.has_key?(istr) and not bypass_exist and self.rpcarr[istr] != nil print_error("Connection already exists #{istr}") else begin temprpc = ::Msf::RPC::Client.new( :host => host, :port => port, :ssl => ssl ) rescue print_error "Unable to connect" #raise ConnectionError return end res = temprpc.login( user , pass) if not res print_error("Unable to authenticate to #{host}:#{port}.") return else res = temprpc.call('core.version') end print_status("Connected to #{host}:#{port} [#{res['version']}].") self.rpcarr[istr] = temprpc end rescue print_error("Unable to connect") end end def local_module_exec(mod,mtype, opts, nmaxjobs) jobify = false modinst = framework.modules.create(mod) if(not 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 not 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 and 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 not self.runlocal print_error("All active nodes not working or removed") return end res = true else rpc_reconnect_nodes() end if self.masstop return end while not 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 self.rpcarr.each do |k,rpccon| if not rpccon print_error("Skipping inactive node #{nid} #{k}") else begin currentjobs = rpccon.call('job.list').length if currentjobs < minjobs minconn = rpccon minjobs = currentjobs end if currentjobs == nmaxjobs if self.nmaxdisplay == false print_error("Node #{nid} reached max number of jobs #{nmaxjobs}") print_error("Waiting for available node/slot...") self.nmaxdisplay = true end end #print_status("Node #{nid} #currentjobs #{currentjobs} #min #{minjobs}") rescue print_error("Unable to connect. Node #{tarr[0]}:#{tarr[1]}") self.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 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.has_key?("job_id") return else print_error("Unable to execute module in node #{k} #{res}") end end else #print_status("Max number of jobs #{nmaxjobs} reached in node #{k}") end idx += 1 end if self.runlocal and not self.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 self.rpcarr.each do |k,v| if v res = v.call('db.driver',{:driver => 'postgresql'}) res = 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() begin # Sucky 5 mins token timeout. idx = nil self.rpcarr.each do |k,rpccon| if rpccon idx = k begin currentjobs = rpccon.call('job.list').length rescue tarr = k.split("|") rflag = false res = rpccon.login(tarr[3],tarr[4]) if res rflag = true print_error("Reauth to node #{tarr[0]}:#{tarr[1]}") break else raise ConnectionError end end end end rescue print_error("ERROR CONNECTING TO NODE. Disabling #{idx} use wmap_nodes -a to reconnect") self.rpcarr[idx] = nil if active_rpc_nodes == 0 print_error("No active nodes") self.masstop = true else #blah end end end def rpc_kill_node(i,j) if not i print_error("Nodes not defined") return end if not 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 self.rpcarr.each do |k,rpccon| if idx == i.to_i or i.upcase == 'ALL' #begin if not rpccon print_error("No connection to node #{idx}") else n = rpccon.call('job.list') n.each do |id,name| if j==id.to_s or 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 self.rpcarr.each do |k,rpccon| if not 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 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? begin 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 end def rpc_mon_nodes() # Pretty monitor color = self.opts["ConsoleDriver"].output.supports_color? rescue false colors = [ '%grn', '%blu', '%yel', '%whi' ] #begin loop do rpc_reconnect_nodes() idx = 0 self.rpcarr.each do |k,rpccon| arrk = k.split('|') v = "NOCONN" n = 1 c = '%red' if not rpccon v = "NOCONN" n = 1 c = '%red' else begin v = "" c = '%blu' rescue v = "ERROR" c = '%red' end begin n = rpccon.call('job.list').length c = '%blu' rescue n = 1 v = "NOCONN" c = '%red' end end #begin if not @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() self.rpcarr.each do |k,rpccon| arrk = k.split('|') if not rpccon v = "NOCONN" n = "" else begin v = rpccon.call('core.version')['version'] rescue v = "ERROR" end begin n = rpccon.call('job.list').length rescue 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 if self.rpcarr.length == 0 return 0 else idx = 0 self.rpcarr.each do |k,conn| if conn idx += 1 end end return idx end end def view_modules indent = ' ' wmaptype = [:wmap_ssl, :wmap_server, :wmap_dir, :wmap_file, :wmap_unique_query, :wmap_query, :wmap_generic ] if not self.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 self.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 # 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(m) temphash=Hash.new() temparr=[] temparr = m.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 or not self.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 if e.respond_to?("wmap_enabled") penabled = e.wmap_enabled if penabled self.wmapmodules << [mtype[1]+'/'+n,mtype[1],e.wmap_type,e.orderid] idx += 1 end 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 = self.opts["ConsoleDriver"].output.supports_color? rescue false 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 not @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 protected end end