sábado, 20 de julio de 2013

Seguridad PHP - Robo de sesiones

Introducción

Antes de nada conviene recordar el funcionamiento de las sesiones y las cookies de sesión explicados en anteriores tutoriales.
PHP tiene soporte nativo para trabajar con sesiones. Cada sesión viene identificada  un id aleatorio (32 caracters hexadecimales), que se crea la inicializar dicha sesión con la función session_start(), y que se almacena en una variable llamada PHPSESSID. Este id es esencial ya que cuando creamos una sesión, se crea un array superglobal que almacenará la información de la sesión. Y éste se almacena en el servidor, en un archivo temporal cuyo nombre está formado por 'sess' + dicho ID de sesión (depende de la configuración de php.ini).
Ahora necesitamos que el cliente tenga una forma de hacer peticiones al servidor y que
este sepa que se está comunicando en un contexto de sesión determinado. O dicho de otra forma el servidor tiene que saber que las peticiones del cliente pertenecen a la sesión.

Esquema intercambio cliente-servidor de cookie de sesión

En una comunicación bidireccional hay dos posibilidades para la comunicación de identificador de sesión:

1- Si el cliente tiene las cookies activadas. En él se crea una cookie con nombre PHPSESSID (modificable en php.ini) y cuyo valor es el identificador de 32 caracteres hexadecimales. Recordemos que cada una de las cookies de un sitio web es una cadena: una lista de parejas "nombre, valor" separadas por punto y coma. La cookie se crea en el cliente gracias a que el servidor, tras haber creado la sesión, envía la primera respuesta con la cookie en su cabecera:

 HTTP/1.1 200 OK  
 Date: Sun, 12 Sep 2010 22:46:25 GMT  
 Server: Apache/2.2.15 (Win32) PHP/5.2.13  
 X-Powered-By: PHP/5.2.13  
 Set-Cookie: PHPSESSID=q75n4oq6turdv67uohqkbl7lv5; path=/  
 Expires: Thu, 5 Sep 2013 08:52:00 GMT  
 Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0  
 Pragma: no-cache  
 Content-Length: 2031  
 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>Página 1</title>  
     ...

A partir de aquí y siempre dentro de la duración de la sesión, el navegador acompañará esta cookie con las solicitudes de páginas que se hagan a ese mismo servidor. Y este último comprobará que el identificador existe y es válido recuperando la sesión de entre sus ficheros donde se almacenan las sesiones.

2- Si el cliente (navegador) tiene las cookies desactivadas. PHP dispone de una opción alternativa que permite la propagación a través de la URL. Para garantizar la propagación de las sesiones aún cuando el navegador esté configurado para no aceptar cookies. Por lo tanto se tendrá que pasar el PHPSESSID junto con las llamadas a las páginas siguientes. Y para ello tenemos 2 opciones:
- Añadir el PHPSESSID manualmente cada vez que creamos un enlace:

 <a href="page.php?PHPSESSID=<?php echo htmlspecialchars(session_id()); ?>">haga click</a>  

- Dejar que PHP lo añada automáticamente. Si modificamos en el php.ini la variable session.use_trans_sid a True, PHP añadirá automáticamente el PHPSESSID a las URI's de tu sitio web de manera transparente al desarrollador. Siempre que la sesión no haya expirado y que las cookies del cliente estén desactivadas.
Y así, cuando el cliente envíe nuevamente la petición al servidor a través de una URI con PHPSESSID, se podrá coger mediante el array $_GET para comprobar que no ha habido cambio.
Esta opción viene desactivada por seguridad ya que estamos mostrando dicho id en la url.
También se puede activar en tiempo de ejecución, en el script evidentemente, mediante

 ini_set('session.use_trans_sid', true);  


Problemas de seguridad con PHPSESSID

El mayor problema con el envío del id de la sesión de un lado a otro, ya sea en las cabeceras o en la url, es que es susceptible a ser interceptado y con ello  la posibilidad de acceder a un servidor de forma ilegítima.
Podría ser interceptado si alguien escanea o monitoriza el tráfico de la red. Y debido al fuerte crecimiento de las redes WIFI con débiles protocolos de encriptación, hacen que el apropiarse de los datos de sesión sea más sencillo.
Además, la posibilidad anteriormente comentada de incluir el PHPSESSID en la url hace que incluso los usuarios despistados revelen ellos mismos el id. Ya que es posible que al copiar y compartir un enlace, estén enviando información de su sesión. Y esto sucede más de lo que uno puede pensar.

Pero no solo es posible interceptar el PHPSESSID mediante un escaneo o monitorización. También es posible a través de un proxy que actué entre el cliente y el servidor y capturar la información de la sesión. Y esto podría ocurrir mediante un mensaje cuyo enlace no lleva al sitio de confianza que se presupone. Si no que lleva a un servidor (hace de proxy) que se encarga de redirigir al sitio legítimo y capturar la información de las cabeceras donde puede haber alguna cookie de sesión.

