8.9.2.4. Ataques contra la autenticación

Uno de los tipos de ataques más socorridos son aquellos destinados a la obtención de una clave acceso a un sistema, que hemos denominado aquí ataques contra la autenticación. De entre ellos, podemos distinguir:

  1. El ataque de fuerza bruta, que se basa en la obtención de una clave mediante la prueba masiva de todas las combinaciones posibles. En la práctica se suelen limitar las combinaciones posibles restringiendo los caracteres posibles o la longitud mínima o máxima de la clave.

  2. El ataque de diccionario, que consiste en reducir las pruebas a las palabras que se encuentran en un diccionario de palabras o a una variación sobre las mismas.

  3. El ataque por deducción que consiste en intentar obtener la contraseña deduciéndola a partir de la información personal que se tiene del usuario (por ejemplo, la información GECOS o el propio nombre de cuenta).

  4. El ataque por ingeniería social que consiste en engañar al usuario para que el mismo sea el que proporcione las claves de acceso. Un envenenamiento DNS o un envenamuiento ARP pueden ser parte de un ataque de este tipo.

Estudiaremos en este apartado los tres primeros tipos.

8.9.2.4.1. Métodos

Para poner en práctica un ataque de estas características debemos distinguir entre:

Ataque local

En este caso disponemos del fichero con la clave ofuscada (mediante una función de hash generalmente) y el propósito es llegar a conocer cuál es la clave sin ofuscar. La suite más socorrida para esta tarea es john the ripper, aunque existen otras como hashcat.

Obsérvese que el ataque no se denomina local porque se realice contra la propia máquina, sino porque se dispone del fichero de contraseñas (p.e. porque se haya robado de un servidor) y el software de averiguación actúa sobre el propio fichero sin necesidad de hacer uso de ningún sistema de autenticación.

Ataque remoto

Que consiste en la averiguación de la clave intentando autenticaciones sucesivas sobre el servicio remoto, muy comúnmente SSH o SMTP. Para esta tarea podemos usar una aplicación como THC-Hydra.

Echaremos un vistazo a cómo se realizan estos ataques para conocer las ténicas y ponen en práctica más eficazmente las contramedidas adecuadas.

8.9.2.4.1.1. John the Ripper

Hay paquete en debian para su instalación:

# apt install john

Advertencia

John the Ripper tiene dos versiones: la oficial que es la incluida en la distribución y la versión Jumbo, con ampliaciones de su comunidad de usuarios y que tiene algunos añadidos bastantes interesantes. Para no complicarnos usaremos la versión oficial incluida en el repositorio oficial de debian. No obstante, puede instalarse la versión de la comunidad usando los repositorios de Kali:

# cat > /etc/apt/sources.list.d/kali.list
deb http://http.kali.org/kali kali-rolling main contrib non-free
# cat > /etc/apt/preferences.d/kali
Explanation: Paquetes de auditoría procedentes de Kali Linux
Package: *
Pin: release o=Kali
Pin-Priority: 50
# apt update
# apt install -t o=kali john

Además, generaremos un fichero de contraseñas para nuestras pruebas con cuatro usuarios:

$ cat <<EOF > passwords.txt
tester1:$(openssl passwd -1 -salt a secreta)
tester2:$(openssl passwd -1 -salt a Secreta2.)
tester3:$(openssl passwd -1 -salt a tester3)
tester4:$(openssl passwd -1 -salt a Tester4)
tester5:$(openssl passwd -1 -salt a 1234)
EOF

El programa tiene cuatro modos de funcionamiento:

single (ataque por deducción)

Toma el nombre de usuario y la información GECOS como base para intentar averiguar la contraseña. A esta información aplicará las reglas de transformación listadas en la sección [List.Rules:Single] del fichero de configuración (en principio, /etc/john/john.conf).

En nuestro fichero de contreseñas no disponemos de la información GECOS[1], así que sólo será posible usar el nombre de usuario. Es bastante evidente que con este modo podremos obtener las contraseñas de usuarios tester3 y tester4.

wordlist (ataque de diccionario)

Realiza un ataque con el diccionario que proporcionemos. A las palabras del diccionario podremos aplicarles transformaciones (añadir números, signos de puntuación, etc.) a fin de mejorar la efectividad. Estas reglas de transformación se listan en la sección [List.Rules:Wordlist] del ficheron de configuración.

Con un diccionario de palabras castellanas podríamos obtener las contraseñas de los otros dos usuarios.

Nota

Las dos técnicas single y wordlist comparten la sintaxis para definbir las reglas de transformación. De hecho, podríamos considerar el ataque single como un ataque de diccionario en que, para cada usuario, el diccionario está compuesto únicamente por el nombre de usuario (y la información gecos si la hay).

incremental (ataque de fuerza bruta)

Realiza un ataque de fuerza bruta en sí, aunque estableciendo una serie de reglas (caracteres válidos, longitud mínima, longitud máxima). Podríamos intentar este último ataque para averiguar la contraseña de tester5.

external (estilo \»libre\»)

Utiliza para la generación de palabras una función escrita en un subconjunto del lenguaje C.

