Suplantación de sesiones

El tema de seguridad en la web es muy extenso y complejo. Requiere un profundo conocimiento del funcionamiento de todos los aspectos relacionados: servidores, navegadores, protocolos de comunicaciones, redes, etc. En esta página no pretendo mostrar un estudio exhaustivo, sino lo básico para empezar a enfrentarnos con este problema en relación con las sesiones. Veremos dos conceptos que se repiten mucho en lo que he leído: session hijacking y session fixation.

En los temas anteriores hemos visto para que sirve una sesión y cómo trabaja. Hemos indicado algunas cuestiones relacionadas con la seguridad de las sesiones. Es obvio que las sesiones sirven para algo más que lo que vimos en los ejemplos de los temas anteriores. Son especialmente útiles en el control de accesos a ciertos recursos protegidos de nuestro sitio, estableciendo un "canal" único entre el servidor y un determinado cliente.

Las sesiones se basan en la existencia del identificador de sesión que emplea el servidor para establecer la comunicación. Ese identificador está presente en el servidor y en el navegador, viajando por la red en uno y otro sentido con las peticiones y respuestas. Si en un momento dado hay una sesión abierta, un atacante que pueda hacerse con su identificador podría acceder al servidor sin que éste o el cliente se apercibieran, a no ser que hagamos algo para evitarlo.

El uso no deseado de un identificador válido de otro usuario es lo que se conoce como secuestro de sesión (session hijacking). Podemos decir que un indentificador válido es aquel que fue generado por un usuario legítimo mediante un script PHP y que identifica su sesión activa. Las sesiones siguen activas mientras no se eliminen de la carpeta Temp, por lo que incluso después de finalizar su vida podrían seguir activas si aún no han sido eliminadas por el mecanismo de control se sesiones. Hay tres formas de obtener un identificador de sesión válido:

  • Interceptando un identificador donde se almacene o fluya. Ya hemos visto que en un alojamiento compartido hay un riesgo de que otros sitios alojados puedan leer nuestros identificadores de sesión. En la propia red se pueden interceptar aunque para evitarlo se usan comunicaciones cifradas con el protocolo HTTPS. E incluso pueden leerse identificadores en el navegador del usuario.
  • Prediciendo cuál será el identificador de sesión para un usuario determinado. En PHP el identificador de sesión es un valor aleatorio que, según parece, no es fácil de predecir.
  • Usando la "fuerza-bruta" para iterar por todos los identificadores posibles hasta que demos con uno igual que alguno que se esté usando en ese momento. Como la longitud de los identificadores es mucho mayor que el número de sesiones simultáneas, parece poco probable que esto tenga éxito.

El objetivo final del atacante es suplantar al usuario, pues una vez que obtenga una sesión válida, podrá acceder a los mismos recursos del servidor que ese usuario. Hay otra forma de suplantar sesiones sin necesidad de conocer un identificador válido. Se trata en este caso de que el atacante emita un identificador previamente a que el usuario haya entrado en el sitio. El atacante entonces intentará que el usuario entre en el sistema con este identificador. Esto se le denomina fijación de sesión (session fixation), pues de alguna forma el atacante fija la sesión antes de que el usuario entre, a diferencia del secuestro, donde el atacante "roba" la sesión después de que el usuario haya entrado. En sentido general a veces decimos suplantación de sesión cuando en realidad queremos referirnos al hecho de suplantar a un usuario en el uso de una sesión, ataques que pueden conseguirse con las técnicas descritas del secuestro y la fijación.

Para realizar un ejemplo de suplantación y exponer los conceptos señalados de fijación y secuestro de sesión, realizamos en el siguiente apartado unas páginas de ejemplo de sesión con autenticación.

Ejemplo de sesión con autenticación

Es obvio que un ataque de sesión se llevará a cabo si el atacante obtiene algún beneficio. Un caso típico es poder acceder a recursos protegidos en el sitio sin que quede huella de su paso. El acceso a estos recursos suele requerir que el usuario canalizado por medio de una sesión sea también autenticado, es decir, sea identificado por medio de la comparación con una información de usuario que previamente tendremos almacenada en nuestro sitio. Por lo tanto hemos de evitar que el atacante suplante una sesión con autenticación, pues le permitirá acceder a esos recursos haciéndose pasar por el usuario legítimo.

