Si bien los informes varían, The Washington Post informó que el reciente pirateo de fotos de celebridades de iCloud se centró en el punto de inicio de sesión desprotegido de Find My iPhone:
"... se dijo que los investigadores de seguridad encontraron una falla en la función Buscar mi iPhone de iCloud que no cortó los ataques de fuerza bruta. La declaración de Apple ... sugiere que la compañía no considera esa revelación como un problema. Y eso es un problema, según al investigador de seguridad y colaborador del Washington Post Ashkan Soltani.
Estoy de acuerdo. Ojalá Apple hubiera sido más cercana; su respuesta cuidadosamente redactada dejó espacio para diferentes interpretaciones y parecía culpar a las víctimas.
Los piratas informáticos pueden haber usado este script iBrute en GitHub para dirigirse a las cuentas de las celebridades a través de Find My iPhone; la vulnerabilidad ya ha sido cerrada.
Dado que una de las corporaciones más ricas del mundo no asignó los recursos para limitar la tasa de todos sus puntos de autenticación, es probable que algunas de sus aplicaciones web no incluyan la limitación de la tasa. En este tutorial, le mostraré algunos de los conceptos básicos de limitación de velocidad y una implementación simple para su aplicación web basada en PHP..
La investigación de hacks anteriores ha revelado contraseñas que las personas tienden a usar con mayor frecuencia. Xeno.net publica una lista de las diez mil contraseñas principales. Su gráfico a continuación muestra que la frecuencia de las contraseñas comunes en su lista de los 100 principales es del 40%, y las 500 principales representan el 71%. En otras palabras, las personas comúnmente usan y reutilizan un pequeño número de contraseñas; en parte, porque son fáciles de recordar y escribir.
Eso significa que incluso un pequeño ataque de diccionario con solo las veinticinco contraseñas más comunes podría ser bastante exitoso al apuntar a los servicios.
Una vez que un pirata informático identifica un punto de entrada que permite intentos de inicio de sesión ilimitados, pueden automatizar ataques de diccionarios de gran volumen y alta velocidad. Si no hay una limitación de velocidad, entonces los hackers se vuelven más fáciles de atacar con diccionarios cada vez más grandes, o algoritmos automatizados con infinitas permutaciones..
Además, si se conoce información personal sobre la víctima, p. Ej. su actual socio o nombre de mascota, un pirata informático puede automatizar los ataques de permutaciones de contraseñas probables. Esta es una vulnerabilidad común para las celebridades..
Para proteger los inicios de sesión, hay un par de enfoques que recomiendo como referencia:
En ambos casos, queremos medir los intentos fallidos durante una ventana o ventanas de tiempo específicas, p. Ej. 15 minutos y 24 horas..
Un riesgo para bloquear los intentos por nombre de usuario es que el usuario real podría quedar bloqueado de su cuenta. Por lo tanto, queremos asegurarnos de hacer posible que el usuario válido vuelva a abrir su cuenta y / o restablezca su contraseña.
Un riesgo de bloquear los intentos por dirección IP es que a menudo son compartidos por muchas personas. Por ejemplo, una universidad puede albergar tanto al titular de la cuenta real como a alguien que intente hackear maliciosamente su cuenta. El bloqueo de una dirección IP puede bloquear al pirata informático así como al usuario real.
Sin embargo, un costo para aumentar la seguridad es a menudo un poco de mayor inconveniencia. Debe decidir cómo limitar estrictamente los servicios a sus tarifas y qué tan fácil desea que los usuarios vuelvan a abrir sus cuentas..
Puede ser útil codificar una pregunta secreta en su aplicación que se puede usar para volver a autenticar a un usuario cuya cuenta fue bloqueada. Alternativamente, puede enviar un restablecimiento de contraseña a su correo electrónico (con la esperanza de que no haya sido comprometido).
He escrito un poco de código para mostrarle cómo limitar el número de aplicaciones web; Mis ejemplos están basados en el Framework Yii para PHP. La mayor parte del código es aplicable a cualquier aplicación o framework PHP / MySQL.
Primero, debemos crear una tabla MySQL para almacenar información de intentos de inicio de sesión fallidos. La mesa debe almacenar el dirección IP
del usuario solicitante, el nombre de usuario o la dirección de correo electrónico intentados y una marca de tiempo:
$ this-> createTable ($ this-> tableName, array ('id' => 'pk', 'ip_address' => 'string NOT NULL', 'username' => 'string NOT NULL', 'created_at' => 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP',), $ this-> MySqlOptions);
Luego, creamos un modelo para la tabla LoginFail con varios métodos: agregar, verificar y purgar.
Siempre que haya un inicio de sesión fallido, agregaremos una fila a la tabla LoginFail:
función pública add ($ username) // agrega una fila a la tabla de inicio de sesión fallida con nombre de usuario y dirección IP $ failure = new LoginFail; $ failure-> username = $ username; $ failure-> ip_address = $ this-> getUserIP (); $ failure-> created_at = new CDbExpression ('NOW ()'); $ fallo-> guardar (); // siempre que haya un inicio de sesión fallido, elimine el registro de fallos anterior $ this-> purge ();
por getUserIP ()
, Utilicé este código de Stack Overflow.
También podemos aprovechar la oportunidad de un inicio de sesión fallido para limpiar la tabla de registros anteriores. Hago esto para evitar que los controles de verificación disminuyan con el tiempo. O bien, puede implementar una operación de purga en una tarea cron de fondo cada hora o todos los días:
purga de función pública ($ minutos = 120) // purgar entradas de inicio de sesión fallidas anteriores a $ minutos $ minutos_ago = (tiempo () - (60 * $ minutos)); // p.ej. Hace 120 minutos $ criterios = nuevo CDbCriteria (); LoginFail :: model () -> older_than ($ minutes_ago) -> applyScopes ($ criteria); LoginFail :: model () -> deleteAll ($ criteria);
El módulo de autenticación Yii que estoy usando se ve así:
autenticación de la función pública ($ atributo, $ params) si (! $ this-> hasErrors ()) // solo queremos autenticar cuando no hay errores de entrada $ identity = new UserIdentity ($ this-> username, $ this-> contraseña); $ identity-> authenticate (); si (LoginFail :: model () -> check ($ this-> username)) $ this-> addError ("username", UserModule :: t ("El acceso a la cuenta está bloqueado, contáctese con el soporte técnico")); else switch ($ identity-> errorCode) case UserIdentity :: ERROR_NONE: $ duration = $ this-> rememberMe? Yii :: app () -> controller-> module-> rememberMeTime: 0; Yii :: app () -> user-> login ($ identity, $ duration); descanso; caso UserIdentity :: ERROR_EMAIL_INVALID: $ this-> addError ("username", UserModule :: t ("El correo electrónico es incorrecto.")); LoginFail :: model () -> add ($ this-> username); descanso; caso UserIdentity :: ERROR_USERNAME_INVALID: $ this-> addError ("username", UserModule :: t ("Username is incorrect.")); LoginFail :: model () -> add ($ this-> username); descanso; caso UserIdentity :: ERROR_PASSWORD_INVALID: $ this-> addError ("password", UserModule :: t ("Password is incorrect.")); LoginFail :: model () -> add ($ this-> username); descanso; caso UserIdentity :: ERROR_STATUS_NOTACTIV: $ this-> addError ("status", UserModule :: t ("Su cuenta no está activada.")); descanso; caso UserIdentity :: ERROR_STATUS_BAN: $ this-> addError ("status", UserModule :: t ("Tu cuenta está bloqueada.")); descanso;
Cada vez que mi código de inicio de sesión detecta un error, llamo al método para agregar detalles al respecto a la tabla LoginFail:
LoginFail :: model () -> add ($ this-> username);
La sección de verificación está aquí. Esto se ejecuta con cada intento de inicio de sesión:
$ identity-> authenticate (); si (LoginFail :: model () -> check ($ this-> username)) $ this-> addError ("username", UserModule :: t ("El acceso a la cuenta está bloqueado, contáctese con el soporte técnico"));
Puede injertar estas funciones en la sección de autenticación de inicio de sesión de su propio código.
Mi verificación de verificación busca un gran volumen de intentos fallidos de inicio de sesión para el nombre de usuario en cuestión y por separado para la dirección IP que se está utilizando:
comprobación de la función pública ($ nombre de usuario) // verifique si se ha violado el umbral de inicio de sesión fallido // para el nombre de usuario en los últimos 15 minutos y la última hora // y para la dirección IP en los últimos 15 minutos y la última hora $ has_error = falso; $ minutes_ago = (tiempo () - (60 * 15)); // Hace 15 minutos $ horas_ago = (tiempo () - (60 * 60)); // Hace 1 hora $ user_ip = $ this-> getUserIP (); if (LoginFail :: model () -> since ($ minutes_ago) -> username ($ username) -> count ()> = self :: FAILS_USERNAME_QUARTER_HOUR) $ has_error = true; else if (LoginFail :: model () -> since ($ minute_ago) -> dirección_ip ($ user_ip) -> count ()> = self :: FAILS_IP_QUARTER_HOUR) $ has_error = true; else if (LoginFail :: model () -> since ($ hours_ago) -> username ($ username) -> count ()> = self :: FAILS_USERNAME_HOUR) $ has_error = true; else if (LoginFail :: model () -> since ($ hours_ago) -> ip_address ($ user_ip) -> count ()> = self :: FAILS_IP_HOUR) $ has_error = true; if ($ has_error) $ this-> add ($ username); devuelve $ has_error;
Verifico los límites de las tarifas durante los últimos quince minutos, así como la última hora. En mi ejemplo, permito 3 intentos de inicio de sesión fallidos por quince minutos y seis por hora para cualquier nombre de usuario dado:
const FAILS_USERNAME_HOUR = 6; const FAILS_USERNAME_QUARTER_HOUR = 3; const FAILS_IP_HOUR = 24; const FAILS_IP_QUARTER_HOUR = 12;
Tenga en cuenta que mis verificaciones de verificación utilizan los ámbitos con nombre ActiveRecord de Yii para simplificar el código de consulta de la base de datos:
// alcance de las filas desde la marca de tiempo de función pública desde ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(UNIX_TIMESTAMP (created_at)>'. $ tstamp. ')' ,)); devuelve $ esto; // alcance de las filas antes de la marca de tiempo public function old_than ($ tstamp = 0) $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(UNIX_TIMESTAMP (created_at)<'.$tstamp.')', )); return $this; public function username($username=") $this->getDbCriteria () -> mergeWith (array ('condition' => '(username = "'. $ username. '")',)); devuelve $ esto; public function ip_address ($ ip_address = ") $ this-> getDbCriteria () -> mergeWith (array ('condition' => '(ip_address ="'. $ ip_address. '")',)); return $ this ;
He intentado escribir estos ejemplos para que puedas personalizarlos fácilmente. Por ejemplo, puede omitir los cheques de la última hora y confiar en el último intervalo de 15 minutos. Alternativamente, puede cambiar las constantes para establecer umbrales más altos o más bajos para el número de inicios de sesión por intervalo. También podrías escribir algoritmos mucho más sofisticados. Tu decides.
Con este ejemplo, para mejorar el rendimiento, es posible que desee indexar la tabla LoginFail por nombre de usuario y por separado por dirección IP.
Mi código de muestra en realidad no cambia el estado de las cuentas para bloquearlas o proporcionar funcionalidades para desbloquear cuentas específicas, eso lo dejaré a usted. Si implementa un mecanismo de bloqueo y restablecimiento, es posible que desee ofrecer una funcionalidad para bloquear por separado por dirección IP o por nombre de usuario.
Espero que hayan encontrado esto interesante y útil. Por favor, siéntase libre de publicar correcciones, preguntas o comentarios a continuación. Me interesaría especialmente los enfoques alternativos. También puedes contactarme en Twitter @reifman o enviarme un correo electrónico directamente.
Créditos: iBrute vista previa de la foto vía Seguridad heise