Empecemos por aplicar el ataque single:

$ john -single passwords.txt
Created directory: /root/.john
Loaded 5 password hashes with 2 different salts (md5crypt [MD5 32/64 X2])
Press 'q' or Ctrl-C to abort, almost any other key for status
tester3          (tester3)
Tester4          (tester4)
2g 0:00:00:00 100% 4.651g/s 5869p/s 5874c/s 9797C/s tester51901..tester51900
Use the "--show" option to display all of the cracked passwords reliably
Session completed
$ john -show passwords.txt
tester3:tester3
tester4:Tester4

2 password hashes cracked, 3 left

Bien, hemos logrado averiguar 2 de las 5 contraseñas: aquellas que eran muy semejantes al nombre de usuario. Pasemos a realizar un ataque de diccionario[2] y, de paso, hagamos trampa ya que conocemos de antemano las contraseñas. Por un lado restringamos mucho el diccionario a unas pocas palabras:

$ grep secret /usr/share/dict/spanish >> /tmp/words.list

y, por otro, apliquemos unas reglas de transformación bastante simplificadas. El modo de crear estas reglas diferenciados, depende de si usamos la versión oficial o la Jumbo:

Versión oficial

Las reglas se encuentran definidas en la sección [List.Rules:Wordlist] y no hay forma de definir reglas con otro nombre distinto y aplicarlas. Lo más limpio, en este caso, es crear un fichero alternativo de configuración:

$ cat > ~/.john/john.conf
[List.Rules:Wordlist]
:
cAz"[0-9][.#_\-]"

en que se define esta sólo regla.

Advertencia

Hecho esto, john dejará de leer el fichero original /etc/john/john.conf, por lo que al acabar este tipo de ataque deberíamos borrar el fichero creado o moverlo a otra ubicación

Las reglas de transformación posibilidad que para cada palabra, se pruebe:

  • La palabra sin transformar.

  • La palabra con la primera en mayúscula (c) y la adición al final (Az) de una cadena compuesta por un número cualquiera y un signo.

Ahora podemos practicar el ataque de diccionario:

$ john -wordlist:/tmp/words.list -rules passwords.txt
Loaded 5 password hashes with 2 different salts (md5crypt [MD5 32/64 X2])
Remaining 3 password hashes with 2 different salts
Press 'q' or Ctrl-C to abort, almost any other key for status
secreta          (tester1)
Secreta2.        (tester2)
2g 0:00:00:00 100% 5.405g/s 2216p/s 2708c/s 2713C/s
Vicesecretaría..Vicesecretario9
Use the "--show" option to display all of the cracked passwords reliably
Session completed
$ john -show passwords.txt
tester1:secreta
tester2:Secreta2.
tester3:tester3
tester4:Tester4

4 password hashes cracked, 1 left
Versión Jumbo

En esta versión sí podemos crear nuevas secciones con otras reglas y aplicarlas. Para ello, lo más conveniente es añadir un .include al fichero de configuración para que podamos añadir nuestras reglas personales aparte:

# cat >> /etc/john/john.conf

.include "~/.john/john-local.conf"

Nota

Deje una línea en blanco antes y detrás del .include

Hecho lo cual podemos hacer nuestros añadidos en tal fichero:

# cat > ~/.john/john-local.conf
[List.Rules:MisReglas]
:
cAz"[0-9][.#_\-]"

que podemos comprobar que están disponibles obteniendo la lista completa de reglas:

# john -list=rules | grep -i MisR
misreglas

En este caso, el ataque se hace exactamente igual que en la versión oficial, pero indicando qué conjunto de reglas quieren aplicarse:

$ john -wordlist:/tmp/words.list -rules:misreglas passwords.txt

Nota

Tanto para este tipo de ataque como para el siguiente, en el que se generan automáticamente contraseñas, podemos comprobar cuáles son las generadas añadiendo la opción -stdout y eliminando el fichero objetivo:

$ john -wordlist:/tmp/words.list -rules -stdout
$ john -incremental:Digits -stdout

Por último, ilustraremos un ataque incremental usando digitos como caracteres válidos:

$ cat >> ~/.john/john.conf

[Incremental:Digits]
File = $JOHN/digits.chr
MinLen = 3
MaxLen = 10
CharCount = 10

En realidad, esta definición (aunque con distinto mínimo y máximo) ya está hecha en el fichero de configuración /etc/john.john.conf, pero establecerla nosotros mismos en nuestro fichero de configuración nos permite echar un vistazo a como se construyen estos ataques de fuerza bruta. Además, de no obrar así, deberiamos deshacernos de ~/.john/john.conf ya que su existencia, anula la lectura del fichero general. Aplicando este ataque incremental:

$ john -incremental:Digits passwords.txt
Loaded 5 password hashes with 2 different salts (md5crypt [MD5 32/64 X2])
Remaining 1 password hash
Press 'q' or Ctrl-C to abort, almost any other key for status
1234             (tester5)
1g 0:00:00:00 5.000g/s 1340p/s 1340c/s 1340C/s 1234..123321
Use the "--show" option to display all of the cracked passwords reliably
Session completed
$ john -show passwords.txt
tester1:secreta
tester2:Secreta2.
tester3:tester3
tester4:Tester4
tester5:1234

