Secure Reality Pty Ltd. Security Advisory #8 (SRADV00008)
Remote command execution vulnerabilities in phpMyAdmin and phpPgAdmin
- phpMyAdmin up to and including official 2.1.0 (without SecureReality
patch), or less than 2.2.0pre5 of unofficial project
- phpPgAdmin below 2.3
phpMyAdmin is an easy to use web based administration interface for MySQL
written in PHP. It was written by Tobias Ratschiller, author of several PHP
textbooks, regular speaker on PHP and prominent member of the PHP community.
phpMyAdmin is extremely popular and very widespread (site rankings show it
almost as popular as PHP itself) since it makes most MySQL administration
tasks much easier.
A further indication of its popularity is the fact that is has since been
ported (largely by independent development) from MySQL to also work on
PostgreSQL as a separate product called phpPgAdmin.
phpMyAdmin (and phpPgAdmin by its common code base) makes insecure calls to
the PHP function include(). Installations of the versions specified are
vulnerable to attacks in which the attacker gains the ability to execute
arbitrary commands (and code) on the remote web server with the permissions
of the web server user, typically 'nobody'. Please note that enabling
'Advanced Authentication' does _NOT_ prevent this attack. Given command
execution ability the attacker also gains the ability to read the
configuration files of the installation, thereby gaining database
Remote command execution (with privileges as above)
Disclosure of Database Credentials
Please note that this vulnerability was discussed in detail at the Black Hat
Briefings in Hong Kong and Singapore in Asia 2001. At some stage, powerpoint
presentation notes and audio/video of the presentation will become available
Note also that this description will be best understood (and is released in
conjunction with) our new paper "A Study In Scarlet - Exploiting Common
Vulnerabilities in PHP Applications" which can be downloaded from
Ok, I'm going to explain this vulnerability in terms of how an attacker
might go about finding the problem, sidestepping the various issues in its
exploitation then successfully executing code on the remote web server.
The problem is spotted initially with a trivial grep of the source. The
following line of code in sql.php seems suspicious:
The include() function tells PHP to read in the file specified in the
variable $goto and interpret it as though it were PHP code. If the attacker
can affect $goto (with form input) they may be able to point this at
sensitive local files (e.g /etc/passwd) and have them returned or even
worse, have their own PHP interpreted which allows them to run arbitrary
Looking at the context around this code:
5 $no_require = true;
8 if(isset($goto) && $goto == "sql.php")
10 $goto =
13 // Go back to further page if table should not be dropped
14 if(isset($btnDrop) && $btnDrop == $strNo)
19 Header("Location: $goto");
sql.php is normally used by phpMyAdmin to perform freeform SQL queries
(usually select statements), its also used to drop and empty tables. For
drop and empty actions the page is designed to first confirm the action
(with an 'Are you sure?' type page) then perform the action and return the
user to an application defined page. The code we are looking at above is the
code to determine if the person said no to the 'Are you sure?' and if so, to
return them to the page where they began.
So, the user enters this page by following a link somewhere else in the
application. The link has as form input, amongst other things, the $goto
variable set to an appropriate place to return to once the action is
completed (or cancelled as the case may be).
Line 4 includes some sort of library code (presumably configuration
information too). Then lines 8-11 redefine $goto to include form information
if the page set to return to is sql.php itself. Line 14 checks if the form
input contains the variable $btnDrop (which is the form button usually used
to select 'Yes' or 'No' at the confirmation prompt). If the input does
contain $btnDrop and it is set to 'No' in the language phpMyAdmin is using
($strNo) sql.php assumes the user has just clicked No to a drop/clear action
and begins processing code to return them to the page they came from. Line
16 looks at the $goto variable (which is set as described above in the link
used to get to sql.php to set a page to return to), it attempts to be
intelligent and if that page is found on the local system
(file_exists($goto)) include()s the file for interpretation by PHP instead
of redirecting the browser (as on line 19).
This code is undoubtedly vulnerable. The variable $goto is MEANT to be set
by the remote web browser in form input and can be pointed at any local file
the attacker wishes. So as a first attempt the attacker might surf in their
web browser to:
which might be expected to return the text of the password file on the
remote machine. Unfortunately, in most cases this won't actually succeed and
instead a username and password box will pop up. This is the 'Advanced
authentication' configuration for phpMyAdmin. phpMyAdmin is not designed for
use on the Internet (this is stated in the documentation) and in its most
basic configuration users do not have to log in, they simply have to know
the url of the installation. In this configuration a set of MySQL
credentials are stored in a configuration file and all users of the
application share those credentials. This is obviously a bad thing, both on
an Intranet and the Internet. Thus later versions supply an 'Advanced
authentication' configuration that forces users to login using a MySQL
username and password and their access is limited to the access of those
credentials. Even though the documentation states phpMyAdmin should not be
used on the Internet many users have done so, relying on the Advanced
authentication to prevent anonymous users accessing the databases.
So, presumably the attacker doesn't have credentials on the remote databases
which means they will need a way around this authentication. Remember line 4
of sql.php which included lib.inc.php? Obviously this authentication must be
happening somewhere inside there so here's some context:
... definition of a few utility functions
103 while(list($key, $val) = each($cfgServers))
105 // Don't use servers with no hostname
106 if (empty($val['host']))
110 if(empty($server) || !isset($cfgServers[$server]) ||
111 $server = $cfgServerDefault;
113 if($server == 0)
115 // If no server is selected, make sure that $cfgServer is empty
116 // (so that nothing will work), and skip server authentication.
117 // We do NOT exit here, but continue on without logging into
118 // any server. This way, the welcome page will still come up
119 // (with no server info) and present a choice of servers in the
120 // case that there are multiple servers and '$cfgServerDefault = 0'
121 // is set.
122 $cfgServer = array();
126 // Otherwise, set up $cfgServer and do the usual login stuff.
127 $cfgServer = $cfgServers[$server];
Line 4 includes some sort of configuration information from config.inc.php.
Line 102 goes on to enumerate an array called $cfgServers (which presumably
is set in config.inc.php) and removes any entries that don't have a 'host'
element (which implies the array is two dimensional, arrays in PHP are
associative). Line 110 then checks if the variable $server is '' or if
$cfgServers[$server] isn't set or isn't itself an array, if any of those
conditions are true $server is set to $cfgServerDefault. Finally the code
checks if $server is 0, if it is then (as the comment specified)
authentication is completely skipped, obviously something the attacker would
Ok, so what does this mean? phpMyAdmin can be configured to manage several
different MySQL servers. In this case, before demanding a login, it provides
a select box for the user to select which MySQL server they want to manage.
The code around line 103 removes misconfigured servers. The code around line
110 checks the users selection, if it isn't in the list of configured
servers the server is set to $cfgServerDefault (a default server). Finally
in line 113 the program checks if no server has yet been selected, and if 0
has been selected it doesn't force a login based on the assumption the user
must be at the main index about to choose a server. It shouldn't matter
anyway, since the user hasn't provided credentials for a database the
application won't connect anywhere so from the applications point of view
there is no security issue in allowing pages to execute while not connected
to a database. However, the attacker is attacking the application and not
Given the above, the attacker obviously wants to set $server to 0 so that
authentication will be skipped. But this doesn't work (in most situations).
Looking at some context from config.inc.php:
9 // The $cfgServers array starts with $cfgServers. Do not use
10 // You can disable a server config entry by setting host to ''.
11 $cfgServers['host'] = 'localhost'; // MySQL hostname
12 $cfgServers['port'] = ''; // MySQL port - leave
blank for default port
13 $cfgServers['adv_auth'] = true; // Use advanced authentic
... more cfgServers entries ...
41 // If you have more than one server configured, you can set
42 // to any one of them to autoconnect to that server when phpMyAdmin is
43 // or set it to 0 to be given a list of servers without logging in
44 // If you have only one server configured, $cfgServerDefault *MUST* be
45 // set to that server.
46 $cfgServerDefault = 1; // Default server (0=
no default server)
47 $cfgServer = '';
Line 48 above deliberately forces cfgServers to be unset. This means that
if an attacker sets $server = 0 the !isset($cfgServers[$server]) clause of
the if statment on line 110 of lib.inc.php will evalutate to true and
$server will be set to $cfgServerDefault. As the comment on line 41 above
indicates $cfgServerDefault is usually set to a specific server (in almost
all installations). So the attacker still needs a way to set $server = 0
without triggering the if statement that evaluates cfgServers[$server] and
resets it to the default.
The answer to this is in loose typing. $server simply needs to evaluate to
the _numeric_ value 0. It doesn't have to be '0', just evaluate to 0. Many
different strings evaluate to 0, for example '', '0', '00'. So the attacker
needs to set $server to some value that evaluates to 0 and insure that the
array entry $cfgServers[$server]['host'] is set. Note that the
config.inc.php code never EMPTIES the cfgServers array, this means that an
attacker can submit as form input entries for this array. Take for example
the $server value '000'. This value evaluates to 0 in a numeric context. The
attacker can now create as form input $cfgServers[host]=hello. Remember
that PHP arrays are associative (that is, the index is a string), thus
$cfgServers is NOT the same as cfgServers.
Given the above, the attacker might try the following in their web browser:
Sure enough, all the tests are passed and the passwd file of the remote
server is returned in a web page, straight through the firewall and past the
Now, the attacker is unlikely to be satisfied with simply being able to read
files on the remote web server, they're goal is to execute commands. They
have the ability to include any file they wish to be executed as PHP, they
simply need to get some PHP code of their choosing into a file on the remote
machine. There are many ways to do this in PHP (see our paper for more
information) but the most obvious one is file upload. Take the following
<FORM ENCTYPE="multipart/form-data" ACTION="http://<vulnerable
<INPUT TYPE="hidden" name="MAX_FILE_SIZE" value="10000">
PHP File to be executed: <INPUT NAME="goto" TYPE="file">
<INPUT TYPE="hidden" NAME="cfgServers[host]" VALUE="hello">
<INPUT TYPE="hidden" NAME="server" VALUE="000">
<INPUT TYPE="hidden" NAME="btnDrop" VALUE="No">
<INPUT TYPE="submit" VALUE="Send File">
If saved into a file and loaded into a web browser it brings up a form
asking for a file containing PHP code to be executed on the remote web
server. The user can click the 'Browse' button and pick any file they wish.
When the user clicks 'Send File' that file is uploaded to the remote web
server. As default PHP functionality, it automatically accepts that file
(even though sql.php does not process file uploads) and saves it on the
local disk of the web server, it then sets the location of the file in the
variable $goto (e.g '/tmp/phpxXuoXG') and sets the variables $server,
$btnDrop, $cfgServers as needed for the exploit. All the tests are
again passed but now instead of reading a file that was already local the
local file is one the attacker has just uploaded. If the file contained the
following for example:
a directory listing of the /etc/ directory on the remote web server would be
returned to the attackers web browser. Obviously any command could be
specified and further exploit code could be uploaded and executed as
described in 'A Study In Scarlet'.
The attacker can also gain further assistance by reading the contents of
config.inc.php. In Advanced authentication installations it contains
database credentials for each database to be administered using phpMyAdmin.
The credentials must be able to read the priviliges in the mysql database.
This means allows an attacker to easily gain access to the encrypted
password hashes of all the users on each MySQL installation. Further, most
installations actually place the MySQL root user credentials in this file to
save effort of creating a new user with select privileges on mysql.*.
The attack on phpPgAdmin is a slight variation on the one detailed above.
This is because phpPgAdmin is based on an older version of phpMyAdmin. The
attacker simply needs to set $LIB_INC to 1 to prevent lib.inc.php being
included at all without having to fool the application into believing the
user is yet to select a server. That is, an attack like the following works:
As always with PHP there are many caveats to the attacks details in this
advisory based on PHP configuration and version. I'm not going to go into
detail discussing those here. Suffice to say this is a bug and it is usually
Development of phpMyAdmin has been continued by an independent and
unauthorized (as yet) group of developers who have released a new version
that contains fixes for this problem. You can upgrade to their version
If you want to continue running the last official version of phpMyAdmin
(2.1.0) please apply the SecureReality patch as described in SRPRE00001 at:
The developers of phpPgAdmin have patched later versions to fix this
problem. Please download the fixed version from:
Our thanks to the developers of the unofficial phpMyAdmin project and Dan
Wilson from the phpPgAdmin project for applying our fixes to their releases.
Advice, directions and instructions on security vulnerabilities in this
advisory do not constitute: an endorsement of illegal behavior; a guarantee
that protection measures will work; an endorsement of any product or
solution or recommendations on behalf of Secure Reality Pty Ltd. Content is
provided as is and Secure Reality Pty Ltd does not accept responsibility for
any damage or injury caused as a result of its use.