El proceso de autenticación o autentificación en un sistema web podemos resumirlo como el modo de asegurar que el usuario que se conecta es quién realmente dice ser. Para ello debemos disponer en nuestro sitio de una copia de la contraseña para compararla con la que el usuario nos envía. Porque partimos de la base de que esa contraseña sólo será conocida por el servidor y el cliente. La autenticación no sustituye a la sesión, pues con una sesión abrimos un canal de comunicación con un único cliente, aspecto necesario para saber que sólo le estamos sirviendo a este usuario que inició la sesión y no a otro. Pero no podemos saber quién es ese usuario, para lo cuál necesitamos la autenticación.

En inglés se usa el término "login" (de "log in") traducido como "entrar o registrarse en un sistema" en un contexto informático, pues el verbo "log" significa registrar. De la misma forma "logout" para la acción de "salir de un sistema". He visto que se usa el verbo en español "loguear" como "autenticar", pero no está admitido en el R.A.E., al menos por ahora. Probablemente el clásico botón de "login" será más asequible para los usuarios no expertos que uno con la leyenda "autenticar" o "autentificar".

Además la autenticación da lugar dos figuras necesarias en un proceso de comunicación. Por una lado la autorización, proceso por el cual el servidor autoriza al usuario autenticado a acceder a ciertos recursos y luego tenemos la auditoría, con lo que el servidor registra los accesos de un usuario autenticado para resolver posibles incidencias.

El proceso de autorización se entiende mejor si tenemos planificado el acceso a los recursos en grupos según niveles de protección de esos recursos. Entonces el usuario de un cierto grupo que se autentique será autorizado a acceder sólo a los recursos a que tenga derecho. Cuando todos los recursos están al mismo nivel, no hay diferencia entre autenticación y autorización, pudiéndose prescindir de este último pero entendiendo que el proceso está implícito. Por otro lado, la auditoría nos permite realizar una investigación posterior de las incidencias que se hayan ocasionado en los accesos.

Para exponer un ejemplo lo más cercano a un caso real y luego aplicar los conceptos anteriores de fijación y secuestro de sesión, elaboramos un conjunto de 3 páginas con sesión y autenticación:

  1. autentica0.php: es una página de entrada sin sesión, donde se ofrece al usuario la posibilidad de entrar en una página con sesión, para autenticarse y acceder a recursos protegidos.
  2. autentica1.php: en esta página se inicia una sesión, ofreciendo al usuario un formulario de autenticación (nombre+contraseña), datos que de validarse le permitirán pasar a la siguiente página. También damos posibilidad para registrarse si no lo estuviera antes.
  3. autentica2.php: en esta página tenemos los recursos o activos de valor, es decir, la información protegida que sólo podrán ver los usuarios registrados.

También se dispone de una página con sólo html (acceso-indebido.html) para redirigir a un usuario cuando indebidamente intente acceder a la página protegida autentica2.php sin estar autenticado. En principio no es necesario conocer en profundidad el mecanismo de este conjunto de páginas para los apartados siguientes, pero si lo desea puede ampliar la información en el siguiente epígrafe (desplegable oculto) o bien ver todo el código completo del conjunto de páginas.

Explicación del funcionamiento de las páginas del ejemplo

Mejor que explicar con palabras como funcionan este conjunto de páginas, pondré un diagrama de flujo:

diagrama de flujo de una sesión PHP con autenticación

Cada página se separa en una zona con líneas de puntos grises. La página de entrada autentica0.php tiene script, pero no es relevante para el tema, pues se trata de una página de entrada para sesiones con cookies (ver el ejemplo propagar SID con sólo cookies). Los cuadros con fondo azul representan estados de la página a la espera de la interacción del usuario para realizar una petición al servidor. Esas peticiones se indican con flechas azules de puntos. El resto se refiere al flujo del script, donde las bifurcaciones se corresponden con las setencias if, con el color verde de las flechas para respuestas afirmativas y marrón para las negativas. Con flechas a rayas se indican redirecciones mediante header(). En rojo se indica donde podría darse un ataque al recurso protegido. Los cuadros de fondo amarillo señalan script necesario para evitar los ataques de fijación de sesión, mientras el de fondo verde es para evitar el secuestro. Se indica la página autentica3.php y siguientes que tienen la misma estructura que autentica2.php, aunque en nuestro ejemplo las obviamos. La página HTML acceso-indebido.html no tiene ningún interés, sólo muestra el mensaje del acceso indebido.