5 password hashes cracked, 0 left

Nota

Con la versión Jumbo, el software también es capaz de atacar contraseñas de ficheros rar o claves simétricas de claves SSH o certificados en formato PEM.

Nota

John the Ripper puede usarse como herramienta para comprobar la robustez de las contraseñas de nuestros usuarios y enviar un mensaje de correo a aquellos usuarios de los que hayamos sido capaces de averiguar la contraseña. Para ello la suite trae un script llamado mailer que automatiza la tarea.

8.9.2.4.1.2. THC-Hydra

Cuando no se dispone del fichero de contraseñas, es obvio que el único modo de averiguar una, es intentado repetidamente la autenticación hasta dar con una pareja usuario/contraseña que permita el acceso. THC-Hydra permite automatizar los intentos de acceso a distintos tipos de servidores (entre ellos FTP, IMAP, MySQL, SMTP o SSH).

A diferencia del caso anterior, el objetivo no es sólo averiguar contraseñas, sino también usuarios, por lo que el programa permite tanto facilitar el usuario como la contraseña. Por ello:

-l <usuario>

Permite facilitar el nombre de usuario.

-L <fichero>

Permite facilitar la lista de usuarios expresada en el fichero indicado (un usuario por línea).

-p <contraseña>

Permite facilitar una contraseña.

-P <fichero>

Permite facilitar la lista de contraseñas expresada en el fichero indicado (una contraseña por línea).

-C <fichero>

Permite facilitar la lista de tuplas usuario/contraseña expresada en el fichero indicado (cada línea debe ser de la forma usuario:contraseña).

-x <min:max:charset>

Genera todas las combinaciones posibles entre una longitud mínima y una longitud máxima de los caracteres definidos con charset. En la definición del charset «1» representan digitos, «a» letras minúsculas, «A» letras mayúsculas y cualquier otro caracter debe expresarse explícitamente. Por tanto, -x '4:8:a1.-' generará todas las claves posibles de entre 4 y 8 caracteres que contengan letras minúsculas, números, puntos y guiones.

-e <nsr>

Permite añadir a las pruebas de contraseña que provequen -p, -P o -x, la contraseña nula (n), la contraseña que coincida con el login (s) y la contraseña que sea el login puesto del revés (r). Así, por ejemplo, -e ns probaría como contraseña la vacía y el login.

Así pues, -p, -P y -C nos permiten realizar ataques de diccionario y -x ataques de fuerza bruta con restricciones (equivalentes a los incrementales de john).

Si se facilitan varios usuarios y varias contraseñas se probarán todas las combinaciones posibles, de modo que para el primer usuarios se prueban todas las contraseñas, para el segundo, todas; y así sucesivamente. Si quiere hacerse la combinación de manera que para la primera contraseña se prueben todos los usuarios, para la segunda, todos, etc. puede añadirse la opción -u.

Otras opciones de interés son:

-f

Abandona el ataque cuando se consigue obtener la primera pareja usuario/contraseña.

-t N

Fija el número de intentos simultáneos que, por defecto, es 16.

Con estos mimbres es fácil hacer un ataque:

$ hydra -l usuario -P /tmp/passwords.txt -e nsr -t 4 -vV VICTIMA ssh

o si se prefiere:

$ hydra -l usuario -P /tmp/passwords.txt -e nsr -t 4 -vV ssh::/VICTIMA

Nota

¿Hay alguna manera de tomar un diccionario y probar no sólo las palabras que contiene, sino también otras contraseñas generadas a partir de esas palabras? Algo así a lo que hace john al añadir -rules. Desgraciadamente, no. O no al menos exclusivamente con hydra. Sí podemos, sin embargo, generar las transformaciones con john y su opción -stdout y el diccionario resultante pasárselo a hydra, haciendo uso de una tubería:

$ john -wordlist:/tmp/words.list -rules -stdout | hydra -l usuario -P /dev/fd/0 -e nsr -t 4 -vV ssh::/VICTIMA

8.9.2.4.2. Contramedidas

