Limitar los accesos fallidos.

Resumen.

Ya hemos presentado dos formas de proteger nuestros sitios Web, una, los sistemas antispam, otra, el bloqueo de IP. Y como sabemos, los sistemas antispam pueden ser violados, y frente al bloqueo de IP, los ciberatacantes pueden cambiar de IP usando por ejemplo conexiones VPN. Esta última precisa una constante supervisión, si no es así, pues… Ahora vamos a ver cómo enfrentarnos a los ataques de fuerza bruta limitando el número de veces que se puede realizar una acción.

Artículo.

Se puede decir que el ataque de fuerza bruta consiste en repetir una acción hasta encontrar la solución correcta, un método perfecto para suplantar la identidad de un usuario cambiando las credenciales, básicamente, e-mail y clave. A partir de ese momento, el usuario legítimo pierde el control de la cuenta de usuario. Más allá de los métodos de control, como la validación mediante SMS a nuestro teléfono móvil, veremos ahora de qué manera enfrentarnos a este tipo de atacantes desde una perspectiva de programación.

Para realizar esta tarea lo primero que tenemos que hacer es contar el número de accesos fallidos que comete el usuario. Una vez superado ese valor, se bloquea la acción por un cierto período de tiempo (5 minutos, una hora, un día, o el tiempo que consideremos adecuado para nuestro caso concreto).

Esto lo podemos hacer de dos formas, una mediante el uso de cookies, y otra registrando los accesos fallidos en nuestra base de datos, pero esta ultima la veremos en otro tutorial.

Lo primero en que pensará cualquier programador es que si está basado en cookies, cualquiera se lo podrá saltar inhabilitando las cookies, y sí, tiene razón, pero también podemos bloquear las acciones para usuarios que no tengan habilitadas las cookies, con esto, problema solucionado, ya que salvo raras excepciones todo el mundo tiene habilitadas las cookies.

Como ya vimos en este artículo, podemos reconocer la legitimidad de un formulario mediante sesiones. Pues usaremos el mismo criterio para reconocer si el usuario tiene o no habilitadas las cookies.

El procedimiento es simple, creamos una cookie en el formulario y en la página que recibe los datos, tratamos de recogerla, si las cookies están habilitadas, recogeremos su valor. En caso contrario, no se recogerá y por tanto, no se procesará el formulario.

Además de crear una cookie, creamos una sesión con el mismo valor y enviamos el valor de estas dos, que es el mismo, mediante un campo oculto (hidden). De esta forma podemos comparar el valor generado en la variable $coki_ok mediante la función mt_rand() enviado por tres vías, cookie, POST y session. De esta forma, también acreditamos la legitimidad del formulario que envía los datos.

Con esto hecho, continuaremos con la misma lógica de reconocimiento de visto en el artículo de referencia, el único cambio es que en lugar de comparar dos variables, usuario y clave, también compararemos la variable $coki_ok y $sesion_coki.

En caso de que los datos de usuario y clave, además de la comparación de sesión y cookie sean válidos, procederemos de la misma forma que lo hicimos en el artículo referenciado.

Cuando los datos no son válidos es cuando procederemos a contar el número de intentos de acceso fallidos y proceder a realizar acciones para impedir los intentos de acceso reiterados. El código seria el siguiente:

lim_accesos.php
<?php session_start();
include('funcion_limitar_accesos.php');

@$coki_ok = $_COOKIE['coki_ok'];
$coki = $_POST['coki'];