Hay dos únicas variables de sesión, que se inician así:

  • $_SESSION["estado"] = "sin_autenticar". Esta variable nos permite controlar la fijación de sesión. Si un atacante envía un identificador y la variable no existe, entonces no hay sesión y regeneramos el identificador. Cuando el usuario se autentica entonces ponemos en esta variable el nombre del usuario.
  • $_SESSION["identifica-usuario"] = md5($_SERVER["HTTP_USER_AGENT"]). En el inicio de sesión leemos el navegador que tiene el usuario. En siguientes accesos podemos comprobar si aún es el mismo, lo que nos permitirá detectar el secuestro de sesión.

En el código se manejan las siguientes variables, que exponemos para entender mejor las bifurcaciones del diagrama de flujo anterior:

VariableNos sirve para responder aNotas
$hay_sesion = isset($_SESSION["estado"])¿Hay sesión iniciada?La sesión se inicia en autentica1.php y luego se consulta en cada página protegida autentica2.php y siguientes.
$finaliza_sesion = isset($_POST["fin-sesion"])¿Finaliza sesión?Se recibe el botón submit del formulario de finalizar sesión, que está en todas las páginas.
$usuario_identificado = ($_SESSION["identifica-usuario"] == md5($_SERVER["HTTP_USER_AGENT"]))¿Se identificó al usuario?En cada acceso comprobamos si el navegador del usuario es el mismo que el que inició sesión.
$autenticado = ($_SESSION["estado"] != "sin_autenticar")¿Está autenticado?Cuando un usuario se autentica ponemos su nombre de usuario en esta variable.
$quiere_autenticarse = isset($_POST["autenticame"])¿Quiere autenticarse?Se recibe el botón submit del formulario de autenticación.
$nuevo_registro = isset($_POST["registrame"])¿Nuevo registro?Se recibe el botón submit del formulario de registro.
$quiere_registrarse = isset($_POST["quiero-registrarme"])¿Quiere registrarse?Se recibe el botón submit del formulario con un único botón para registrarse (y luego se le devolverá el formulario de registro).

La parte donde se almacenan y validan los usuarios y contraseñas se simula en este ejemplo, por lo tanto no debe servir como modelo de registro y autenticación, pues sólo lo voy a usar para experimentar aquí. Lo que parece más seguro es usar una base de datos para guardar los usuarios. Además está la cuestión de que la contraseña se recibe a través de HTTP, por lo que alguién podría leerla. Para esto hay que usar protocolos seguros (HTTPS).

Espero que este diagrama más los comentarios del código sean suficientes para entender el proceso, pero sin perder de vista que nuestro interés es ejecutar un ejemplo para observar la suplantación de sesión. Los siguientes apartados se han escrito basándose en la experiencia de la ejecución de ese conjunto de páginas en mi servidor localhost, con Apache+PHP, configurando las variables de control que se exponen a continuación.

Las páginas PHP de este ejemplo disponen al inicio del script de unas variables específicas de control para poder usar los ejemplos de suplantación de sesiones:

  • $solo_cookies (en todas las páginas PHP). Con valor true se realizará una propagación de SID con sólo cookies (ver propagar SID con sólo cookies). Con valor false propagaremos el SID con sólo URL (ver propagar SID con sólo URL sin trans_sid). Así se tratará de evidenciar el riesgo que tiene este método en las suplantaciones de sesión.
  • $evita_fijacion (en autentica1.php). Con valor true se regenerará el identificador de sesión para evitar la fijación de sesiones, efecto que podrá comprobar con valor false.
  • $evita_secuestro (en autentica1.php y autentica2.php). Con valor true realizará una identificación del usuario comprobando si su navegador es el mismo que usó cuando se inició la sesión. Con valor false podrá comprobarse el efecto del secuestro de sesión.

Ejecutando este ejemplo en línea desde este sitio estarán con valor true de forma permanente. Pero si queremos ver los efectos de la suplantación, hemos de ejecutar estas páginas en un dominio localhost para usar con un servidor Apache+PHP montado como local. Para ello puede descargar el conjunto de páginas comprimidas en paginas.zip. Las extensiones de los archivos ".php" las he modificado por ".txt", por lo que una vez descargado y descomprimido hay que hacer el proceso inverso, aparte de también poner aquellas variables a false para ver el efecto de la suplantación.