De los métodos de ataque anteriores, debemos de entresacar dos enseñanzas:

  • Hemos de evitar el robo de ficheros de contraseñas, aunque éstas estén ofuscadas, ya que existen herramientas como john para intentar averiguarlas.

  • Deben evitarse los nombres de usuarios que por defecto nos sugieran las aplicaciones, porque son esos usuarios los que suelen probarse con herramientas como hydra.

  • Hemos de defender los servicios accesibles de ataques automatizados como los realizados por hydra. Enfocaremos esta sección en ver cómo hacerlo mediante dos alternativas:

    1. Ocultamiento del servicio mediante port-knocking, de manera que el servicio será accesible sólo si previamente se ha dado el «santo y seña».

    2. Veto al acceso para aquellas máquinas cuya actividad las vuelva sospechosas de estar intentando un ataque contra la autenticación. Para practicar este veto, linux provee dos mecanismos:

      hosts.deny

      Toda dirección IP listada en /etc/hosts.deny no podrá acceder a ningún servicio que use TCP Wrappers <https://es.wikipedia.org/wiki/TCP_Wrapper>. El mecanismo es poco versátil, porque se denegará el acceso a todos los servicios que lo usen y no a algunos individuales, y porque los servicios ajenos al mecanismo no se verá afectados en ningún caso. Para conocerse si un servicio usa este mecanismo basta con comprobar si el ejecutable enlaza con la librería libwrap.so:

      $ ldd /usr/sbin/sshd | grep -q 'libwrap.so' && echo "SI" || echo "NO"
      SI
      $ ldd /usr/sbin/nginx | grep -q 'libwrap.so' && echo "SI" || echo "NO"
      NO
      

      Por tanto, este método serviría para impedir el acceso al servidor SSH, pero no al servidor web. Este mecanismo de veto era el que usaba denyhosts[3].

      Cortafuegos (por lo general, iptables o nftables). Consiste en crear reglas para las máquinas sospechosas que impidan el acceso a los servicios disponibles. El mecanismo es mucho más versátil ya que se podrá aplicar a servicios individuales y será aplicable a todo servicio[4]. Las soluciones con el módulo recent de iptables y fail2ban usan esta técnica.

      Nota

      Se use uno u otro mecanismo, las listas de direcciones vetadas son dinámicas, así que debe habilitarse algún medio para gestionarlas.

8.9.2.4.2.1. IPtables

El módulo recent de iptables es muy apropiado tanto para limitar los atanques de fuerza bruta como para implementar la técnica del port-knocking.

8.9.2.4.2.1.1. Limitación de accesos

Es obvio que en condiciones normales un uso legítimo de algunos de nuestros servicios no implica que una misma dirección IP abra muchas conexiones en un corto espacio de tiempo. Por ejemplo, al servidor SSH accederá de vez en cuando un usuario por lo que muy de vez en cuando se abrirá una conexión al puerto SSH. Con el servicio SMTP que escucha en los puertos 587 o 465 ocurrirá básicamente lo mismo: un usuario, cada cierto tiempo, mandará un mensaje, pero es bastante improbable que lo haga repetidamente en un corto espacio de tiempo.

Nuestro propósito, pues, es vetar el acceso a aquellas direcciones que, durante un espacio de tiempo que establezcamos, sobrepasen un umbral de conexiones nuevas, porque si lo hacen, sospechamos que se trata de intentos automatizados de acceso que pretenden encontrar una pareja usuario/contraseña válida.

Veamos cómo asegurar los servicios SSH y SMTP de ataques de fuerza bruta para impedir que desde una misma IP se pueda abrir más de 6 conexiones nuevas en un periodo menor a 5 minutos:

# Para desechar paquetes registrando el rechazo
iptables -N LOGDROP
iptables -A LOGDROP -j LOG --log-level warning --log-prefix "Ataque fuerza bruta: "
iptables -A LOGDROP -j DROP

# Anti ataques de fuerza bruta contra SSH
iptables -N SSH
iptables -A SSH -m recent --name brute-ssh --update --seconds 300 --hitcount 6 --reap -j LOGDROP
iptables -A SSH -m recent --name brute-ssh --set -j ACCEPT

# Anti ataques de fuerza bruta contra SMTP
iptables -N SMTP
iptables -A SMTP -m recent --name brute-smtp --update --seconds 300 --hitcount 6 --reap -j LOGDROP
iptables -A SMTP -m recent --name brute-smtp --set -j ACCEPT

iptables -A INPUT -p tcp --dport ssh --syn -m conntrack --ctstate NEW -j SSH
iptables -A INPUT -p tcp -m multiport --dport urd,submission --syn -m conntrack --ctstate NEW -j SMTP

Nota

Suponemos que seguimos una política de lista blanca y que por el puerto 25 no se realizan conexiones autenticadas, tal como se propugna aquí.

Obsérvese, porque es importante que, usando esta estrategia, el tiempo de comprobación del accesos y el tiempo de veto debe coincidir. En nuestra propuesta, son 5 minutos. Cuanto mayor sea este tiempo, mayor es el tiempo de veto, pero también mayor el número de falsos positivos. Por tanto, una mejora a lo anterior consiste en independizar ambos tiempos, de modo que el de comprobación podamos reducirlo (por ejemplo, 30 segundos) y el de veto podamos ampliarlo (por ejemplo, 10 minutos). Para ello podemos crear dos conjuntos distintos: uno que apunte accesos y otro que apunte vetos (banned). Por ejemplo:

# Para desechar paquetes registrando el rechazo
iptables -N LOGDROP
iptables -A LOGDROP -j LOG --log-level warning --log-prefix "Ataque fuerza bruta: "
iptables -A LOGDROP -m recent --name banned --set -j DROP

# Anti ataques de fuerza bruta contra SSH
iptables -N SSH
iptables -A SSH -m recent --name access-ssh --update --seconds 30 --hitcount 5 --reap -j LOGDROP
iptables -A SSH -m recent --name access-ssh --set -j ACCEPT

