WordPress session hijacking and prevention

In computer science, session hijacking is the exploitation of a valid computer session, sometimes also called a session key—to gain unauthorized access to information or services in a computer system. In particular, it is used to refer to the theft of a cookie used to authenticate a user to a remote server. It has particular relevance to web developers, as the HTTP cookies used to maintain a session on many web sites can be easily stolen by an attacker using an intermediary computer or with access to the saved cookies on the victim’s computer.1 In this article, we’ll successfully implement a session hijacking attack on a WordPress blog and show measures to prevent this kind of attack from happening.

All software that relies on cookie stored sessions is vulnerable if the cookies are sent over an insecure channel. If we take WordPress into acount, the default installation is vulnerable to a session hijacking attack (or the so called man in the middle attack). Before we start session hijacking, lets see how the WordPress login works. When you login, some cookies are stored on your local computer:

Wordpress session

WordPress stores the PHP session id (which is a cookie that closes when the browser is closed) and the most important one, the wordpress_logged_in cookie, which also contains a random key at the end. This is the cookie that if stolen, can be used to authenticate a computer that actually isn’t logged in. Now lets show how a session hijack attack works. Lets say we have two computers on the same wireless network (called WordPressNET), computer A and computer B as shown in this diagram:

Wordpress wireless network.

When computer A logs in, a HTTP request is made and the cookies are stored on A:

Wordpress login.

 

With each new request, the cookies are also sent. Because it is a wireless network, any computer on that network can sniff the requests and get the data if the data isn’t sent on a secure channel:

Wordpress session hijack.

 

HIJACKING A WORDPRESS SESSION

This is how it works in theory. Lets put this into practice. We’ll need some tools for the job:

When you’ve installed Wireshark, Firefox and the GreaseMonkey addon, copy the script from the url above, go into Firefox, click on the GreaseMonkey icon and select new user script. Select paste from clipboard. And we’re prepared. Instead of writing a  how-to on doing the attack, I’ve decided to make a screencast, which is more easier to follow (a note: the keyboard shortcut used when pasting the cookie data in Firefox is ALT+C).

 

 

PREVENTING WORDPRESS SESSION HIJACKING

To prevent these kind of attacks, you need to use HTTPS whenever possible (or only when the user is logged in). There is a simple plugin for managing these things called WordPress HTTPS (SSL) , but you can also do it manually. If this is a well known production website, make sure you buy yourself a certificate, if not, you can make a self-signed one:

openssl req -new -x509 -days 1826 -nodes -out server.crt -keyout server.key

Then you need to configure your server to run HTTPS. I can supply the instructions for nginx:

server {

        server_name your_domain;
        listen 443 ssl;
        root /path/to/root;
        index index.php index.html;
        ssl on;
        ssl_certificate /path/to/server.crt;
        ssl_certificate_key /path/to/server.key;

}

When the server is configured, edit the wp-config.php file by adding the following two lines in the end:

define('FORCE_SSL_LOGIN', true);
define('FORCE_SSL_ADMIN', true);

And now, your cookies are safe (but only in the administration area). When you are logged in and you go to the main page, you are still logged in but you are reverted to a HTTP connection. We still need to force WordPress to use SSL even if we are not in the administration area. I have written a simple plugin that does this called Force SSL Everywhere, that is available at the WordPress repository. Please read the instructions before using it. The source code contains two files, force-ssl-everywhere.php and cookie.php:

The first one is the plugin file itself that does all the work:

<?php

	/* 
		Plugin Name: Force SSL everywhere
		Plugin URI: http://wpplugz.is-leet.com
		Description: A simple plugin that forces SSL on all pages when logged in.
		Version: 1.0
		Author: Bostjan Cigan
		Author URI: http://bostjan.gets-it.net
		License: GPL v2
	*/ 

	add_action('init', 'force_ssl_everywhere_force_ssl_when_admin');
	add_action('auth_redirect', 'force_ssl_everywhere_check_force_ssl');
	add_action('wp_logout', 'force_ssl_everywhere_clear'); // Adding action to logout (clearing cookies etc.)
	add_action('wp_login', 'force_ssl_everywhere_add_auth', 10, 2);

	function force_ssl_everywhere_add_auth($user_login, $user) {
	
		$user_data = get_user_meta($user->ID, 'force_ssl_everywhere_id', true);

		$red_url = get_admin_url();
		
		if(!isset($_COOKIE['wp_force_id'])) {
			$force_id = force_ssl_everywhere_random_string(32);
			$id = force_ssl_everywhere_random_string(32);
			if(!is_array($user_data)) {
				$user_data = array();
				$user_data[$id] = $force_id;
				update_user_meta($user->ID, 'force_ssl_everywhere_id', $user_data);
			}
			else {
				$user_data[$id] = $force_id;
				update_user_meta($user->ID, 'force_ssl_everywhere_id', $user_data);
			}
			$red_url = plugin_dir_url(__FILE__).'cookie.php?force_id='.$force_id.'&id='.$id;
		}
		
		header("Location: $red_url");
		exit;
		
	}
	
	function force_ssl_everywhere_clear() {
		global $current_user;
		get_currentuserinfo();
		if(isset($_COOKIE['wp_force_id'])) {
			$user_data = get_user_meta($current_user->ID, 'force_ssl_everywhere_id', true);
			unset($user_data[$_COOKIE['wp_force_id']]);
			update_user_meta($current_user->ID, 'force_ssl_everywhere_id', $user_data);
		}
		setcookie("wp_force_ssl", "", time()-3600*24, COOKIEPATH, COOKIE_DOMAIN, true);
		setcookie("wp_force_id", "", time()-3600*24, COOKIEPATH, COOKIE_DOMAIN, true);
	}
	
	function force_ssl_everywhere_check_force_ssl() {	
		
		global $current_user;
		get_currentuserinfo();

		$user_data = get_user_meta($current_user->ID, 'force_ssl_everywhere_id', true);
		
		if(isset($_COOKIE['wp_force_ssl']) && is_user_logged_in() && is_ssl() && isset($_COOKIE['wp_force_id'])) {
			if(strcmp($user_data[$_COOKIE['wp_force_id']], $_COOKIE['wp_force_ssl']) == 0) {
				return true;
			}
			else {
				wp_logout();
			}
		}
		else {
			wp_logout();
		}

	}
	
	// Generate a random string	
	function force_ssl_everywhere_random_string($length) {
	    
		$characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
    	$string = '';
    	for($i=0; $i<$length; $i++) {
	        $string .= $characters[mt_rand(0, strlen($characters)-1)];
    	}
    
		return $string;

	}

	function force_ssl_everywhere_force_ssl_when_admin() {
	
		if(is_ssl()) {
			return;
		}
		else {
			if(is_user_logged_in()) {
				wp_redirect(
					'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] 
				);
				exit;
			
			}
		}
	}

?>

The second one is needed so that the cookies are set:

<?php

	$path = dirname(dirname(dirname(dirname(__FILE__))));
	require($path.'/wp-load.php');
	
	if(isset($_GET['redirect']) && is_ssl() && is_user_logged_in()) {
		$url = get_admin_url();
		header("Location: $url");
		exit;
	}

	if(is_user_logged_in() && is_ssl() && isset($_GET['force_id']) && isset($_GET['id'])) {
		$force_id = $_GET['force_id'];
		$id = $_GET['id'];
		if(strlen($force_id) > 0 && strlen($id) > 0) {
			$url = plugin_dir_url(__FILE__).'cookie.php?redirect=1';
			header("Location: $url");
			setcookie("wp_force_ssl", $force_id, NULL, COOKIEPATH, COOKIE_DOMAIN, true);
			setcookie("wp_force_id", $id, NULL, COOKIEPATH, COOKIE_DOMAIN, true);
			exit;
		}
		else {
			$url = get_bloginfo("wpurl");
			header("Location: $url");
			exit;
		}
	}
	else {
		$url = get_bloginfo("wpurl");
		header("Location: $url");
		exit;
	}

?>

I admit, it isn't a perfect solution, but it is a solution if you want to run on SSL only when logged in.

References:
  1. Session hijacking, Wiki. http://en.wikipedia.org/wiki/Session_hijacking  []

About this article