Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/plugins/nexpose.rb
Views: 11705
require 'English'1require 'nexpose'23module Msf4Nexpose_yaml = "#{Msf::Config.config_directory}/nexpose.yaml".freeze # location of the nexpose.yml containing saved nexpose creds56# This plugin provides integration with Rapid7 Nexpose7class Plugin::Nexpose < Msf::Plugin8class NexposeCommandDispatcher9include Msf::Ui::Console::CommandDispatcher1011def name12'Nexpose'13end1415def commands16{17'nexpose_connect' => 'Connect to a running Nexpose instance ( user:pass@host[:port] )',18'nexpose_save' => 'Save credentials to a Nexpose instance',19'nexpose_activity' => 'Display any active scan jobs on the Nexpose instance',2021'nexpose_scan' => 'Launch a Nexpose scan against a specific IP range and import the results',22'nexpose_discover' => 'Launch a scan but only perform host and minimal service discovery',23'nexpose_exhaustive' => 'Launch a scan covering all TCP ports and all authorized safe checks',24'nexpose_dos' => 'Launch a scan that includes checks that can crash services and devices (caution)',2526'nexpose_disconnect' => 'Disconnect from an active Nexpose instance',2728'nexpose_sites' => 'List all defined sites',29'nexpose_site_devices' => 'List all discovered devices within a site',30'nexpose_site_import' => 'Import data from the specified site ID',31'nexpose_report_templates' => 'List all available report templates',32'nexpose_command' => 'Execute a console command on the Nexpose instance',33'nexpose_sysinfo' => 'Display detailed system information about the Nexpose instance'3435# @TODO:36# nexpose_stop_scan37}38end3940def nexpose_verify_db41if !(framework.db && framework.db.usable && framework.db.active)42print_error('No database has been configured, please use db_connect first')43return false44end4546true47end4849def nexpose_verify50return false if !nexpose_verify_db5152if !@nsc53print_error("No active Nexpose instance has been configured, please use 'nexpose_connect'")54return false55end5657true58end5960def cmd_nexpose_save(*args)61# if we are logged in, save session details to nexpose.yaml62if args[0] == '-h'63print_status('Usage: ')64print_status(' nexpose_save')65return66end6768if args[0]69print_status('Usage: ')70print_status(' nexpose_save')71return72end7374group = 'default'7576if ((@user && !@user.empty?) && (@host && !@host.empty?) && (@port && !@port.empty? && (@port.to_i > 0)) && (@pass && !@pass.empty?))77config = { group.to_s => { 'username' => @user, 'password' => @pass, 'server' => @host, 'port' => @port, 'trust_cert' => @trust_cert } }78::File.open(Nexpose_yaml.to_s, 'wb') { |f| f.puts YAML.dump(config) }79print_good("#{Nexpose_yaml} created.")80else81print_error('Missing username/password/server/port - relogin and then try again.')82return83end84end8586def cmd_nexpose_connect(*args)87return if !nexpose_verify_db8889if !args[0] && ::File.readable?(Nexpose_yaml.to_s)90lconfig = YAML.load_file(Nexpose_yaml.to_s)91@user = lconfig['default']['username']92@pass = lconfig['default']['password']93@host = lconfig['default']['server']94@port = lconfig['default']['port']95@trust_cert = lconfig['default']['trust_cert']96unless @trust_cert97@sslv = 'ok' # TODO: Not super-thrilled about bypassing the SSL warning...98end99nexpose_login100return101end102103if (args.empty? || args[0].empty? || (args[0] == '-h'))104nexpose_usage105return106end107108@user = @pass = @host = @port = @sslv = @trust_cert = @trust_cert_file = nil109110case args.length111when 1, 2112cred, _split, targ = args[0].rpartition('@')113@user, @pass = cred.split(':', 2)114targ ||= '127.0.0.1:3780'115@host, @port = targ.split(':', 2)116@port ||= '3780'117unless args.length == 1118@trust_cert_file = args[1]119if File.exist? @trust_cert_file120@trust_cert = File.read(@trust_cert_file)121else122@sslv = @trust_cert_file123end124end125when 4, 5126@user, @pass, @host, @port, @trust_cert = args127unless args.length == 4128@trust_cert_file = @trust_cert129if File.exist? @trust_cert_file130@trust_cert = File.read(@trust_cert_file)131else132@sslv = @trust_cert_file133end134end135else136nexpose_usage137return138end139nexpose_login140end141142def nexpose_usage143print_status('Usage: ')144print_status(' nexpose_connect username:password@host[:port] <ssl-confirm || trusted_cert_file>')145print_status(' -OR- ')146print_status(' nexpose_connect username password host port <ssl-confirm || trusted_cert_file>')147end148149def nexpose_login150if !((@user && !@user.empty?) && (@host && !@host.empty?) && (@port && !@port.empty? && (@port.to_i > 0)) && (@pass && !@pass.empty?))151nexpose_usage152return153end154155if ((@host != 'localhost') && (@host != '127.0.0.1') && (@trust_cert.nil? && @sslv != 'ok'))156# consider removing this message and replacing with check on trust_store, and if trust_store is not found validate @host already has a truly trusted cert?157print_error('Warning: SSL connections are not verified in this release, it is possible for an attacker')158print_error(' with the ability to man-in-the-middle the Nexpose traffic to capture the Nexpose')159print_error(" credentials. If you are running this on a trusted network, please pass in 'ok'")160print_error(' as an additional parameter to this command.')161return162end163164# Wrap this so a duplicate session does not prevent a new login165begin166cmd_nexpose_disconnect167rescue ::Interrupt168raise $ERROR_INFO169rescue ::Exception170end171172begin173print_status("Connecting to Nexpose instance at #{@host}:#{@port} with username #{@user}...")174nsc = Nexpose::Connection.new(@host, @user, @pass, @port, nil, nil, @trust_cert)175nsc.login176rescue ::Nexpose::APIError => e177print_error("Connection failed: #{e.reason}")178return179end180181@nsc = nsc182nexpose_compatibility_check183nsc184end185186def cmd_nexpose_activity(*_args)187return if !nexpose_verify188189scans = @nsc.scan_activity || []190case scans.length191when 0192print_status('There are currently no active scan jobs on this Nexpose instance')193when 1194print_status('There is 1 active scan job on this Nexpose instance')195else196print_status("There are currently #{scans.length} active scan jobs on this Nexpose instance")197end198199scans.each do |scan|200print_status(" Scan ##{scan.scan_id} is running on Engine ##{scan.engine_id} against site ##{scan.site_id} since #{scan.start_time}")201end202end203204def cmd_nexpose_sites(*_args)205return if !nexpose_verify206207sites = @nsc.list_sites || []208case sites.length209when 0210print_status('There are currently no active sites on this Nexpose instance')211end212213sites.each do |site|214print_status(" Site ##{site.id} '#{site.name}' Risk Factor: #{site.risk_factor} Risk Score: #{site.risk_score}")215end216end217218def cmd_nexpose_site_devices(*args)219return if !nexpose_verify220221site_id = args.shift222if !site_id223print_error('No site ID was specified')224return225end226227devices = @nsc.list_site_devices(site_id) || []228case devices.length229when 0230print_status('There are currently no devices within this site')231end232233devices.each do |device|234print_status(" Host: #{device.address} ID: #{device.id} Risk Factor: #{device.risk_factor} Risk Score: #{device.risk_score}")235end236end237238def cmd_nexpose_report_templates(*_args)239return if !nexpose_verify240241res = @nsc.list_report_templates || []242243res.each do |report|244print_status(" Template: #{report.id} Name: '#{report.name}' Description: #{report.description}")245end246end247248def cmd_nexpose_command(*args)249return if !nexpose_verify250251if args.empty?252print_error('No command was specified')253return254end255256res = @nsc.console_command(args.join(' ')) || ''257258print_status('Command Output')259print_line(res)260print_line('')261end262263def cmd_nexpose_sysinfo(*_args)264return if !nexpose_verify265266res = @nsc.system_information267268print_status('System Information')269res.each_pair do |k, v|270print_status(" #{k}: #{v}")271end272end273274def nexpose_compatibility_check275res = @nsc.console_command('ver')276if res !~ /^(NSC|Console) Version ID:\s*4[89]0\s*$/m277print_error('')278print_error('Warning: This version of Nexpose has not been tested with Metasploit!')279print_error('')280end281end282283def cmd_nexpose_site_import(*args)284site_id = args.shift285if !site_id286print_error('No site ID was specified')287return288end289290msfid = Time.now.to_i291292report_formats = ['raw-xml-v2', 'ns-xml']293report_format = report_formats.shift294295report = Nexpose::ReportConfig.build(@nsc, site_id, "Metasploit Export #{msfid}", 'pentest-audit', report_format, true)296report.delivery = Nexpose::Delivery.new(true)297298begin299report.format = report_format300report.save(@nsc)301rescue ::Exception => e302report_format = report_formats.shift303if report_format304retry305end306raise e307end308309print_status('Generating the export data file...')310last_report = nil311until last_report312last_report = @nsc.last_report(report.id)313select(nil, nil, nil, 1.0)314end315url = last_report.uri316317print_status('Downloading the export data...')318data = @nsc.download(url)319320# Delete the temporary report ID321@nsc.delete_report_config(report.id)322323print_status('Importing Nexpose data...')324process_nexpose_data(report_format, data)325end326327def cmd_nexpose_discover(*args)328args << '-h' if args.empty?329args << '-t'330args << 'aggressive-discovery'331cmd_nexpose_scan(*args)332end333334def cmd_nexpose_exhaustive(*args)335args << '-h' if args.empty?336args << '-t'337args << 'exhaustive-audit'338cmd_nexpose_scan(*args)339end340341def cmd_nexpose_dos(*args)342args << '-h' if args.empty?343args << '-t'344args << 'dos-audit'345cmd_nexpose_scan(*args)346end347348def cmd_nexpose_scan(*args)349opts = Rex::Parser::Arguments.new(350'-h' => [ false, 'This help menu'],351'-t' => [ true, 'The scan template to use (default:pentest-audit options:full-audit,exhaustive-audit,discovery,aggressive-discovery,dos-audit)'],352'-c' => [ true, 'Specify credentials to use against these targets (format is type:user:pass'],353'-n' => [ true, 'The maximum number of IPs to scan at a time (default is 32)'],354'-s' => [ true, 'The directory to store the raw XML files from the Nexpose instance (optional)'],355'-P' => [ false, 'Leave the scan data on the server when it completes (this counts against the maximum licensed IPs)'],356'-v' => [ false, 'Display diagnostic information about the scanning process'],357'-d' => [ false, 'Scan hosts based on the contents of the existing database'],358'-I' => [ true, 'Only scan systems with an address within the specified range'],359'-E' => [ true, 'Exclude hosts in the specified range from the scan']360)361362opt_template = 'pentest-audit'363opt_maxaddrs = 32364opt_verbose = false365opt_savexml = nil366opt_preserve = false367opt_rescandb = false368opt_addrinc = nil369opt_addrexc = nil370opt_scanned = []371opt_credentials = []372373opt_ranges = []374375opts.parse(args) do |opt, _idx, val|376case opt377when '-h'378print_line('Usage: nexpose_scan [options] <Target IP Ranges>')379print_line(opts.usage)380return381when '-t'382opt_template = val383when '-n'384opt_maxaddrs = val.to_i385when '-s'386opt_savexml = val387when '-c'388if (val =~ /^([^:]+):([^:]+):(.+)/)389type = Regexp.last_match(1)390user = Regexp.last_match(2)391pass = Regexp.last_match(3)392msfid = Time.now.to_i393newcreds = Nexpose::SiteCredentials.for_service("Metasploit Site Credential #{msfid}", nil, nil, nil, nil, type)394newcreds.user_name = user395newcreds.password = pass396opt_credentials << newcreds397else398print_error("Unrecognized Nexpose scan credentials: #{val}")399return400end401when '-v'402opt_verbose = true403when '-P'404opt_preserve = true405when '-d'406opt_rescandb = true407when '-I'408opt_addrinc = OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(val)409when '-E'410opt_addrexc = OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(val)411else412opt_ranges << val413end414end415416return if !nexpose_verify417418# Include all database hosts as scan targets if specified419if opt_rescandb420print_status('Loading scan targets from the active database...') if opt_verbose421framework.db.hosts.each do |host|422next if host.state != ::Msf::HostState::Alive423424opt_ranges << host.address425end426end427428possible_files = opt_ranges # don't allow DOS by circular reference429possible_files.each do |file|430next unless ::File.readable? file431432print_status "Parsing ranges from #{file}"433range_list = ::File.open(file, 'rb') { |f| f.read f.stat.size }434range_list.each_line { |subrange| opt_ranges << subrange }435opt_ranges.delete(file)436end437438opt_ranges = opt_ranges.join(' ')439440if opt_ranges.strip.empty?441print_line('Usage: nexpose_scan [options] <Target IP Ranges>')442print_line(opts.usage)443return444end445446if opt_verbose447print_status("Creating a new scan using template #{opt_template} and #{opt_maxaddrs} concurrent IPs against #{opt_ranges}")448end449450range_inp = ::Msf::OptAddressRange.new('TEMPRANGE', [ true, '' ]).normalize(opt_ranges)451range = ::Rex::Socket::RangeWalker.new(range_inp)452include_range = opt_addrinc ? ::Rex::Socket::RangeWalker.new(opt_addrinc) : nil453exclude_range = opt_addrexc ? ::Rex::Socket::RangeWalker.new(opt_addrexc) : nil454455completed = 0456total = range.num_ips457count = 0458459print_status("Scanning #{total} addresses with template #{opt_template} in sets of #{opt_maxaddrs}")460461while (completed < total)462count += 1463queue = []464465while ((ip = range.next_ip) && (queue.length < opt_maxaddrs))466467if (exclude_range && exclude_range.include?(ip))468print_status(" >> Skipping host #{ip} due to exclusion") if opt_verbose469next470end471472if (include_range && !include_range.include?(ip))473print_status(" >> Skipping host #{ip} due to inclusion filter") if opt_verbose474next475end476477opt_scanned << ip478queue << ip479end480481break if queue.empty?482483print_status("Scanning #{queue[0]}-#{queue[-1]}...") if opt_verbose484485msfid = Time.now.to_i486487# Create a temporary site488site = Nexpose::Site.new(nil, opt_template)489site.name = "Metasploit-#{msfid}"490site.description = 'Autocreated by the Metasploit Framework'491site.included_addresses = queue492site.site_credentials = opt_credentials493site.save(@nsc)494495print_status(" >> Created temporary site ##{site.id}") if opt_verbose496497report_formats = ['raw-xml-v2', 'ns-xml']498report_format = report_formats.shift499500report = Nexpose::ReportConfig.build(@nsc, site.id, site.name, opt_template, report_format, true)501report.delivery = Nexpose::Delivery.new(true)502503begin504report.format = report_format505report.save(@nsc, true)506rescue ::Exception => e507report_format = report_formats.shift508if report_format509retry510end511raise e512end513514print_status(" >> Created temporary report configuration ##{report.id}") if opt_verbose515516# Run the scan517begin518res = site.scan(@nsc)519rescue Nexpose::APIError => e520nexpose_error_message = e.message521nexpose_error_message.gsub!(/NexposeAPI: Action failed: /, '')522print_error nexpose_error_message.to_s523return524end525526sid = res.id527528print_status(" >> Scan has been launched with ID ##{sid}") if opt_verbose529530rep = true531begin532prev = nil533while true534info = @nsc.scan_statistics(sid)535break if info.status != 'running'536537stat = "Found #{info.nodes.live} devices and #{info.nodes.dead} unresponsive"538if (stat != prev) && opt_verbose539print_status(" >> #{stat}")540end541prev = stat542select(nil, nil, nil, 5.0)543end544print_status(" >> Scan has been completed with ID ##{sid}") if opt_verbose545rescue ::Interrupt546rep = false547print_status(" >> Terminating scan ID ##{sid} due to console interrupt") if opt_verbose548@nsc.stop_scan(sid)549break550end551552# Wait for the automatic report generation to complete553if rep554print_status(' >> Waiting on the report to generate...') if opt_verbose555last_report = nil556until last_report557last_report = @nsc.last_report(report.id)558select(nil, nil, nil, 1.0)559end560url = last_report.uri561562print_status(' >> Downloading the report data from Nexpose...') if opt_verbose563data = @nsc.download(url)564565if opt_savexml566::FileUtils.mkdir_p(opt_savexml)567path = ::File.join(opt_savexml, "nexpose-#{msfid}-#{count}.xml")568print_status(" >> Saving scan data into #{path}") if opt_verbose569::File.open(path, 'wb') { |fd| fd.write(data) }570end571572process_nexpose_data(report_format, data)573end574575next if opt_preserve576577# Make sure the scan has finished clean up before attempting to delete the site578loop do579info = @nsc.scan_statistics(sid)580break if info.status == 'stopped' || info.status == 'finished'581582select(nil, nil, nil, 5.0)583end584print_status(' >> Deleting the temporary site and report...') if opt_verbose585begin586@nsc.delete_site(site.id)587rescue ::Nexpose::APIError => e588print_status(" >> Deletion of temporary site and report failed: #{e.inspect}")589end590end591592print_status("Completed the scan of #{total} addresses")593end594595def cmd_nexpose_disconnect(*_args)596@nsc.logout if @nsc597@nsc = nil598end599600def process_nexpose_data(fmt, data)601case fmt602when 'raw-xml-v2'603framework.db.import({ data: data })604when 'ns-xml'605framework.db.import({ data: data })606else607print_error("Unsupported Nexpose data format: #{fmt}")608end609end610611#612# Nexpose vuln lookup613#614def nexpose_vuln_lookup(doc, vid, refs, host, serv = nil)615doc.elements.each("/NexposeReport/VulnerabilityDefinitions/vulnerability[@id = '#{vid}']]") do |vulndef|616title = vulndef.attributes['title']617# pci_severity = vulndef.attributes['pciSeverity']618# cvss_score = vulndef.attributes['cvssScore']619# cvss_vector = vulndef.attributes['cvssVector']620621vulndef.elements['references'].elements.each('reference') do |ref|622if ref.attributes['source'] == 'BID'623refs['BID-' + ref.text] = true624elsif ref.attributes['source'] == 'CVE'625# ref.text is CVE-$ID626refs[ref.text] = true627elsif ref.attributes['source'] == 'MS'628refs['MSB-MS-' + ref.text] = true629end630end631632refs['NEXPOSE-' + vid.downcase] = true633634vuln = framework.db.find_or_create_vuln(635host: host,636service: serv,637name: 'NEXPOSE-' + vid.downcase,638data: title639)640641rids = []642refs.each_key do |r|643rids << framework.db.find_or_create_ref(name: r)644end645646vuln.refs << (rids - vuln.refs)647end648end649650end651652#653# Plugin initialization654#655656def initialize(framework, opts)657super658659add_console_dispatcher(NexposeCommandDispatcher)660banner = ['0a205f5f5f5f202020202020202020202020205f20202020205f205f5f5f5f5f2020205f2020205f20202020205f5f20205f5f2020202020202020202020202020202020202020200a7c20205f205c205f5f205f205f205f5f20285f29205f5f7c207c5f5f5f20207c207c205c207c207c205f5f5f5c205c2f202f5f205f5f2020205f5f5f20205f5f5f20205f5f5f200a7c207c5f29202f205f60207c20275f205c7c207c2f205f60207c20202f202f20207c20205c7c207c2f205f205c5c20202f7c20275f205c202f205f205c2f205f5f7c2f205f205c0a7c20205f203c20285f7c207c207c5f29207c207c20285f7c207c202f202f2020207c207c5c20207c20205f5f2f2f20205c7c207c5f29207c20285f29205c5f5f205c20205f5f2f0a7c5f7c205c5f5c5f5f2c5f7c202e5f5f2f7c5f7c5c5f5f2c5f7c2f5f2f202020207c5f7c205c5f7c5c5f5f5f2f5f2f5c5f5c202e5f5f2f205c5f5f5f2f7c5f5f5f2f5c5f5f5f7c0a20202020202020202020207c5f7c20202020202020202020202020202020202020202020202020202020202020202020207c5f7c202020202020202020202020202020202020200a0a0a'].pack('H*')661662# Do not use this UTF-8 encoded high-ascii art for non-UTF-8 or windows consoles663lang = Rex::Compat.getenv('LANG')664if (lang && lang =~ (/UTF-8/))665# Cygwin/Windows should not be reporting UTF-8 either...666# (! (Rex::Compat.is_windows or Rex::Compat.is_cygwin))667banner = ['202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200a20e29684e29684e29684202020e29684e29684202020202020202020202020e29684e29684e296842020e29684e29684e2968420202020202020202020202020202020202020202020202020202020202020202020202020202020200a20e29688e29688e29688202020e29688e2968820202020202020202020202020e29688e2968820e29684e29688e296882020202020202020202020202020202020202020202020202020202020202020202020202020202020200a20e29688e29688e29680e296882020e29688e29688202020e29684e29688e29688e29688e29688e296842020202020e29688e29688e29688e2968820202020e29688e29688e29684e29688e29688e29688e2968420202020e29684e29688e29688e29688e29688e29684202020e29684e29684e29688e29688e29688e29688e29688e29684202020e29684e29688e29688e29688e29688e2968420200a20e29688e2968820e29688e2968820e29688e296882020e29688e29688e29684e29684e29684e29684e29688e296882020202020e29688e296882020202020e29688e29688e296802020e29680e29688e296882020e29688e29688e296802020e29680e29688e296882020e29688e29688e29684e29684e29684e2968420e296802020e29688e29688e29684e29684e29684e29684e29688e29688200a20e29688e296882020e29688e29684e29688e296882020e29688e29688e29680e29680e29680e29680e29680e2968020202020e29688e29688e29688e2968820202020e29688e2968820202020e29688e296882020e29688e2968820202020e29688e29688202020e29680e29680e29680e29680e29688e29688e296842020e29688e29688e29680e29680e29680e29680e29680e29680200a20e29688e29688202020e29688e29688e296882020e29680e29688e29688e29684e29684e29684e29684e29688202020e29688e296882020e29688e29688202020e29688e29688e29688e29684e29684e29688e29688e296802020e29680e29688e29688e29684e29684e29688e29688e296802020e29688e29684e29684e29684e29684e29684e29688e296882020e29680e29688e29688e29684e29684e29684e29684e29688200a20e29680e29680202020e29680e29680e2968020202020e29680e29680e29680e29680e29680202020e29680e29680e296802020e29680e29680e296802020e29688e2968820e29680e29680e29680202020202020e29680e29680e29680e296802020202020e29680e29680e29680e29680e29680e296802020202020e29680e29680e29680e29680e2968020200a20202020202020202020202020202020202020202020202020202020202020e29688e29688202020202020202020202020202020202020202020202020202020202020202020202020200a202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200a'].pack('H*')668end669print(banner)670print_status('Nexpose integration has been activated')671end672673def cleanup674remove_console_dispatcher('Nexpose')675end676677def name678'nexpose'679end680681def desc682'Integrates with the Rapid7 Nexpose vulnerability management product'683end684end685end686687module Nexpose688class IPRange689def to_json(*_args)690if @to.present?691"#{@from} - #{@to}".to_json692else693@from.to_json694end695end696end697end698699700