if($coki_ok == $coki){
// El usuario tiene habilitadas las cookies

// Datos provenientes del formulario
$usumail = $_POST['usumail'];
$clave = $_POST['clave'];

$sesion_coki = $_SESSION['sesion_coki']; // Recuperamos sesion
@$bloqueo_aceso = $_COOKIE['n_aceso']; // Numero de accesos

// Datos de identificacion validos
$usuario_ok = 'aw';
$clave_ok = 'abc123';

if($usumail == $usuario_ok AND $clave == $clave_ok AND $coki_ok == $sesion_coki){

echo '<p>Usuario identificado correctamente</p>';

}else{
echo '<p>Usuario NO identificado</p>';

// Aplicamos la funcion de limitacion de intentos fallidos
$minutos = 1; // Tiempo en minutos que tendra bloqueada la identificacion
$ac_permitidos = 5; // Numero maximo de intentos de identificacion permitidos
$pag_form = 'form_cooki.php'; // Ruta del formulario de identificacion
$time_retro = 4; // Tiempo en segundos que tardara en devolver al formulario de identificacion

$resul_logeo = End_Login($usumail,$clave,$usuario_ok,$clave_ok,$minutos,$ac_permitidos,$pag_form,$time_retro);

if($bloqueo_aceso < 1){
echo "Error¡¡ Te quedan $ac_permitidos intentos";
}else{
echo "Error¡¡ Acceso $bloqueo_aceso de $ac_permitidos.";
}


echo "<p><a href=\"$pag_form\" title=\"Recargamos el formulario\">Volver al formulario</a> -- $resul_logeo</p>";
}
unset($_SESSION['sesion_coki']); // Eliminamos la sesion
}else{
// El usuario NO tiene habilitadas las cookies
echo '<p>AVISO: Habilita las cookies para poder identificarte.</p>';
}

?>


La función que hemos cargado puede resultar un tanto compleja, pero su lógica es muy simple. Tras capturar la IP del usuario y crear con ella una cookie, empezamos a hacer el recuento teniendo en cuenta el valor de esa cookie. Iniciamos el contador de accesos con la cookie en el valor cero y vamos incrementando de uno en uno.

Lo siguiente es, recuperando esa cookie, hacer las comprobaciones pertinentes de los valores de usuarios y claves válidas, así como de IP y recuento de accesos. Todo esto no hubiera sido preciso para el caso de un proceso de logueo, pero esta función fue creada con otros propósitos. El hecho de realizar estas comprobaciones no tiene mayor relevancia en cuanto a su operatividad, aunque resulta reiterativa.

Aclarado esto, la función es la siguiente:

funcion_limitar_accesos.php
<?php
// Esta funcion impide los ataques de fuerza bruta en los formularios de identificacion
// limitando el numero de veces que un usuario puede identificarse erronenamente.
// Tras el numero determinado de identificaciones NO validas, tendra que esperar un cierto tiempo,
// determinado en la funcion, para volver a identificarse

