Auditando código PHP me topé con una implementación de seguridad de contraseñas que me hizo pasar por los cinco estados emocionales del Modelo de Kübler-Ross: 1- Negación: "Esto no puede estar hecho así"; 2- Ira: "¡Por qué carajo hicieron ésto!"; 3- Negociación: "Por lo menos voy a tratar de que lo arreglen"; 4- Depresión: "¿Por qué me toca auditar este software horrible? ¡No quiero seguir revisando este código!"; 5- Aceptación: "No hay nada que hacer, no puedo luchar contra la ineptitud de los desarrolladores de software".

Como muchos sabrán, las contraseñas son uno de los eslabones más débiles en la cadena de protección de acceso a un sistema informático. Por ello, hay que implementar políticas y mecanismos seguros para generarlas, almacenarlas, modificarlas, y transmitirlas por al red. En este artículo voy a puntualizar el tema del almacenamiento seguro de contraseñas.

Las contraseñas son un dato más relacionado al usuario, como puede ser su nombre, apellido, código de identificación, etc. Y como tal, debe ser almacenado en una base de datos. La diferencia es que las contraseñas son datos sensibles, comparado con los demás, ya que proveen el acceso a un sistema.

Si las contraseñas de los usuarios se almacenan de forma plana (a simple vista, sin codificar o encriptar) como el resto de la información, alguien que logra acceso a la base de datos automáticamente tendrá acceso a las contraseñas de todos los usuarios. Entonces, ¿de qué forma es posible evitar almacenar las contraseñas en la base de datos garantizando un mecanismo de autenticación (validar que el usuario es quien dice ser)? La respuesta a esta problemática son las funciones hash.

La idea detrás de este mecanismo de protección es que el sistema no conozca la contraseña, es decir, que sólo la conozca el usuario. Lo que conoce el sistema es una transformación (codificación irreversible) de la contraseña, llamada hash. Cuando se genera la contraseña, el sistema le aplica una función matemática que produce una transformación irreversible de la misma. La fortaleza de esta función consiste en que no sea posible descifrar o determinar la contraseña a partir del hash generado.

Ejemplo de codificación hash utilizando la función MD5 en GNU/Linux:

root@debian7# echo "admin" | md5sum -t
456b7016a916a4b178dd72b947c152b7  -

Se observa que la palabra "admin" fue transformada a "456b7016a916a4b178dd72b947c152b7".

La función hash se considera irreversible si se observa un alto nivel de aleatoriedad en los valores generados. Para comprobarlo, codificar la palabra cambiando una sola letra y notar como se altera notablemente el resultado:

root@debian7# echo "bdmin" | md5sum -t
a6606fba4d89973e8486a62c6824d8cd  -

Existen muchas funciones hash con diferentes niveles de fortaleza, como pueden ser MD5, SHA1, SHA2, Tiger, Whirlpool, CRC, etc.

Para proveer autenticación, cuando el usuario envía la contraseña en el login, el sistema vuelve a computar el hash y lo compara con el que tiene almacenado en la base de datos. Si ambos hashes coinciden, significa que la contraseña es correcta. Ahora, ¿qué pasa si dos contraseñas diferentes generan el mismo hash? El usuario podría ingresar al sistema enviando una contraseña incorrecta. Efectivamente esto es así y se conoce como "colisión" en la función hash. Es otro factor importante que determina el nivel de seguridad o fortaleza del algoritmo de cifrado.

En el ejemplo anterior, existe una colisión con la palabra "admin", si encontramos otra palabra distinta tal que su hash es igual a "456b7016a916a4b178dd72b947c152b7".

Volviendo al tema del almacenamiento, si un atacante se hace de la base de datos, no tendrá acceso a las contraseñas de los usuarios sino a los hashes de las mismas. Si hemos utilizado una función hash fuerte, no habrá ningún riesgo asociado ya que no hay forma de revertir los hashes.

