From f842bb71695d3e8b8f5283a8bd17eeeb26a29267 Mon Sep 17 00:00:00 2001 From: Chris Pomfret Date: Mon, 24 Nov 2025 12:15:55 +0000 Subject: [PATCH] Nexpose plugin - Query nexpose via v3 api when doing scan --- Gemfile.lock | 8 ++++ metasploit-framework.gemspec | 3 +- plugins/nexpose.rb | 88 ++++++++++++++++++++++++++---------- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 67ed70ba9b..9a1e05e8d9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -79,6 +79,7 @@ PATH recog redcarpet reline + rest-client rex-arch rex-bin_tools rex-core @@ -285,6 +286,7 @@ GEM hrr_rb_ssh-ed25519 (0.4.2) ed25519 (~> 1.2) hrr_rb_ssh (>= 0.4) + http-accept (1.7.0) http-cookie (1.0.8) domain_name (~> 0.5) http_parser.rb (0.8.0) @@ -388,6 +390,7 @@ GEM net-smtp (0.5.1) net-protocol net-ssh (7.3.0) + netrc (0.11.0) network_interface (0.0.4) nexpose (7.3.0) nio4r (2.7.4) @@ -478,6 +481,11 @@ GEM reline (0.6.2) io-console (~> 0.5) require_all (3.0.0) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rex-arch (0.1.18) rex-text rex-bin_tools (0.1.10) diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 9d61547452..d4e9c79fee 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -157,8 +157,9 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'net-ldap' spec.add_runtime_dependency 'net-smtp' spec.add_runtime_dependency 'net-sftp' + spec.add_runtime_dependency 'rest-client' spec.add_runtime_dependency 'winrm' - # Pinned to avoid WinRM warnings: https://github.com/WinRb/WinRM/issues/355 - if bumping verify windows/winrm/winrm_script_exec works against metasploitable with vagrant/vagrant creds + # Pinned to avoid WinRM warnings: https://github.com/WinRb/WinRM/issues/355 - if bumping verify windows/winrm/winrm_script_exec works against metasploitable with vagrant/vagrant creds spec.add_runtime_dependency 'rexml', '3.4.1' spec.add_runtime_dependency 'ffi', '< 1.17.0' diff --git a/plugins/nexpose.rb b/plugins/nexpose.rb index c6e9d2db4c..c426b08838 100644 --- a/plugins/nexpose.rb +++ b/plugins/nexpose.rb @@ -1,5 +1,6 @@ require 'English' require 'nexpose' +require 'rest-client' module Msf Nexpose_yaml = "#{Msf::Config.config_directory}/nexpose.yaml".freeze # location of the nexpose.yml containing saved nexpose creds @@ -391,9 +392,15 @@ module Msf user = Regexp.last_match(2) pass = Regexp.last_match(3) msfid = Time.now.to_i - newcreds = Nexpose::SiteCredentials.for_service("Metasploit Site Credential #{msfid}", nil, nil, nil, nil, type) - newcreds.user_name = user - newcreds.password = pass + newcreds = { + "name": "Metasploit Site Credential #{msfid}", + "enabled": true, + "account": { + "service": type, + "username": user, + "password": pass, + }, + } opt_credentials << newcreds else print_error("Unrecognized Nexpose scan credentials: #{val}") @@ -486,19 +493,35 @@ module Msf msfid = Time.now.to_i # Create a temporary site - site = Nexpose::Site.new(nil, opt_template) - site.name = "Metasploit-#{msfid}" - site.description = 'Autocreated by the Metasploit Framework' - site.included_addresses = queue - site.site_credentials = opt_credentials - site.save(@nsc) + create_site_payload = { + description: "Autocreated by the Metasploit Framework'", + scanTemplateId: opt_template, + name: "Metasploit-#{msfid}", + scan: { + assets: { + includedTargets: { + addresses: queue, + }, + }, + }, + } - print_status(" >> Created temporary site ##{site.id}") if opt_verbose + response = post_v3(resource: "sites", payload: create_site_payload) + @site_id = response["id"] + + print_status(" >> Created temporary site ##{@site_id}") if opt_verbose + + opt_credentials.each do |credential| + response = post_v3(resource: "sites/#{@site_id}/site_credentials", payload: credential) + credential_id = response["id"] + + print_status(" >> Created site credential ##{credential_id}: #{credential['name']}") + end report_formats = ['raw-xml-v2', 'ns-xml'] report_format = report_formats.shift - report = Nexpose::ReportConfig.build(@nsc, site.id, site.name, opt_template, report_format, true) + report = Nexpose::ReportConfig.build(@nsc, @site_id, create_site_payload[:name], opt_template, report_format, true) report.delivery = Nexpose::Delivery.new(true) begin @@ -514,17 +537,8 @@ module Msf print_status(" >> Created temporary report configuration ##{report.id}") if opt_verbose - # Run the scan - begin - res = site.scan(@nsc) - rescue Nexpose::APIError => e - nexpose_error_message = e.message - nexpose_error_message.gsub!(/NexposeAPI: Action failed: /, '') - print_error nexpose_error_message.to_s - return - end - - sid = res.id + response = post_v3(resource: "sites/#{@site_id}/scans", payload: { name: opt_template }) + sid = response["id"] print_status(" >> Scan has been launched with ID ##{sid}") if opt_verbose @@ -584,7 +598,7 @@ module Msf end print_status(' >> Deleting the temporary site and report...') if opt_verbose begin - @nsc.delete_site(site.id) + @nsc.delete_site(@site_id) rescue ::Nexpose::APIError => e print_status(" >> Deletion of temporary site and report failed: #{e.inspect}") end @@ -648,6 +662,34 @@ module Msf end end + def post_v3(resource: "", payload: {}) + response = RestClient::Request.execute( + verify_ssl: false, + method: :post, + url: nexpose_v3_url(resource: resource), + payload: payload.to_json, + headers: nexpose_shared_headers + ) + + dlog "Response body: #{response.body}" + JSON.parse(response.body) + end + + def nexpose_v3_url(resource: "") + "https://#{@host}:#{@port}/api/3/#{resource}" + end + + def nexpose_basic_auth_credentials + "Basic #{Base64::encode64([@user, @pass].join(':'))}" + end + + def nexpose_shared_headers + { + content_type: :json, + accept: :json, + Authorization: nexpose_basic_auth_credentials, + } + end end #