function End_Login($usuario_for,$clave_for,$usuario_bd,$clave_bd,$minutos,$ac_permitidos,$pag_form,$time_retro){
$N_usu = strlen($usuario_for);
$N_cla = strlen($clave_for);
$val_usucla = $N_usu * $N_cla;

if($val_usucla > 0){

$ip_usuario = $_SERVER['REMOTE_ADDR'];

setcookie('ip_usuario', $ip_usuario, time() + (60 * $minutos), '/');
@$ip_aceso = $_COOKIE['ip_usuario'];
if($ip_aceso == null){
$ip_aceso = $ip_usuario;
}else{
$ip_aceso = $ip_aceso;
}
setcookie('n_aceso', 0, time() + (60 * $minutos), '/');
@$n_aceso = $_COOKIE['n_aceso'];
if($ip_usuario == $ip_aceso){
@$n_aceso = $n_aceso;
}else{
@$n_aceso = 0;
}
setcookie('n_aceso', ++$n_aceso, time() + (60 * $minutos), '/');
@$total_acesos = $_COOKIE['n_aceso'];
if($total_acesos == null){
$total_acesos = 0;
}else{
$total_acesos = $total_acesos;
}
/* echo "1- $ip_aceso <br />
2- N $n_aceso <br />
3- T <b>$total_acesos</b> <br /><hr>"; */
if($usuario_bd == $usuario_for && $clave_bd == $clave_for && $ip_usuario == $ip_aceso && $total_acesos <= $ac_permitidos){
// Identificacion correcta
// Tras identificarse correctamente, el contador de accesos se reinicia a 0
setcookie('ip_usuario', $ip_usuario, time() + (60 * 0), '/');
// echo "1 Te has identificado correctamente <br />$total_acesos";
$Resul_Identificacion = 0;

}elseif($usuario_bd == $usuario_for && $clave_bd <> $clave_for && $ip_usuario == $ip_aceso && $total_acesos <= $ac_permitidos){
// Identificacion incorrecta
// echo "1 Error¡¡¡ Introduce de nuevo tus datos <br />$total_acesos";
$Resul_Identificacion = 1;
header("refresh:$time_retro; url=$pag_form");
}elseif($usuario_bd <> $usuario_for && $clave_bd == $clave_for && $ip_usuario == $ip_aceso && $total_acesos <= $ac_permitidos){
// Identificacion incorrecta
// echo "2 Error¡¡¡ Introduce de nuevo tus datos <br />$total_acesos";
$Resul_Identificacion = 1;
header("refresh:$time_retro; url=$pag_form");
}elseif($usuario_bd <> $usuario_for && $clave_bd <> $clave_for && $ip_usuario == $ip_aceso && $total_acesos <= $ac_permitidos){
// Identificacion incorrecta
// echo "3 Error¡¡¡ Introduce de nuevo tus datos <br />$total_acesos";
$Resul_Identificacion = 1;
header("refresh:$time_retro; url=$pag_form");
}elseif($usuario_bd == $usuario_for && $clave_bd <> $clave_for && $ip_usuario == $ip_aceso && $total_acesos > $ac_permitidos){
// Identificacion no admitida
// echo "1 Identificacion bloqueada, intentalo de nuevo en $minutos minutos<br />$total_acesos";
$Resul_Identificacion = 2;
header("refresh:$time_retro; url=$pag_form");
}elseif($usuario_bd <> $usuario_for && $clave_bd == $clave_for && $ip_usuario == $ip_aceso && $total_acesos > $ac_permitidos){
// Identificacion no admitida
// echo "2 Identificacion bloqueada, intentalo de nuevo en $minutos minutos<br />$total_acesos";
$Resul_Identificacion = 2;
header("refresh:$time_retro; url=$pag_form");
}elseif($usuario_bd <> $usuario_for && $clave_bd <> $clave_for && $ip_usuario == $ip_aceso && $total_acesos > $ac_permitidos){
// Identificacion no admitida
// echo "3 Identificacion bloqueada, intentalo de nuevo en $minutos minutos<br />$total_acesos";
$Resul_Identificacion = 2;
header("refresh:$time_retro; url=$pag_form");
}
// echo "<p>Resultado: $Resul_Identificacion</p>";
}else{
$Resul_Identificacion = "<p>Error¡¡¡ Introduce datos de acceso.</p>";
header("refresh:$time_retro; url=$pag_form");
}
return $Resul_Identificacion;
}
// Ejecutamos la funcion
// Datos provenientes del formulario
/* $usuario_for = $_POST['us'];
$clave_for = $_POST['cl'];
// Datos del usuario valido
$usuario_bd = 'aw';
$clave_bd = 'abc123';

$minutos = 1; // Tiempo en minutos que tendra bloqueada la identificacion
$ac_permitidos = 5; // Numero maximo de intentos de identificacion permitidos
$pag_form = 'form_cooki.php'; // Ruta del formulario de identificacion
$time_retro = 4; // Tiempo en segundos que tardara en devolver al formulario de identificacion

// La funcion End_Login() devuelve los siguientes valores:
// 0 - si la identificacion es correcta
// 1 - si la indentificacion es erronea, es decir, datos no validos
// 2 - si la indentificacion ha sido bloqueada por el tiempo en minutos de la variable $minutos
// Para bloquear los intentos en el formulario es preciso recuperar la cookie $_COOKIE['n_aceso']
// por ejemplo, asi: $bloqueo_aceso = $_COOKIE['n_aceso'];
$resultado = End_Login($usuario_for,$clave_for,$usuario_bd,$clave_bd,$minutos,$ac_permitidos,$pag_form,$time_retro);

echo "<p>Valor devuelto: $resultado</p>
<p><a href=\"$pag_form\" title=\"Recargamos el formulario\">Volver al formulario</a></p>"; */

