Session Hijacking
Welcome to another edition of Security Corner. This month's topic is session hijacking, often referred to as an impersonation attack. Session hijacking describes all methods by which an attacker can access another user's session. A successful session hijack attack exploits a flaw in the application; as PHP developers, the safeguard is our responsibility.
In an earlier column, I discussed session fixation, a method by which an attacker can gain a valid session identifier. The purpose of such an attack is to use this identifier to attempt to hijack a session. Thus, defending against session fixation helps to defend against session hijacking, but it only addresses a small part of the problem.
Capturing a Session Identifier
A more popular method of obtaining a valid session identifier is to capture it. There are many methods of capture, and these can be categorized according to the method used to propagate the session identifier. For example, if the session identifier is propagated as GET data, attacks focus on obtaining GET data, not specifically the session identifier.
This type of propagation is less secure than using a cookie, because GET data is more exposed. When possible, use a cookie to store the session identifier. Of course, this is just a defense in depth mechanism and should not be considered the primary safeguard.
It's a good practice to anticipate the worst case scenario. Thus, in this article, I demonstrate how to complicate impersonation under the assumption that the session identifier has already been captured. Of course, this is not a desirable situation, but every bit of complication for an attacker increases the security of the application.
Complicating Impersonation
If your session implementation consists of nothing but session_start()
, it is very susceptible to session hijacking. In order to discover a method that can help to prevent simple exploits, first consider a typical HTTP request:
GET / HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0
Accept: text/xml, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234
A cookie named PHPSESSID
is used to propagate the session identifier in this example.
Of the HTTP headers given in this example, only Host
is required, and it certainly isn't very unique. So, it may seem at first glance that nothing within the request can help to identify the user with any more assurance than with the session identifier alone. While this isn't entirely false, any consistency that can be found in each request from the same client can be used to complicate impersonation, and there are steps that can be taken to provide some of this consistency. To further illustrate this concept, consider the following request sent soon after the previous one:
GET /profile.php HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 (compatible; MSIE)
Accept: text/xml, image/jpeg, image/png, image/gif, */*
Cookie: PHPSESSID=1234
If every previous request from client 1234
used a different User-Agent
header, should this request not be treated with some suspicion? It's possible that this request is an impersonation attempt, and asking the user to verify the password is a good safeguard. The legitimate user will be able to provide the correct password and continue, but an attacker cannot.
The following example shows how you can add a simple check for this:
<?php
session_start();
if (isset($_SESSION['HTTP_USER_AGENT']) &&
$_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT'])) {
exit;
} else {
$_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
}
?>
Does this provide enough protection against impersonation? Not really. If the session identifier is being propagated in a cookie, consider that most cookie exploits involve the victim visiting the attacker's site. Thus, if the session identifier has been captured, it is reasonable to assume that the attacker also has access to the HTTP headers that the victim's client includes in a typical request. An impersonation attack simply has to reproduce all of these headers, and then any extra check that relies on any of these is rendered useless.
What if the MD5 of the User-Agent
header is used as a browser fingerprint that is propagated with every request, just like the session identifier? In order to bypass this extra safeguard, an attacker would not only have to reproduce the correct session identifier and User-Agent
header, but also the correct browser fingerprint. This requires an extra step, so it is more secure.
The weakness with this approach is that guessing the value of the browser fingerprint is not difficult. An MD5 is easily recognizable, and an attacker can use the application itself to get a sample one. With this, figuring out that it's the MD5 of theUser-Agent
header isn't too terribly difficult. However, consider a browser fingerprint that is generated with the following code:
<?php
$string = $_SERVER['HTTP_USER_AGENT'];
$string .= 'SHIFLETT';
$fingerprint = md5($string);
?>
With the addition of some secret padding (SHIFLETT
in this example), generating a valid browser fingerprint for someone else is more difficult. In fact, prediction becomes so difficult at this point that capturing a valid browser fingerprint is more likely to be the easiest route for an attacker to take.
To complicate the process of capturing the browser fingerprint, a different method of propagation should be used for the session identifier and browser fingerprint. If both are propagated as cookies, it is reasonable to assume that the same attack can capture both. The same is true if both are propagated as GET data. I recommend propagating the session identifier as a cookie and the browser fingerprint as GET data. If you rely on session.use_trans_sid
, then you can simply focus on including the browser fingerprint in each URL. Users who disable cookies will have both the session identifier and browser fingerprint propagated as GET data, but this cannot be avoided. Those who enable cookies will automatically have a bit more protection against impersonation.
Some experts warn against relying on the consistency of the User-Agent
header. The argument is that an HTTP proxy in a cluster can modify the User-Agent
header inconsistently with other proxies in the same cluster. While I have never observed this myself, it is definitely worth noting.
I have observed that the Accept
header can change for Internet Explorer users depending upon whether they refresh the browser to request a page, so this should not be relied upon for consistency.
If you are skeptical about relying on any consistency in the HTTP headers, you can opt to use a unique token rather than a browser fingerprint. More often than note, this is the approach I choose. To generate this token, use code similar to the following:
<?php
$token = md5(uniqid(rand(),TRUE));
$_SESSION['token'] = $token;
?>
This token should then be propagated with each request, using a different method than used to propagate the session identifier (just like the browser fingerprint). This token can also be frequently regenerated to tighten the window of opportunity for an attacker.
Until Next Time...
The purpose of this article, like many others here at Security Corner, is to give you enough information to develop solutions that fit you best. Hopefully you now have a clearer understanding of the types of session-based attacks that you must defend against and have a few ideas to get you started protecting your applications. Until next month, be safe.