Ataque por fijación de sesión

Como hemos estado comentando, cuando un usuario se autentica en dicha aplicación el valor de la sesión que se creará no cambiará en toda la sesión (si no lo cambiamos nosotros). Por tanto, se puede atacar intentando que la víctima se autentique en la aplicación usando un ID de sesión previamente creada por el atacante. De tal forma que una vez el usuario se autentique, el atacante pueda acceder a la aplicación con el mismo rol que la víctima gracias a que conoce la sesión de ésta.

Este ataque es posible gracias a uso de PHPSESSID en las las urls y a programar el comportamiento de las sesiones de una forma equivocada.

Ejemplo

Vamos a ver un ejemplo donde un mal uso de una función puede provocar un fallo de seguridad.
Con la función PHP session_id() hay que tener cuidado ya que ofrece la posibilidad de cambiar el identificador actual de la sesión si se le pasa como parámetro dicho identificador. Pero para que funcione este cambio tiene que ir antes de iniciar la sesión con la llamada a session_start().Y esto puede ser aprovechado por los atacantes para intentar que un usuario registrado se identifique y sin saberlo cree un PHPSESSID conocido por ellos. Y así poder suplantarlo.

Imaginad que un atacante, conociendo el uso de id de sesión en la url por parte del servidor, proporciona este enlace para ver si lo usa algún usuario registrado en dicho servidor y que tenga desactivado el uso de cookies (recuerda que el servidor que haga uso de PHPSESSID en url primero comprueba que las cookies están desactivadas en el cliente):

 www.ejemplo.com/index.php?action=login&PHPSESSID=1234  

Pues si ese usuario se identifica en el servidor y resulta que el servidor ejecuta algo parecido a session_id($_GET['PHPSESSID'])  antes de una llamada a  session_start(), se acaba de crear una sesión con el id 1234. Y como ese ID lo conoce el atacante, se puede apropiar de la sesión. Y utilizarla para identificarse en el servidor y suplantar al usuario legítimo.
La utilización de $_GET['PHPSESSID']  solo tiene sentido si se transportan PHPSESSID por la url. Si el servidor solo utilizará cookies no tendría sentido que en algún momento se capturase de variable anterior.

Por lo tanto este ataque (el del ejemplo) no podría llevarse a cabo si solo se transmite el PHPSESSID por cookies. Pero aún está el problema de la interceptación de la comunicaciones (proxy o escaneo) y utilizar la cookie de sesión para identificarse.

Opciones para prevenir ataques

A) SSL

Muchos sitios web usan SSL únicamente para encriptar contenido de formularios, como podría ser el de login. Sin embargo también sería muy aconsejable usar SSL para encriptar toda la información que se transmite entre las partes de la comunicación (cliente-servidor). Ya que encriptando la PHPSESSID podríamos protegernos de dos de los tipos de ataques comentados en el apartado anterior:
-Por un lado nos protegeríamos gracias a la encriptación de posible atacantes que escaneando o monitorizando el tráfico de la red buscan datos de sesión.
-Y por otro lado nos protegeríamos de posibles redirecciones a proxys inesperados. Ya que al establecerse la comunicación entre el cliente y el servidor fraudulento (autofirmado con su certificado fraudulento), el navegador (cliente) nos avisaría que el certificado de dicho servidor es desconocido.

Evidentemente el problema de este mecanismo de seguridad es su coste (certificados de clave pública firmado por entidad certificadora). Por lo que vamos a presentar otros mecanismos para proporcionar una seguridad decente sin coste adicional. Pero que no es completa al cien por cien. Ya que el la seguridad completa ante proxies y escaneos solo la proporciona el uso correcto de SSL.

B) Tiempo de expiración en la sesión

Esto tiene poca historía. Si definimos un tiempo de expiración de sesión muy alto estamos dando más tiempo al usuario 'malicioso' a que pruebe y desarrolle sus ataques.
El tiempo de vida de la 'cookie de sesión' lo podemos especificar mediante la siguiente función:

 ini_set("session.cookie_lifetime",120); //El tiempo viene dado en segundos  

Es posible que intentéis modificar el tiempo de vida de una cookie y no funcione. Hay que tener en cuenta que es posible que vuestro equipo siga teniendo una cookie activa, por lo que hasta que no caduque la anterior no se guardará la nueva con el tiempo de vida modificado.
Pero este cambio sólo afecta la vida de la cookie y la sesión puede ser todavía válida. Es tarea del servidor invalidar una sesión, no del cliente. Así que como lo que queremos es que expire la sesión en el servidor tenemos que buscar una alternativa mejor.