?>


En la parte inferior de este código pueden verse instrucciones sobre cómo configurar esta función, motivo por el cual se han dejado dado lo aclaratorio que puede resultar.

Con los valores devueltos por esta función podemos condicionar el comportamiento del nuestro formulario para que permita o no los intentos de acceso.

form_cooki.php
<?php session_start();
// Creamos una cookie con una hora de duracion
$coki_ok = mt_rand(1000, 9999);
setcookie('coki_ok', $coki_ok, time() + 3600, '/'); // Creamos una cookie
$_SESSION['sesion_coki'] = $coki_ok; // Creamos una sesion

// Condicional para impedir el envio de datos
@$bloqueo_aceso = $_COOKIE['n_aceso']; // Recuperamos cookie
if($bloqueo_aceso <= 5){ // Tomara el mismo valor que la variable $ac_permitidos de la funcion
// Permite el envio de datos
$campo_for = null;
$enviar = '<input class="btn" type="submit" value="Entrar">';
$aviso = null;
}else{
// No permite el envio de datos
$campo_for = 'value="Denegado¡¡" ReadOnly';
$enviar = null;
$coki_ok = null;
$aviso = '<p>Espera 1 minuto para volver a intentarlo.</p>';
}
?><!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>Limitar accesos</title>
<meta name="robots" content="noindex, nofollow" />
<link rel="stylesheet" href="https://www.artesaniaweb.es/arch_dw/estilos_dw.css">
</head>

<body>
<div align="center">
<table width="50%">
<tr><td>
<div id="caja_frm">
<form method="POST" action="lim_accesos.php" name="limitar">
<fieldset>
<legend>Login</legend>
<?php echo $aviso;?>
<label for="usumail">Usuario / E-mail</label>
<input class="cmp50" type="text" id="usumail" name="usumail" <?php echo $campo_for; ?>/>

<label for="clave">Clave</label>
<input class="cmp50" type="password" id="clave" name="clave" />

<input type="hidden" name="coki" value="<?php echo $coki_ok; ?>">
<div style="clear: both"></div>
<?php echo $enviar; ?>
</fieldset>
</form>
</div>
<td><tr>
</table>
</div>
</body>

</html>


Como puede verse hemos creado una variable $coki_ok, y la hemos creado una sesión y una cookie. Dicho valor también lo enviamos como campo oculto (hidden) con nuestro formulario de identificación.

Con la variable $bloqueo_aceso recuperamos los accesos guardados en la cookie, y si superan el valor indicado para la variable $ac_permitidos, bloqueamos el acceso declarando como nula la variable $coki_ok y la variable que contiene como valor el botón de envio del formulario, $enviar. De igual manera, en la variable $campo_for hemos atribuido un nuevo valor, además de bloquear para escritura dicho campo. También hacemos emerger un mensaje que indique al usuario que está bloqueado durante un cierto tiempo mediante la variable $aviso.

Por supuesto, los valores con que hemos configurado esta aplicación son valores de prueba, en la práctica, y dependiendo de los requerimientos de seguridad que cada cual desee implementar, consideramos como adecuado no permitir más de 10 intentos durante la duración de la cookie (configurada a 1 hora) y el tiempo de bloqueo, por ejemplo, 15 minutos. Pero como hemos dicho, estos parámetros quedan supeditados a los criterios que considere oportuno el programador según la naturaleza del sitio Web.

Y con esto, ya podemos bloquear todos los ataques de “fuerza bruta” que son responsables de la mayoría de robos de datos de identificación aprovechándose de las contraseñas débiles.
Tags: Seguridad || logeo || End_Login || accesos || cookies

Comentarios.

Sin comentarios, publica el tuyo.