# Anti ataques de fuerza bruta contra SMTP
iptables -N SMTP
iptables -A SMTP -m recent --name access-smtp --update --seconds 30 --hitcount 5 --reap -j LOGDROP
iptables -A SMTP -m recent --name access-smtp --set -j ACCEPT

iptables -A INPUT -m conntrack --ctstate NEW -m recent --name banned --update --seconds 600 --reap -j DROP
iptables -A INPUT -p tcp --dport ssh --syn -m conntrack --ctstate NEW -j SSH
iptables -A INPUT -p tcp -m multiport --dport urd,submission --syn -m conntrack --ctstate NEW -j SMTP

En la propuesta, los accesos a los distintos servicios se apuntan por separado (access-ssh, access-smtp), así que se pueden establecer distintas limitaciones según el servicio, pero una vez que un cliente alcanza la limitación en uno de ellos, se apunta en una lista única (banned): eso facilita las reglas a costa de que el tiempo de veto sea uno con independencia de por qué puerto se haya recibido el ataque. Además, el cliente vetado no podrá acceder a ningún servicio y, si lo intenta durante el tiempo de veto, se renovará el tiempo de veto.

8.9.2.4.2.1.2. Port-knocking

Port knocking (golpeteo de puertos, en castellano) es una técnica que permite abrir desde el exterior en el firewall el puerto en el que escucha algún servicio mediante una secuencia preestablecida de intentos de conexión a puertos cerrados. Con ello se logra que un escaneo de puertos no detecte los servicios realmente ofrecidos.

En consecuencia, el acceso al servicio oculto se realiza del siguiente modo:

  • En principio, el cortafuegos impiede el acceso al servicio.

  • Un cliente que conoce la secuencia que permite abrir el puerto de escucha, la lleva a cabo.

  • El servicio de port-knocking intercepta esta secuencia y, sin aviso al cliente (nunca lo hay) abre durante un tiempo limitado el acceso a ese cliente.

  • El cliente realiza la conexión al servicio.

Para ponerlo en práctica puede usarse un servicio independiente como knockd, pero aquí trataremos de hacerlo exclusivamente con iptables.

Para nuestro ejemplo, el golpeteo será de tres golpes: el primero al puerto 11111/UDP, el segundo al puerto 22222/UDP y el tercero al puerto 33333/UDP.

# Las máquinas dan el primer toque (debe ser al puerto 11111/UDP)
iptables -N toc
iptables -A toc -p udp --dport 11111 -m recent --set --name toc -j DROP

# Las máquinas dan el segundo toque (debe ser al puerto 22222/UDP)
iptables -N toctoc
iptables -A toctoc -p udp --dport 22222 -m recent --set --name toctoc -j DROP

# Las máquinas dan el tercer toque (debe ser al puerto 33333/UDP)
iptables -N toctoctoc
iptables -A toctoctoc -p udp --dport 33333 -m recent --set --name toctoctoc -j DROP

# Máquinas con el paso concedido
iptables -N paso
iptables -A paso -p tcp --dport ssh -j ACCEPT
# ... y otros posibles servicios también ocultos


iptables -A INPUT -m recent --remove --name toctoctoc -j paso
iptables -A INPUT -m recent --remove --name toctoc -j toctoctoc
iptables -A INPUT -m recent --remove --name toc -j toctoc
iptables -A INPUT -j toc

Obsérvese que cada vez que damos un toque, borramos el toque anterior (p.e. al hacer «toctoc» borramos «toc») y que finalmente tras «toctoctoc» el siguiente paquete enviado borra el último «toctoctoc». En consecuencia, para conectar al servicio debemos mandar en orden y exclusivamente «toc», «toctoc», «toctoctoc» y el paquete que inicia la conexión. Si no seguimos la secuencia o intercalamos otro paquete entre medias, la apertura se malogrará.

Nota

Suponemos una política de lista blanca y que todas estas reglas sustituyen a la repla única:

iptables -p tcp --dport ssh -j ACCEPT

es obvio que nuestro conjunto de reglas deberá incluir todas las demas necesarias (permitir el tráfico de la interfaz loopback, permitir el tráfico establecido, etc.)

De este modo, para que una máquina pudiera acceder al servicio SSH debería antes hacer lo siguiente:

#!/bin/sh
#
# Accede al servicio SSH, tocando primero

TOC="11111 22222 33333"

get_server() {
   ssh -G "$@" | awk '$1=="hostname" {print $2}'
}

server=$(get_server "$@")
for p in $TOC; do
   printf "toc... "
   echo "x" | nc -u -q1 "$server" "$p"
done
echo
ssh "$@"

Nota

Si decidimos abrir el servicio con el golpeo del mismo puerto un número determinado de golpes (en el ejemplo, 3), podemos simplificar las reglas:

#!/bin/sh

# Repique en el puerto
iptables -N toc
iptables -A toc -p udp --dport 11111 -m recent --set --name toc -j DROP
iptables -A toc -m recent --remove --name toc