Una buena solución sería utilizar marcas de tiempo. En el siguiente ejemplo destruiremos las sesión si han pasado mas de 30 minutos desde la identificación del usuario.
Tras hacer login y al crear la sesión nos guardariamos la tiempo Unix del último acceso:

 $_SESSION['LAST_LOGIN'] = time(); // última identificación  

Y podríamos introducir la siguiente función para saber si tenemos que cancelar la sesión o no por haber pasado el tiempo límite.

 function tiempo() {  
   if (isset($_SESSION['LAST_LOGIN']) && (time() - $_SESSION['LAST_LOGIN'] > 1800)) {  
     if (isset($_COOKIE[session_name()])) {  
       setcookie(session_name(), "", time() - 3600, "/");  
       //limpiamos completamente el array superglobal    
       session_unset();  
       //Eliminamos la sesión (archivo) del servidor   
       session_destroy();  
     }  
     header(...) //redirigir al punto de partida para identificarse  
     exit;  
   }  
   //...  
 }  


C) Cookies vs PHPSESSID en la URL

A partir de PHP 4.2, la variable  session.use_trans_sid en el php.ini esta a false por lo que por defecto el uso de PHPSESSID automático en las urls, por parte de php, esta desactivado. Y únicamente se usarán cookies para la comunicación del PHPSSID.
Tambien podemos usar este código en entornos donde  session.use_trans_sid este activo y queramos desactivarlo

 ini_set('session.use_only_cookies', true);  
 ini_set('session.use_trans_sid', false);  

Recuerda que usar cookies no evitaba ataque mediante proxies o escaneos.


D) Regenerar ID de sesión 

Una manera de proteger contra ataques provocados por el posible de robo del identificador de sesión es mediante una simple  regeneración de dicho id. De está manera un atacante no podría utilizar el id apropiado ya que no sería el mismo que el generado por el servidor.
Hay que pensar que el robo de id de sesión solo es útil para el atacante después de que el usuario se haya logeado y se haya creado la sesión. Por lo tanto es un buen momento para regenerar dicho id.en tiempo de ejecución:

Por ejemplo:

 <?php  
 session_start();  
 if (!isset($_SESSION['inicializada']))  
 {  
   session_regenerate_id();  
   $_SESSION['inicializada'] = true;  
 }  
 ?>  

Esto nos protegería ante la fijación de la sesión ya que el id que pretendía usar el atacante, forzando al usuario a crear una sesión con él, habrá cambiado.
Aunque evidentemente, no solucionará nada si intercepta todas las comunicación. Ya que todos los cambios de cookies de sesión viajan por las cabeceras de la peticiones y respuestas.

E) User-Agent

Una forma de incrementar la seguridad es mediante el uso del user-agent. En las cabeceras de cada mensaje entre cliente y servidor además de la cookie con el PHPSESSID también se especifica el user-agent. Que incluye información como la versión del navegador del cliente que está haciendo las peticiones. Pues podemos utilizar esta información para que aunque se apropien del PHPSESSID, los atacantes necesiten las mismas condiciones que indica  el user-agent.
Por lo que podríamos guardarlo tras identificar al usuario y posteriormente hacer comprobaciones para cada para cada script.

 <?php  
 /*login.php*/  
 ...  
  if($existeUsuario){       
      session_start( );  
      $_SESSION['user_id']     =     $data['user_id'];   
      $_SESSION['first_name']     =     $data['first_name'];  
      $_SESSION['agent'] = sha1($_SERV['HTTP_USER_AGENT']);  
      header("location:home.php");  
  }       
 ... 

 <?php  
 /*home.php*/  
 session_start();  
 if (isset($_SESSION['agent']))  
 {  
   if ($_SESSION['agent'] != sha1($_SERVER['HTTP_USER_AGENT']))  
   {  
     exit;  
   }  
 }  
 ...  
 ?>  

Sin embargo hay que tener cuidado ya que por ejemplo Internet Explorer, al usar el modo compatibilidad, puede cambiar su usar-agent.

Conclusión 

La forma eficaz de combatir el robo de sesiones es mediante la combinación de las técnicas explicadas.
- Usar SSL para evitar que les sirva de algo la información interceptada por lo atacantes.
- Tener el servidor configurado correctamente (no permitir PHPSESSID en url, expirar sesiones ....).
- Regenerar sesiones para que si alguien ha obtenido en algún momento un identificador de sesión no pueda utilizarlo. Ya que el identificador habrá cambiado con la regeneración y el que tiene el atacante no es válido.


Entradas relacionadas

Sesiones en PHP

Cookies en PHP

1 comentario:

  1. Execelente Post Dios te bendiga..! justo lo que buscaba, saludos desde Ecuador

    ResponderEliminar