La fijación de sesión (session fixation)

Si ejecuta este ejemplo en su localhost, recuerde poner las variables de control así para poder ver el efecto:

$solo_cookies = false; (en las 3 páginas php)
$evita_fijacion = false; (en autentica1.php)
$evita_secuestro = false; (en autentica1.php y autentica2.php)

La fijación de sesión (session fixation) podemos definirla como la suplantación de usuario antes de que éste inicie sesión. El primer paso del atacante sería iniciar una sesión en el conjunto de páginas. Supongamos que esto lo hago en local con uno de los navegadores que tengo instalado en mi ordenador, como el Google Chrome. Después de ir a la página de entrada sigo el vínculo a la primera página autentica1.php y obtengo una pantalla con un formulario para autenticar:

autenticar

En primer lugar el atacante necesita mantener una sesión viva en el servidor. Además debe obtener el identificador de sesión lo que puede hacer de varias formas. Puede ver el código fuente HTML en el navegador para comprobar si está embebido en los vínculos. En nuestro ejemplo lo va a encontrar como campo oculto de los formularios. Recuerde que estamos en localhost usando el valor false para la variable $solo_cookies, por lo que los formularios se envían por GET con campos ocultos para el identificador de sesión, pues la propagación es por URL:

<form action="/como-se-hace/php-sesion/ejemplos/
    sesion-registrada/autentica1.php" 
method="get" style="border: gray solid 1px">
<input type="hidden" name="aseguraSesion" 
value="r96ol6v2ecorf76gf72d0ne135" />
</form>

También podría enviar el botón de autenticar con el formulario vacío y el servidor le responde otra vez con el formulario, acompañando el SID en la ruta que se expone en la barra de direcciones del navegador Chrome, donde podría copiar el identificador:

autenticar

Decíamos que debe mantener la sesión viva hasta conseguir el ataque. Recordemos que las sesiones tienen una duración de vida, por defecto de 24 minutos (ver tema II en el apartado de vida de una sesión). Si el usuario no interactúa con la sesión, esta se borra de la carpeta Temp si ya ha cumplido su vida útil desde la última interacción. El atacante puede mantenerla viva presionado periódicamente el botón de autenticar, aunque supuestamente usará algún programa que le permita esta acción de forma automática.

Mientras la sesión se mantiene viva, el atacante intentará que un usuario legítimo entre en el sistema usando su identificador. Debe entonces trasladarlo al navegador del usuario. Esta es la parte más compleja y caben varios métodos, incluso si se está usando propagación por cookies. En este caso estamos propagando por URL y es más sencillo presentar un ejemplo de traslado de identificador. Por ejemplo, supongamos que el atacante conoce de alguna forma el email del usuario. Podría enviarle uno con un mensaje HTML con este vínculo:

Tenemos una oferta atractiva para Usted. Si está interesado, por favor,
<a href="http://localhost/como-se-hace/php-sesion/ejemplos/
    sesion-registrada/autentica1.php?aseguraSesion=r96ol6v2ecorf76gf72d0ne135">
entre en el sistema</a>

El atacante presentaría el mensaje con lo necesario para engañar al cliente quién podría ya estar acostumbrado a recibir correos electrónicos del sitio donde esta registrado. Para emular en nuestro servidor local el acto del usuario pulsando sobre ese vínculo recibido en su email, copiamos el URL y lo trasladamos a la barra de direcciones de otro navegador, en este caso el Internet Explorer (este será supuestamente el navegador del usuario):

autenticar

Esto le conduce al usuario a entrar en una sesión iniciada previamente por el atacante, comprobándose que el identificador de sesión es el mismo que inició el atacante:

autenticar

El usuario no piensa que está siendo engañado, pues realmente está en la página del sitio. Así que se autenticará introduciendo su nombre de usuario y contraseña, tras lo cual el script le devolverá el vínculo con la página de recursos protegidos:

autenticar
Es evidente que por mucho que hagamos para protegernos la fijación de sesión, no estaremos conseguiendo nada si el atacante es capaz de robar el nombre de usuario y la contraseña que viaja sin cifrar por la red. Para evitarlo se usan protocolos seguros, pero aún así es necesario protegerse de la fijación.