# Ya repicado lo suficiente
iptables -N paso
iptables -A paso -m recent --remove --name toc
iptables -A paso -p tcp --dport ssh -j ACCEPT
# ... y otros posibles servicios también ocultos


iptables -A INPUT -m recent --rcheck --hitcount 3 --name toc -j paso
iptables -A INPUT -j toc

8.9.2.4.2.2. nftables

Como habrá que ir jubilando el viejo iptables, es preciso buscar alternativas con nftables.

8.9.2.4.2.2.1. Limitación de accesos

Mediante el uso de la limitación del caudal y los conjuntos dinámicos, puede definirse una limitación efectiva. Es conveniente, además, que lea las consideración vertidas en el epígrafe correspondiente para la implementación con iptables, porque son pertinentes también para nftables.

Como en aquel epígrafe, prepararemos una solución para proteger los servicios SMTP y SSH. Partiremos de que tomamos como base la política de lista blanca propuesta para la resolución de los casos de uso a la cual añadimos este fíchero que implementa la defensa:

# /etc/nftables.d/01-antibruta.nft
#
# Configuración contra ataques de fuerza bruta.
#
table ip filter {
   set banned {
      type ipv4_addr
      size 4095
      timeout 5m
   }

   chain BANNED {
      type filter hook input priority filter-5

      ct state new ip saddr @banned counter reject comment "Veto a atacantes de fuerza bruta"
   }

   chain PROTECTOR {
      ct state new meter test size 65535 {ip saddr timeout 1m limit rate 3/minute burst 3 packets} accept
      ct state new add @banned
   }
}

Posteriormente, en el fichero en que definamos los servicios accesibles podemos escribir algo como esto:

# /etc/nftables/90-whitelist.nft

table ip filter {
   set protected_ports {
      type inet_service
      flags constant
      elements = {
         ssh,
         urd,
         submission
      }
   }

   chain OUTPUT {
      ct state {established, related} accept
   }

   chain INPUT {
      ct state {established, related} accept

      tcp dport @protected goto PROTECTOR comment "Servicios protegidos contra fuerza bruta"
      tcp dport {http, https} accept comment "Servicio WEB"  # Servicio no protegido
   }
}

8.9.2.4.2.2.2. port-knocking

8.9.2.4.2.3. fail2ban

fail2ban (página web) es una aplicación escrita en python que escruta los accesos a distintos servicios, identifica cuáles de ellos son fallidos y, aplicando ciertas reglas de tolerancia (p.e. 5 accesos fallidos en los últimos 5 minutos desde una misma dirección) impide el acceso al servidor desde la dirección desde la que se realizan tales accesos durante un espacio de tiempo determinado. Para vetar el acceso crea dinámicamente reglas de iptables.

Por tanto, persigue el mismo objetivo que la limitación de accesos que practicamos con iptables, pero no del mismo modo, por lo que presenta ventajas y inconvenientes:

Ventajas
  • Al escudriñar si el acceso ha tenido éxito, sólo contabiliza como accesos sospechos los fallidos. Por tanto, es más improbable que obtengamos con él falsos positivos.

  • Es una solución más general. El módulo recent no sirve para evitar ataques de fuerza bruta a servidores web, por ejemplo, ya que por la naturaleza de este protocolo, un cliente puede abrir muchas conexiones seguidas (o incluso simultáneas) con el servidor. Por tanto, limitar el acceso no es solución. En cambio, sí lo es detectar acceso fallidos.

Desventajas
  • Debemos instalar software adicional y mantenerlo en ejecución.

  • El desempeño de la solución que usa el módulo recent de iptables es muchísimo mejor ya que, por su naturaleza, fail2ban debe hacer muchas más tareas y tareas más pesadas: cada cierto tiempo debe buscar en uno o varios registros accesos restringidos y añadir o eliminar reglas de iptables.

  • Cada servidor requiere una configuración de búsqueda de fallos diferente, puesto que registra los acceso fallidos con un formato diferente y, posiblemene, en un lugar diferente.

La instalación es sencilla puesto que se dispone de paquete en debian:

# apt install fail2ban

8.9.2.4.2.3.1. Estructura de la configuración

Toda la configuración se encuentra dentro de /etc/fail2ban:

/etc/fail2ban/
|-- action.d
|   |-- iptables.conf
|   `-- ...
|-- fail2ban.conf
|-- fail2ban.d
|-- filter.d
|   |-- sshd.conf
|   `-- ...
|-- jail.conf
`-- jail.d
    |-- sshd.conf
    `-- ...