Esto pareciera ser suficiente para proteger las contraseñas de los usuarios, aunque no lo es, por dos motivos. Primero, si recuerdan mi artículo Cómo descifrar un hash MD5 online, es posible descifrar hashes simplemente buscándolos en Google. Esto se debe a que los hashes de las contraseñas más débiles suelen estar publicados en diferentes sitios y foros en Internet. A veces porque se han comprometido sitios, o porque han sido usados en ejemplos (como en este artículo). A partir de ahora, cada vez que vean la cadena "456b7016a916a4b178dd72b947c152b7", sabrán que se trata del hash MD5 de la palabra "admin". Y si buscan "456b7016a916a4b178dd72b947c152b7" en Google, entre tantos resultados tal vez aparezca este humilde blog. Segundo, si dos usuarios diferentes utilizan el mismo password, en la base de datos compartirán el mismo hash. ¿Qué tiene de grave esto? Que si se compromete el password de un usuario (porque es irresponsable con la seguridad de sus contraseñas, o es víctima de un ataque o engaño) y el atacante logra acceso a la base de datos, automáticamente se verá comprometida la cuenta del otro usuario.

Para evitar esta situación se utiliza lo que se llama "salt" (sal). El salt es un dato aleatorio que se agrega a cada contraseña, para evitar que dos hashes de una misma contraseña sean iguales. El salt se guarda plano en la base de datos junto con el hash resultante. Cada vez que es necesario autenticar, se toma la contraseña que ha ingresado el usuario junto con el salt y se recomputa el hash, para luego compararlo con el que está guardado en la base de datos. Para que esto funcione (desde el punto de vista de seguridad) a cada usuario se le asigna un salt aleatorio en el momento de crear la contraseña.

Gracias a esta técnica, si dos usuarios tienen el mismo password, tendrán diferentes hashes. Porque cada uno tendrá un diferente salt generado de forma aleatoria. Como se observa en la siguiente gráfica, al cambiar el salt (manteniendo el mismo password) cambia el hash.

La forma correcta de implementar hashes con salt es tal como está descrito en este artículo: combinar algo que sabe el usuario, con un dato aleatorio que conoce el servidor, para así obtener hashes únicos. ¿Qué hizo el desarrollador del código que audité? Aquí les dejo un trozo de código PHP:

$pass // variable que contiene el password plano enviado vía POST HTTP desde el cliente

$salt = sha1(md5($pass));
$pass = md5($pass.$salt);

$pass

/* ahora $pass tiene el hash que genera el password, o el hash para comparar contra la base de datos */

Para poner en un cuadrito y utilizar como ejemplo de cómo no se deben hacer las cosas en Seguridad de la Información. Obviamente no existe una columna para almacenar el salt en la tabla de usuarios de la base de datos. Lo que hizo el programador es generar el salt a partir de un doble hash (primero MD5 y luego SHA1) de la contraseña. ¿Qué tiene ésto de aleatorio? Nada. ¿Evito tener hashes repetidos en la base de datos? ¡Claro que no! El hash lo genera a partir de la contraseña, no a partir de un dato aleatorio. Por ende, dos contraseñas iguales generarán un mismo salt, que concatenado a la misma contraseña, generará el mismo hash. No gano nada, pierdo tiempo de procesamiento, y escribo código estúpido e inútil.

Esta es la solución de hashes con salt que implementó este desarrollador, para que su aplicación cumpla con los requisitos de seguridad que se piden a cada proveedor de software en una de las empresas en las que me desempeño como consultor de sistemas. Posiblemente no sea su culpa, tal vez esté asignado a más proyectos de los que puede manejar, puede que su líder de equipo lo esté sobre exigiendo y sus patrones explotando. Se me ocurren todas estas razones para justificar su falta de conocimiento y tiempo para investigar mínimamente este tópico. Pero la realidad es que demostró no saber qué es lo que estaba haciendo al momento de desarrollar ese código.

Eso es todo, sólo un poco de catarsis de parte de un Administrador de Seguridad de la Información. Por favor no cometan este tipo de errores en su código.


Tal vez pueda interesarte


Compartí este artículo