Session Hijacking

Introduction

The idea behind HTTP sessions is that a user can store their information on any given site in between HTTP requests. The most common method of storing this user information is to use cookies to store a "session id," which itself is stored in the database on the remote site. When a user returns to any page on that site, the site can access the cookie for the domain, giving the site access to the user's current session.

The security breach involving sessions is when sessions are improperly authenticated or hidden such that an arbitrary user can either access information not meant for their permissions level or, using a technique known as "session stealing," fabricate their way into another user's session and data.

This is a walkthrough of session stealing in a simplified environment to simulate the stealing of other users' sensitive data. For simplicity, all the source code to the site and database setup has been provided.

The target website is http://netsec-projects.cs.northwestern.edu:2000/session.php and the source code for the site is at session_hijack.tgz.

In order to visit the webpage from the local computer you can run the following command

ssh -L 8000:netsec-projects.cs.northwestern.edu:2000
<username>@netsec.cs.northwestern.edu

and then simply visit http://localhost:8000/session.php.

Database Layout

First note the layout of the sessions table itself:

+----------+-------------+------+-----+---------+-------+
| Field    | Type        | Null | Key | Default | Extra |
+----------+-------------+------+-----+---------+-------+
| date     | datetime    | NO   |     |         |       | 
| alias    | varchar(10) | NO   | PRI |         |       | 
| user_tag | varchar(23) | YES  |     | NULL    |       | 
+----------+-------------+------+-----+---------+-------+

There are three fields here, each of which is important in its own way. The user_tag field holds what we call "sensitive data." As you can see by the definition, the default value is NULL so only some accounts will probably have information we want to access.

Look at the following lines from setup.php:

$admin_tag = uniqid("", TRUE);
$query = "INSERT INTO sessions (date, alias, user_tag) VALUES ('$sqldate', 'admin', '$admin_tag');";

So now we know what account's information we are looking for: admin. Let's see what other information we can learn about the admin account.

Here's something, in a different pattern: these accounts don't look interesting at all.

<?php
// Generate a ton of dummy user accounts
for($i=0; $i<1000; $i++) {
    $rand_alias = rand_string(8);
    //echo("$rand_alias\n");
    $query = "INSERT INTO sessions (date, alias) VALUES ('$sqldate', '$rand_alias')";
    $phptime += rand(1,10);
    $sqldate = date('Y-m-d H:i:s', $phptime);
    $result = mysql_query($query);
    //echo("$query");
}

And neither do these:

// Let's add a whole bunch of filler accounts here
for($i=0; $i<100; $i++) {
    $rand_alias = rand_string(8);
    $query = "INSERT INTO sessions (date, alias) VALUES ('$sqldate', '$rand_alias')";
    $phptime += rand(1,10);
    $sqldate = date('Y-m-d H:i:s', $phptime);
    $result = mysql_query($query);
    //echo("$rand_alias\n");
}

They do have something in common though -- notice that all of the rows are indexed by both alias and date. The filler and dummy accounts both use dates based on the previous date. More precisely, each account is

    $phptime += rand(1,10);

between 1 and 10 seconds after the previous time. However, this doesn't really help us unless we know where to start (i.e., what the initial time is). Fortunately we can identify this starting time.

// Creating the "startup" entry
// This is simply a dummy entry to simulate the time when
// the last time the server was restarted
$phptime = time();
$sqldate = date('Y-m-d H:i:s', $phptime);
$query = "INSERT INTO sessions (date, alias) VALUES ('$sqldate', 'startup');";
//echo("$query\n");
$result = mysql_query($query);
$phptime += rand(1,10);
$sqldate = date('Y-m-d H:i:s', $phptime);

Here we see the database has another special account: startup. Startup seems to be the first account in the database and also holds the initial timestamp for the sessions table. If we have the startup timestamp it may be possible to predict or calculate the admin timestamp.

However, we don't have direct access to the database. Let's try navigating to the website and see what info we can find.

The Site

Notice that the site is split into two parts. On the left is what looks like an "uptime" printout, which lets us know when the server was last restarted. The way this is determined can be found in the session.php file:

$query = "SELECT date FROM sessions WHERE alias='startup';";
$result = mysql_query($query);
$post_date = mysql_result($result, 0, 'date');
$phptime = strtotime($post_date);
$dif = time() - $phptime;

$hours = intval($dif / 3600);
$mins = intval(($dif - 3600*$hours) / 60);
$secs = intval(($dif - 3600*$hours - 60*$mins) % 60);

echo("$hours hours, $mins mins, $secs seconds.");

Once again we find the startup account. However, now we have a method of accessing that first date -- session retrieves the startup date by executing a SQL query injection and then formatting it into an hours/minutes/seconds format.

Unfortunately, unless we find a way to gather account info the startup time is not really useful. Let's take a look at the second section of output code:

  <?php
                $session_id = $_COOKIE["session_id"];
                if ($session_id) {
        $mysqldate = date('Y-m-d H:i:s', $session_id);
        $query = sprintf("SELECT alias,user_tag FROM sessions WHERE date='%s';",
            mysql_real_escape_string($mysqldate));
        $result = mysql_query($query);
                    echo(sprintf("Logged in as %s", mysql_result($result, 0, 'alias')));
        $tag = mysql_result($result, 0, 'user_tag');
    ?>
        <form action="logout.php" method="post">
            <input type="submit" name="logout" value="Log Out" />
        </form>
        <p>This tag's account is <?php 
        echo(strlen($tag) > 0 ? '$tag' : 'NULL'); ?>.
        </p>
    <?php   
                } else {
    ?>
        <p>
                    Please enter your alias. The default alias is "test."
        <form action="login.php" method="post">
            Alias:  <input type="text" name="alias" />
            <input type="submit" name="Submit" />
        </form>
        </p>
    <?
        }
    ?>

Now this may be interesting. The first thing that should be noticed is that the code does one of two things depending on whether or not a cookie has been set in the browser. Now, because we haven't yet set any cookies in the browser we can be guaranteed that the 'Null' case will be executed. Here's the code for just that case:

    ?>
        <p>
                    Please enter your alias. The default alias is "test."
        <form action="login.php" method="post">
            Alias:  <input type="text" name="alias" />
            <input type="submit" name="Submit" />
        </form>
        </p>
    <?
        }
    ?>

This gives us at least a place to start. The site says that the default alias is "test" so presumably we have permission to access it. So at this point enter "test" into the input box and press Submit. We will now be redirected to another PHP page: login.php.

include 'db_access.php';

$us_alias = $_POST['alias'];

if ($us_alias == 'test') {
    $query = sprintf("SELECT UNIX_TIMESTAMP(date) FROM sessions WHERE alias='%s';",
            mysql_real_escape_string($us_alias));
    $result = mysql_query($query);

    if($result and mysql_num_rows($result)) {
        $date_test = mysql_result($result, 0, 'UNIX_TIMESTAMP(date)');
        setcookie("session_id", $date_test);
    }
}

The first thing to notice is that this doesn't do anything if the inputted alias is not "test." Unfortunately, this doesn't bode well for shortcutting and logging in as admin directly. However, if we do login the server proceeds to set a cookie such that variable session_id=date. In fact, we can easily view this value after we login. In order to do so, simply enter the following code into the address bar of your browser:

javascript:alert(document.cookie);

A window will pop up -- look for "session_id=" in the result. Notice that the value is a fairly large number; doesn't quite look like a date, right? This is the UNIX form of a date. It is represented as the seconds since the UNIX epoch (January 1st, 1970). Here's the trick, though; we can change the cookie. Moreover, we can predictably change the cookie.

Now we know all we need to to write the exploit.

Lab

Now you must write a Python script to hijack the admin session and grab the user tag. Here are some tips to get started:

* Python has two modules you probably want to lookup: re
 [re](http://docs.python.org/library/re.html) and urllib2
 ([](http://docs.python.org/library/urllib2.html))
* You can use the startup date to find the value of the cookie where you
  need to start checking.
* The session page is guaranteed to only contain the string "admin" when you
  are logged into the admin account

When you successfully get the admin tag, alert an instructor and they can check you out.