Las claves de esta configuración modular son las siguientes:

  • Los ficheros de configuración son ficheros ini con extensión .conf. Ahora bien, si en el mismo directorio se encuentra un fichero extensión .local, fail2ban leerá el fichero .conf primero y usará el fichero .local para añadir más opciones o sobreescribir las que incluyera .conf. Por tanto, es mejor no modificar nunca los ficheros .conf ya que son los que distribuye debian con su paquete.

  • El fichero fail2ban.conf incluye opciones para controlar cómo arranca el demonio (p.e. podríamos cambiar el nombre del fichero donde fail2ban apunta sus propios registros). En realidad, la definición de estas opciones no se circunscribe exclusivamente al fichero, sino que hay toda una estructura modular de ficheros que se leen en el siguiente orden:

    1. fail2ban.conf.

    2. fail2ban.d/*.conf, en orden alfabético.

    3. fail2ban.local

    4. fail2ban.d/*.local, en orden alfabético.

    de manera que lecturas posteriores añaden nuevas opciones y sobreescriben las ya existentes.

  • El fichero jail.conf (y toda su estructura correspondiente) es lo que nos toca modificar fundamentalmente para ajustarlo a nuestro servidor. Aquí es donde se define cada jail, o sea, cada tarea de vigilancia que consiste en:

    • Reconocer en la fuente correcta (un registro de syslog, por ejemplo) los intentos fallidos de conexión. El reconocimiento de estos intentos se logra aplicando expresiones regulares sobre los registros, la definición de las cuales se denomina filtro en la jerga de fail2ban.

    • En base a lo anterior, establecer cuáles son los ataques sufridos por la máquina. Lo habitual es que fail2ban considere ataque la existencia de una cantidad prefijada de conexiones fallidas en un periodo también prefijado de tiempo.

    • En caso de detectar uno o más ataques, desencadenar una acción de veto (la adición de una regla de iptables, por ejemplo).

    Por tanto, la escritura de un jail supone definir:

    • Qué filtro queremos aplicar.

    • Qué condiciones se deben cumplir para encontrarnos ante un ataque. Por ejemplo, la opción maxretry indica el máximo número de intentos fallidos que toleramos o findtime el periodo de tiempo en que deben buscarse esos intentos.

    • Qué acción debe desencadenarse. Por ejemplo, iptables.conf crea reglas de iptables, iptables-ipset-proto4.conf crea reglas de iptables que aprovechan las capacidades del módulo set, hostsdeny.conf añade entradas en /etc/hosts.deny, etc.

  • En action.d se definen las acciones.

  • En filter.d se definen los filtros.

8.9.2.4.2.3.2. Configuración básica

El paquete trae un fichero jail.conf que incluye entre otras cosas los valores de la configuración por defecto en la sección [DEFAULT]. Esto es así, porque el fichero es de tipo INI y cada jail se define en una sección distinta.

Si echamos un ojo veremos que en la referida sección hay opción que dice:

maxretry = 6

Esto significa que, a menos que en un jail en particular redefinamos la opción con otro valor, para todos los servicios en umbral máximo de fallos que toleraremos será 6.

Empecemos por crear un jail.local para redefinir a nuestro gusto las opciones comunes:

[DEFAULT]
ignoreip   = 127.0.0.0/8 192.168.0.0/16
bantime    = 1200    ; 20m
findtime   = 120    ; 2m
destemail  = root@localhost
sendermail = root@localhost

Hemos modificado:

ignoreip

Para que se evite crear reglas de veto a máquinas locales.

bantime

Tiempo en segundo que se mantendrá vetada a la máquina.

findtime

Periodo de tiempo hacia atrás en que se restrean accesos fallidos.

destemail, sendermail

Direcciones de correo que se usan como destinatario y remitente para el envío de avisos cuando se ha realizado un veto. fail2ban no tiene una opción que habilite o deshabilite estos mensajes, sino que está definida la acción action.d/sendmail.conf que no realiza veto alguno, sino que simplemente manda el aviso. Si nuestra intención es que el jail avise, deberá de definirse con dos acciones: la que propiamente realiza el veto y ésta.

Bien, pero ¿qué hay de los jails? Vale. Pues hagamos (o mejor, modifiquemos) un jail para SSH. Si el servicio es habitual, es más que probable que ya tenga una sección en la configuración. La de este servicio se llama [sshd] y estudiaremos bajo el próximo epígrafe por qué es así y por qué ya viene activo este jail. Ahora nos limitaremos a establecer las opciones particulares para este jail. Lo más adecuado es crear jail.d/ssh.local con un contenido como éste:

[sshd]
;enable = true
maxretry = 5
findtime = 90     ; 1.5m

Nota

No lo habilitamos, porque ya está activo.

8.9.2.4.2.3.3. Funcionamiento

Es obvio que cada vez que hagamos un cambio como en la configuración anterior, podremos reiniciar el servicio con la herramienta habitual de debian:

# invoke-rc.d fail2ban restart

Además, podemos comprobar de forma genérica cuales son los servicios protegidos o, más precisamente dicho, los jails activos:

# fail2ban-client status
Status
|- Number of jail:      1
`- Jail list:   sshd

Para consultar el estado de un jail particular:

# fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed: 0
|  |- Total failed:     2
|  `- File list:        /var/log/auth.log
`- Actions
   |- Currently banned: 1
   |- Total banned:     1
   `- Banned IP list:   192.168.1.241

8.9.2.4.2.3.4. Hurgando en la configuración

Hasta ahora, nos hemos limitado a modificar a tientas un jail ya creado ([sshd]). Ahora bien, más allá de que por su nombre parezca que efectivamente vigila el servicio SSH, ¿por qué realmente vigila tal servicio? ¿Cómo lo hace? ¿Cómo lo defiende? Para resolver las preguntas lo mejor es hacer un poco de ingeniera inversa sobre la configuración. Si revisamos jail.conf descubriremos la siguiente sección:

[sshd]

port    = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s

en donde está claro que parece un jail que actúa sobre el puerto 22 (ya veremos qué signifca eso). Por otro lado, se fija backend que si seguimos la madeja de variables nos lleva a un valor de «auto». Leyendo los comentarios en el propio fichero llegaremos a la conclusión de que esto implica usar como backend «polling», que significa que se leen registros del fichero indicado con logpath. De nuevo, siguiendo valores, veremos que en este caso tal fichero es /var/log/auth.log, valor que es correcto si está activo syslog, como es el caso de Stretch o Buster.

Nota

Desde las últimas versiones de fail2ban es posible también usar como backend para los registros systemd, aunque esto exige definir la opción journalmatch y no logpath.

Bien, sabemos qué registros revisa fail2ban, pero ¿qué filtros usa este jail? Es obvio que, si la respuesta no está en su sección, se tiene que hallar en la sección [DEFAULT], y efectivamente así es:

filter = %(__name__)s

que implica de modo predeterminado que el filtro se llama como el nombre de la sección, en este caso, sshd. Por tanto, el filtro se encuentra en filter.d/sshd.conf. Si echamos un ojo al filtro, encontraremos las expresiones regulares que concuerdan con los mensajes de acceso fallido que el demonio del servicio escribe en los registros. Además, está definida la opción journalmatch apropiada, de modo que hacer que el jail use systemd en vez de syslog se limita a escribir:

[sshd]
# ... configuración personalizada ...
backend = systemd

Resuelto el asunto del filtro, ¿cuál es la acción que se lleva a cabo? La respuesta de nuevo está en la sección [DEFAULT]:

banaction = iptables-multiport
action_ = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s",
                        port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
action_mw = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s",
                          port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
            %(mta)s-whois[name=%(__name__)s, sender="%(sender)s",
                          dest="%(destemail)s", protocol="%(protocol)s", chain="%(chain)s"]

# ... otras variantes de la acción ...

action = %(action_)s

La configuración está desgranada de esta forma para simplificar las modificaciones:

  • La opción que define cuál es la acción a realizar es action, que se apoya en action_, que a su vez usa banaction. Una alternativa podría haber sido:

    [sshd]
    action = iptables-allports[name=%(__name__)s, chain=INPUT]
    

    que veta a la máquina sospechosa el acceso a todos los puertos. En este caso, no es necesaria la inclusión de port o protocol.

  • La razón de la existencia de action_ es que es necesario pasar a la acción algunos datos necesarios como el puerto, por ejemplo. Lo cierto es que se pasan parámetros muy generosamente, porque no todos son necesarios y pasar de más no provoca ningún fallo.

    Nota

    El valor predeterminado de chain está definido en action.d/iptables-common.conf y es INPUT.

  • La acción definida en action.d/ que usaremos viene definida por banaction y será action.d/iptables-multiport.conf. Si echamos un vistazo a ese fichero, es fácil darse cuenta de que para vetar se usa iptables usando el módulo multiport[5] y se veta exclusivamente el acceso a los puertos del servicio.

    Así, podríamos hacer:

    [sshd]
    # ... otra configuración adicional ...
    banaction = iptables-allports
    

y todo funcionaría perfectamente, ya que esta acción requiere aún menos parámetros que iptables-multiport y, por tanto, el valor de action_ seguirá siendo válido.

  • Otros action_* añaden alguna acción extra. Por ejemplo, si hiciéramos:

    [sshd]
    # ... otra configuración adicional ...
    action = %(action_mw)s
    

    Además de vetar, se enviaría un mensaje de aviso cada vez que se produjera un veto.

Por último, afirmamos rotundamente al principio que [sshd] estaba activo; pero, ¿por qué es así? De hecho, en la sección [DEFAULT] se dice sin ambages:

enabled = False

La razón de ello es que jail.d/ viene de serie con un jail llamado jail.d/defaults-debian.conf con este contenido:

[sshd]
enabled = true

Por lo general, si los ataques son frecuentes es conveniente hacer[6]:

banaction = iptables-ipset-proto6

que usa el módulo ipset de iptables para gestionar la lista de direcciones IP vetadas.

Advertencia

Es bastante probable que ipset no esté instalado en su sistema:

# apt install ipset

Ejemplo de aplicación

Si quisiéramos vetar con /etc/hosts.deny, en vez de hacerlo con el cortafuegos y leer registros con systemd, podríamos ensayar la siguiente configuración:

[sshd]
enabled = false

[sshd-denyhosts]
enabled = true

port    = ssh
backend = systemd

filter = sshd
;banaction = hostsdeny[daemon_list=sshd]
banaction = hostsdeny

Advertencia

Usar /etc/hosts.deny es sólo una excentricidad para jugar un poco más con la configuración: no imite esta idea en un servisdor real. De hecho, fail2ban no parece andar del todo fino usando este modo de veto.

Notas al pie