A partir de ese momento, en algúno de los procesos automáticos que realiza el atacante pulsando el botón de autenticar, si el usuario ya se autenticó, entonces el atacante podrá entrar. Esto es lo que vemos si ahora pulsamos ese botón en el navegador Chrome, permitiéndole pasar a la página de recursos protegidos autentica2.php sin necesidad de conocer la contraseña de acceso:

autenticar
  • Usando JavaScript en el navegador del usuario para emitir la cookie. Para ello puede servirse de cross-site scripting (XSS) que consiste en aprovechar vulnerabilidades del código HTML del sitio para incrustar JavaScript y hacer un document.cookie="aseguraSesion=r96ol...".
  • Inyectando la etiqueta HTML <meta http-equiv=Set-Cookie content="aseguraSesion=r96ol..."> con alguna vulnerabilidad del servidor.

Afortunadamente PHP dispone de un mecanismo para evitar la fijación de sesión en cualquiera de las dos formas de propagación del identificador. En nuestro ejemplo, cuando el usuario engañado por el email entra en la página autentica1.php ya hay un identificador previo creado por el atacante. De hecho es una sesión completamente válida, con su archivo de sesión almacenado en Temp. Pero PHP no sabe que proviene de un ataque. Lo que podemos hacer es cambiar de identificador cuando el usuario sea autenticado. Eso se hace con la la función session_regenerate_id(true). Entonces PHP cambia el identificador y traspasa el contenido de las variables de esa sesión al nuevo, eliminando el archivo de sesión anterior. Ahora el usuario ya autenticado tiene un nuevo identificador, de hecho una nueva sesión, por lo que el atacante no podrá usarla. Si realiza el mismo proceso que antes, verá que tras la autenticación del usuario se genera un nuevo identificador. Luego en el otro navegador del atacante ya no será válido el identificador anterior, de hecho, no existirá y se le generará uno nuevo cuando pulse "autenticar".

Cuando con PHP hacemos session_start() y de alguna forma esa página ya porta un identificador de sesión entonces PHP lo adopta, independientemente de que lo haya generado anteriormente. Es lo que se llama un sistema permisivo, pues adopta cualquier identificador si no hay uno creado previamente. En oposición están los sistemas estrictos que sólo aceptan identificadores conocidos que fueron generados por el sistema en algún momento del pasado (como hace IIS de Microsoft). En los sistemas permisivos también existe la posibilidad de enviar un falso identificador con sesiones no inicidas. El atacante puede iniciar una sesión con un identificador como "1234" por ejemplo. Poniendo en la barra de direcciones del Chrome (navegador del atacante) esto:

http://localhost/temas/php-sesion/ejemplos/
    sesion-registrada/autentica1.php?aseguraSesion=1234

PHP genera una nueva sesión con ese identificador. El resto del proceso es igual que antes, enviarlo al usuario y esperar que este se autentique para tener luego acceso a esta sesión (siempre que no se regenere el identificador después de la autenticación).

Podemos evitar los falsos identificadores con sesiones no iniciadas si asociamos cada inicio de sesión con una variable de sesión que nos sirva para controlar el estado de la misma. Así usando una variable como $_SESSION["estado"], cuando el atacante entre por primera vez para registrar su falso identificador esta variable no existirá para ese identificador. Esto lo podemos ver en el diagrama de flujo, donde preguntamos si "¿Hay sesión iniciada?", que equivale a observar si la variable de sesión ha sido establecida: isset($_SESSION["estado"]). Entonces podemos hacer que PHP regenere el identificador. A partir de este momento el atacante se verá obligado a usar un identificador generado por PHP, que junto al método de regeneración tras la autenticación podrá evitar el ataque de fijación.

La norma general es que cuando se pase de un nivel de seguridad o otro más restrictivo, como puede ser una autenticación, se aconseja regenerar el identificador. No es bueno regenerarlo con excesiva frecuencia, por ejemplo con cada petición de una página, pues PHP estará ejecutando esa función y con muchos usuarios podrá recargar el sistema.

Secuestro de sessión (session hijacking)

