Thursday, September 8, 2011

suPHP chroot gotchas

Keep your nose out! 

Chrooting is not a proper security measure and was never intended as such. Nonetheless, when 'proper' chrooting is deployed in a multi-user environment (such as web hosting), it adds a layer of protection against gathering information about the underlaying system, and, more importantly, it stops spying on, or messing with, other users' files. How?

Well, let's take the example at hand (web hosting). If you are hosting dynamic web sites you have to give your users some way to run php/cgi scripts. Apache (mod_php) will run all of yours (and others') scripts as the unprivileged user. By doing so, and since the unprivileged user has to have access to all the hosted web sites, I can write a script to gather your database passwords, to delete all your files, to change all your links to point to some nasty stuff, etc.

This is, clearly, an unwanted situation. Ideally, you want to run a script process as the user (owner) of the script. To achieve this there exists a number of solutions:
I will not explain what they do or how they work since this is outside the scope of this post. I will only explain why I am going to use suPHP and how I am going to deploy it.

VHosts isolation

Hosting web sites can be thought of as managing a hotel. You rent rooms to guests. Apache+mod_php is like having no doors for your rooms. Guests are roaming freely almost anywhere.

suPHP & Co. are, out of the box, adding glass doors to your rooms. Guests can still have a look around, count how many rooms you have, gather the names of your other guests, map out the layout of your hotel, get in the kitchens, basement, etc.

You might not give a damn, but if you do, you'll have to put, at least, wooden doors in certain places.

Apache's ChrootDir

To avoid your guests getting into your basement you might want to deploy Apache's built-in chroot. This will confine them to the rooms' floors. Unfortunately, mpm-itk, which in my opinion is pretty promising in terms of process separation vs. performance, does not support Apache built-in chroot and has not any chroot mechanism of its own.

suEXEC, suPHP and mpm-peruser all support ChrootDir. The latter two have also their own chroot mechanism.

VHost isolation

What I want to achieve is complete isolation of every vhost. I do not want them to roam in the corridors. I want to keep them in their rooms. Because of this, I am left with suPHP or mpm-peruser and their chrooting mechanism. Since I am lazy right now, I will use suPHP which is readily available as a package for Ubuntu.

suPHP chroot

I will not cover how to install or set-up suPHP. There's about a million posts that do just that. I will only explain how to chroot a vhost using suPHP.

First of all let's set-up a vhost ready for chrooting. Let's say we want to host www.example.com.

# mkdir -p /var/www/vhosts/wwwexample/home/wwwexample/public_html
# chown 0:0 /var/www/vhosts/ -R
# chmod 755 /var/www/vhosts/ -R

I will explain later about the 'strange' directory tree. Now, let's setup a user.

# useradd -c "vhost admin" -b /var/www/vhosts/wwwexample/home \
    -s /dev/null wwwexample
# chown wwwexample:wwwexample /var/www/vhosts/wwwexample/home/wwwexample -R

We need to create a configuration file for this vhost and enable suPHP. Shown below is the part of the configuration file which deals with suPHP:

# vi /etc/apache2/sites-available/wwwexample.conf

-----8<-----
<VirtualHost xxx.xxx.xxx.xxx:80>
        ServerName www.example.com
        ServerAlias example.com
        DocumentRoot /var/www/vhosts/wwwexample/home/wwwexample/public_html

        php_admin_value engine off
        AddType application/x-httpd-php .php
        AddHandler application/x-httpd-php .php
        suPHP_Engine on
        suPHP_AddHandler application/x-httpd-php
        #suPHP_UserGroup wwwexample wwwexample
        suPHP_PHPPath /usr/bin
        
        ...

</VirtualHost>
----->8-----

Two gotchas:
  1. You cannot use suPHP_UserGroup with pre-built .deb packages
  2. You have to use application/x-httpd-php NOT application/x-httpd-suphp OR x-httpd-php alone
Let's modify suphp.conf to enable chrooting:

# vi /etc/suphp/suphp.conf

-----8<-----
;Path all scripts have to be in
docroot=${HOME}/public_html

;Path to chroot() to before executing script
chroot=/var/www/vhosts/${USERNAME}
----->8-----

HOME will be expanded to the home directory of the user running the script, USERNAME expands to, of course, the username of the user.

Jailtime rock

We need to build the chroot jail. I would suggest doing it by hand (ldd, strace and patience) because it is a great learning experience. This link will help you through the process of putting php in a jail.
I personally use a great tool: makejail. It eases the pain of running ldd and strace looking for clues as to why your jail does not work.

Makejail needs something to run its checks on. Let's prepare a php script:

# vi  /var/www/vhosts/wwwexample/home/wwwexample/public_html/mj.php

-----8<-----
<?php
  
   // mysql connection test - root is mysql superuser
   $link = mysql_connect("127.0.0.1", "root", "password");
   mysql_select_db("mysql");

   $query = "show tables";
   $result = mysql_query($query);
   echo "<h1>MySQL DB Test executed from ". $_SERVER['SCRIPT_NAME']. "</h1>\n";
   print "Script name: ". $_SERVER['SCRIPT_FILENAME'] ."<hr>\n";
   while ($line = mysql_fetch_array($result)){
      print "$line[0]<br>\n";
   }
   mysql_close($link);

   // file & directories testing - this should show the root of chroot
   if ($handle = opendir('/')) {
      echo "Directory handle: $handle\n";
      echo "Files:\n";

      while (false !== ($file = readdir($handle))) {
          echo "$file\n";
      }
      closedir($handle);
   }

   // try to open real /etc/passwd file
   print "<h1>Trying to open /etc/passwd file</h1>";
   displayFileContents("/etc/passwd");
   // try to open real /etc/hosts file
   print "<h1>Trying to open /etc/hosts file</h1>";
   displayFileContents("/etc/hosts");

   function displayFileContents($file){
      $f = @fopen($file, "r");
      if ( !$f ) { print "Error - Cannot open file <b>".$file."</b>"; return;}
      echo "<hr>File: $file<hr><pre>";
      while ( $line = fgets($f, 1000) ) {
          print $line;
      }
   }

   // test writing - check that the resulting file is owned by 
   // the caller of the script
   $myFile = "testFile.txt";
   $fh = fopen($myFile, 'w') or die("can't open file");
   $stringData = "Ipsum Lorem Blah Blah\n";
   fwrite($fh, $stringData);
   fclose($fh);

   //infos!
   phpinfo();

?>
----->8-----

It is not necessary to put all those things in the script, but I am running some tests as well.

Makejail needs a configuration script which will call our php script:


# vi /etc/makejail/php5.py

-----8<-----
chroot="/var/www/vhosts/wwwexample/"
testCommandsInsideJail=["/usr/bin/php-cgi /home/wwwexample/public_html/mj.php"]
----->8-----

The path of the script is relative to the new root.

Run makejail:

# makejail /etc/makejail/php5.py

After makejail has finished we should have a nice little jail built just right for our host.

# ls -lah /var/www/vhosts/wwwexample
total 36K
drwxr-xr-x 10 root root 4.0K 2011-04-04 16:41 .
drwxr-xr-x  4 root root 4.0K 2011-04-04 20:08 ..
drwxr-xr-x  2 root root 4.0K 2011-04-04 14:32 dev
drwxr-xr-x  4 root root 4.0K 2011-04-04 17:13 etc
drwxr-xr-x  3 root root 4.0K 2011-04-04 15:36 home
drwxr-xr-x  2 root root 4.0K 2011-04-04 16:41 lib
lrwxrwxrwx  1 root root    4 2011-04-04 14:32 lib64 -> /lib
drwxr-xr-x  2 root root 4.0K 2011-04-04 17:13 sbin
drwxr-xr-x  3 root root 4.0K 2011-04-04 16:41 sys
drwxr-xr-x  5 root root 4.0K 2011-04-04 16:41 usr

Does this structure reminds you of anything? Nice, but if we try now to connect to www.example.com/mj.php we will get the dreaded error 500 page.

suPHP will chroot to whatever directory you have chosen and then run the script, but the script path is not modified to the new root directory. So, we need to create some new directories and a symlink to solve this (annoying) problem.
# mkdir -p /var/www/vhosts/wwwexample/var/www/vhosts
# cd /var/www/vhosts/wwwexample/var/www/vhosts
# ln -s ../../.. wwwexample

Now, when suPHP will try to execute the script, from within the jail, it will follow our linking craziness without spitting out any error 500 page. The symlink re-creates the part of the path that we have cut off from the jail. You'd have thought that the suPHP people would do this for you. By the way, apache's own chroot suffers from the same problem. If you set the document root of a vhost relative to the chroot it will spit out a warning that the path does not exists.

Connecting now to www.example.com/mj.php will (again) disappoint us. No connection to the database!
If we have a look through the output of phpinfo() we'll notice that no additional configuration files have been parsed. That is because they have not being copied in the jail.
So, we need to copy anything we need from /etc/php5/conf.d to our homologous in the jail.

# cp --preserve=all /etc/php5/conf.d/* \ 
    /var/www/vhosts/wwwexample/etc/php5/conf.d/.

Then run makejail again:

# makejail /etc/makejail/php5.py

This time the jailed php version will pick up on those previously missing configuration files and makejail will correctly copy all the missing libraries/datafiles.

Connecting now to the site should display the tables in the `mysql` database alongside the directories at the top of the jail. It should say it cannot find /etc/passwd (not in the jail) but /etc/hosts is there (makejail puts it there the second time around, some php module needs it.) and if you look in public_html there should be a file named testFile.txt owned by wwwexample.
At this point, you should go through the files in the jail and see if makejail has copied something it should have not (very unlikely) or if it forgot something else. In any case, before running makejail, you should customise the configuration file to accommodate your specific needs. 

This was a rather rushed explanation, but it is mainly done to help me remember what I did so that it won't bite me in the ass.
If at all possible, mount /var/www on its own partition and set nosuid,nodev. This will secure a little bit more the jail. And of course, use a grsecurity kernel.

On a final note, suPHP runs as root since it needs to chroot. If there's a bug in its code lurking somewhere all bets are off since if an attacker can run a privilege escalation exploit, and successfully gains root status, then he's out of the jail and in the wild.

Chroot stops peeping toms, flashers and streakers. Some muggers too, but if Hannibal Lecter gets into your system, you'd better pray you did not swear by chroot alone or you'll be toast (for breakfast.) 

Happy jailing, or not.

---

Useful links:

Chroot best practices
Another chrooting primer
A heated discussion on slashdot

---

No comments:

Post a Comment