Si la fijación de sesión es más díficil de entender que de solucionar, para el secuestro el problema se invierte. Secuestrar una sesión es suplantar a un usuario que ya ha entrado al sistema y por lo tanto existe una sesión activa. Las formas de "robar" su identificador de sesión se expusieron en el primer apartado de este tema. El problema es que si un atacante conoce el identificador después de ser regenerado tras la autenticación del usuario, nada le impide realizar una petición haciéndose pasar por el usuario legítimo.

En el ejemplo anterior usábamos propagación por URL que sabemos que tiene riesgos como cuando las páginas se almacenan en cachés con el parámetro del identificador. Si aún tenemos el ejemplo abierto con una sesión autenticada en autentica1.php, podemos pasar a la página protegida autentica2.php y copiar la URL de la barra de direcciones:

http://localhost/temas/php-sesion/ejemplos/sesion-registrada/
    autentica2.php?aseguraSesion=r96ol6v2ecorf76gf72d0ne135

Luego la trasladamos a otro navegador diferente y observará que podemos acceder también a esa página protegida, como hemos experimentado con el navegador Safari:

autenticar

De igual forma podría hacer cualquiera si encuentra esa dirección almacenada en una caché, como la que usan los buscadores. Esto se mitiga usando propagación con sólo cookies, pues el identificador únicamente viaja en una cookie y no se expone en la URL. Pero usar sólo cookies no evita este secuestro, pues el atacante puede aún robar una cookie de sesión en el navegador del usuario o en su fluir por la red.

Para verificar que es posible el secuestro de sesiones con cookies, podemos realizar esta suplantación en nuestro localhost. Para ello ponemos las variables de control del ejemplo de esta forma:

$solo_cookies = true; (en las 3 páginas php)
$evita_fijacion = true; (en autentica1.php)
$evita_secuestro = false; (en autentica1.php y autentica2.php)

Luego vamos a un navegador, por ejemplo Internet Explorer y, desde una sesión finalizada, iniciamos autentica0.php, luego seguimos el enlace a autentica1.php para autenticarnos y pasar a la página protegida autentica2.php. Copiamos la dirección de la barra

http://localhost/temas/php-sesion/ejemplos/
    sesion-registrada/autentica2.php

y los datos de la sesión que aparece en los resultados:

session_name(): aseguraSesion 
session_id(): i6p1b3ujomn9vqj4738t7nbta0

Hemos de imaginar que el atacante logró este SID de alguna otra forma, pero sea cual sea ahora podrá acceder a esa página en su navegador, supongamos el Google Chrome. Para ello puede usar una aplicación telnet con php y enviar esta petición al servidor:

GET /temas/php-sesion/ejemplos/sesion-registrada/
    autentica2.php HTTP/1.1
Host: localhost
cookie: aseguraSesion=i6p1b3ujomn9vqj4738t7nbta0
Connection: Close 

El servidor le responderá con el HTML de la página protegida autentica2.php, cuyo código verá en ese telnet-php:

HTTP/1.1 200 OK
Date: Sun, 07 Nov 2010 11:59:56 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4869
Connection: close
Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    <title>autentica2.php</title>

    <p>Ha entrado en este sitio <big><big>PROTEGIDO</big></big>
    con su nombre usuario: <big style="color: green">user</big>.
    </p>

    <li><code>$_COOKIE["aseguraSesion"]</code>:<code class="verde">
    i6p1b3ujomn9vqj4738t7nbta0</code></li>
    ...

Se observa que el atacante puede acceder a la página protegida enviando la cookie de sesión desde el telnet-php. Es importante observar el encabezado de la respuesta, con un valor 200 que significa en el protocolo HTTP que la respuesta ha finalizado con éxito. Por otro lado hemos ejecutado ese telnet-php en el navegador Chrome pero la cookie se envió desde telnet-php, no desde el navegador, por lo que no está almacenada ahí. Podemos comprobar en el navegador Chrome que no hay ninguna cookie de localhost almacenada. De hecho el PHP en el servidor la recibe pensando que proviene de un navegador que la tiene almacenada y que se generó en algún momento anterior al iniciar una sesión. En la repuesta no vuelve a enviar la cookie de sesión pues en la petición comprobó que el identificador coincidía con la que tenía almacenada en una sesión.

En cambio si volvemos a repetir el experimento pero antes poniendo la variable $evita_secuestro a true en las páginas correspondientes veremos que se puede intentar evitar. El procedimiento en este caso es almacenar una variable que identifique al usuario:

$_SESSION["identifica-usuario"] = md5($_SERVER["HTTP_USER_AGENT"])

Se trata de guardar la cabecera que envían los navegadores con su información, el User-Agent que viene a ser el agente de usuario que envío la petición. Esto lo hacemos en el inicio de sesión y con cada petición del usuario comprobamos que el navegador es el mismo. Hay otras cabeceras como Accept o incluso almacenar la IP del usuario, pero parece ser que no son tan fiables pues el usuario puede estarse conectando mediante mecanismos que modifican estas cabeceras, como los proxys, o incluso cuando las IP son dinámicas y pueden ser modificadas durante el transcurso de varias peticiones. Parece que incluso el User-Agent podría verse alterado en ciertos casos, no sirviendo tampoco para este cometido.

El tema no lo tengo muy claro en este aspecto. Sea como fuere vamos a utilizar el User-Agent como identificador de usuario. Probamos esta posibilidad finalizando la sesión actual en el Explorer (navegador del usuario) y volviendo a autenticarnos para llegar luego a la página protegida. Esta sesión ha generado el identificador o9o496qi2oo6abedgti31gedb3. En el telnet-php que tenemos abierto en el Chrome (navegador del atacante) modificamos la petición con el nuevo identificador que aparece en la nueva sesión del usuario en el Explorer:

GET /temas/php-sesion/ejemplos/sesion-registrada/
    autentica2.php HTTP/1.1
Host: localhost
cookie: aseguraSesion=o9o496qi2oo6abedgti31gedb3
Connection: Close

obteniéndose la siguiente respuesta en el telnet-php:

HTTP/1.1 302 Found
Date: Sun, 07 Nov 2010 12:02:06 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: aseguraSesion=deleted; expires=Sat, 07-Nov-2009 12:02:05 GMT; path=/
Location: http://localhost/como-se-hace/php-sesion/ejemplos/
    sesion-registrada/acceso-indebido.html
Content-Length: 0
Connection: close
Content-Type: text/html

En este caso el servidor envía una cabecera 302, lo que supone una redirección que se ha ejecutado debido a que hemos incluído un control para evitar secuestro, obligando a redireccionar a la página acceso-indebido.html en lugar de ofrecer la del recurso protegido solicitada autentica2.php. Además se observa que envió un borrado de la cookie de sesión tal como dispusimos en el script para destruir la sesión, que por supuesto no tendrá efecto pues no está en el navegador. En un navegador se redireccionaría a la página acceso-indebido.html, pero en este telnet-php que he escrito no se previsto la posibilidad de redirección. Aunque el propósito está conseguido, verificar que podemos evitar el secuestro de sesión.

Pero, por supuesto, el atacante podría enviar una cabecera User-Agent con el telnet usando la misma cabecera que el usuario (podría probar los User-Agent más frecuentes):

GET /temas/php-sesion/ejemplos/sesion-registrada/
    autentica2.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; ...)
Cookie: aseguraSesion=o9o496qi2oo6abedgti31gedb3
Connection: Close

Con el mismo User-Agent que tiene el usuario que se autenticó, el de Internet Explorer que hemos acortado por comodidad, podría llevar a cabo el ataque pues el servidor le devolvería la página protegida:

HTTP/1.1 200 OK
Date: Sat, 13 Nov 2010 19:47:06 GMT
Server: Apache/2.2.15 (Win32) PHP/5.2.13
X-Powered-By: PHP/5.2.13
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 4997
Connection: close
Content-Type: text/html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="es" xml:lang="es">
<head>
    ...
    <p>Ha entrado en este sitio <big><big>PROTEGIDO</big></big> con su nombre
    usuario: <big style="color: green">user</big>.
    ...

El problema aquí es que estamos usando dos cosas para identificar al usuario: el SID de la cookie, que el atacante nos ha robado, y el User-Agent que es un dato que el atacante también puede conocer. Al fin y al cabo cualesquiera otros identificadores que ideemos no dejan de ser otra cosa que sinónimos del SID. Si éste puede robarse, los otros también, pues ambos viajan por el mismo canal HTTP de comunicación a efectos de que el usuario se identifique. Una solución parece que pasa por HTTPS, donde toda la información fluye cifrada.