Como agregar la lista negra de AbuseIPDB al Firewall de Linux o Ubuntu

[Tutorial] Como agregar la lista negra de AbuseIPDB al Firewall

por | Oct 21, 2019 | Seguridad | 16 Comentarios

La lista negra de AbuseIPDB es una gran herramienta que nos permite conocer cuales son los clientes con más reportes de abuso reciente. Si usamos esto a nuestro favor, podemos evitar toda clase de ataques.

En esta pequeña entrada, hablaré de como hacer uso de dicha base de datos para proteger un servidor corriendo Linux. Y en mi caso Ubuntu.

La base de datos que ofrece AbuseIPDB nos permite saber todo un historial sobre una dirección IP. Cuantas veces ha sido reportada, por quien e incluso que tipos de ataques ha realizado.

Esta información es especialmente útil si contamos con otras herramientas como fail2ban. Esto para proteger de forma eficiente un servidor.

Pero a veces, no queremos consumir una dirección a la vez. Tampoco queremos solo reportar quienes están intentando vulnerar un recurso.

Es ahí en donde la lista negra de AbuseIPDB entra en acción.

¿Qué información contiene la lista negra de AbuseIPDB?

La lista negra, en pocas palabras, solamente contiene una lista de los clientes con mayor actividad. De los clientes con el mayor número de reportes desde múltiples fuentes.

Por lo cual se vuelve una gran herramienta a la hora de detener cualquier dirección que intente vulnerar un servidor. Ya que al saber su historial, es fácil de deducir que seguramente que lo harán.

Dicho recurso, al ser consultado, nos regresa una lista de hasta 10,000 direcciones altamente peligrosas para la versión gratuita.

Si bien no son todos los clientes mal intencionados, si son una cantidad importante. Nada despreciable considerando el tipo de actividad que realizan.

¿Cómo aprovechar esta lista?

Gracias a que AbuseIPDB ofrece puntos de acceso fáciles de usar, nosotros podemos acceder a la lista negra usando el siguiente endpoint: https://api.abuseipdb.com/api/v2/blacklist

Sin embargo, para poder consultarla debemos de estar registrados y obtener una llave API. Si no la tienes, puedes crearte una cuenta en minutos. Así puedes obtener to llave API.

Pero el truco real está en implementar directamente la lista en el firewall de cualquier servidor. Eso se puede lograr usando scripts y cron jobs. De esta forma, es posible actualizar la lista en el servidor una vez al día.

Para no sobrecargar el servidor, debemos usar las herramientas iptables e ipset. Ya que soluciones como fail2ban son simplemente muy lentas a la hora de agregar gran número de direcciones.

El programa ipset nos permite agregar cantidad de direcciones IP y manejarlas fácilmente mediante colecciones. Todo esto utilizando pocos recursos.

Un ejemplo práctico de la implementación

El agregar las direcciones conflictivas al cortafuegos es sencillo. En mi caso, he creado un script en python3 que lo hace.

Para ello, utilizo también una colección creada con ipset. La cual se limpia antes de cualquier actualización.

De esa forma, los clientes que ya no se encuentran en la lista negra de AbuseIPDB son descartados. Aunque es posible mantenerlos, para fines prácticos de esta ejemplo, no se explora esa posibilidad.

Así, este es el script que se corre cada día y que actualiza los datos desde AbuseIPDB:

Explicación del script de la lista negra

El script de python3 anterior realiza una serie de acciones antes de consultar a AbuseIPDB para agregar las direcciones. Se explican los puntos más importantes del programa:

#ejecuta: ipset flush blacklist
run([ipset_bin, 'flush', set_name])

Esta línea corre un subproceso, un comando, invocando a /sbin/ipset para indicarle que limpie la lista negra local de todos los ips que tiene en ella.

#ejecuta: ipset create blacklist iphash -exist
run([ipset_bin, 'create', set_name, 'iphash', '-exist'])

Si no existe una colección creada por ipset, esta será crea. Esta usará un método de hash ideal para direcciones aleatorias. Si la colección ya existe, no pasa nada.

#Consulta el API mandando las credenciales en las cabeceras
response = requests.get(blacklist_url, headers=request_headers)

Entonces, realizamos la consulta y obtenemos el grupo de direcciones que queremos agregar al cortafuegos. En este caso, los datos están en formato json y más adelante son convertidos para ser usados por el script.

#ejecuta: ipset add blacklist <direccion_ip> -exist
run([ipset_bin, 'add', set_name, client_data['ipAddress'], '-exist'])

Finalmente, procedemos a agregar las direcciones de la lista de AbuseIPDB en la colección. Con la opción -exist nos aseguramos de que, en el remoto caso de que exista un duplicado, no nos muestre algún error.

#ejecuta: iptables -I INPUT -m set --match-set blacklist src -j DROP
run([iptables_bin, '-I', 'INPUT', '-m', 'set', '--match-set', set_name, 'src', '-j', 'DROP'])

Esto es entonces lo que hace la magia. Con este comando le decimos al cortafuegos que use la colección que hemos creado para las conexiones entrantes. Si la IP que se intenta conectar está en la lista negra, simplemente será ignorada de forma silenciosa.

Recuerda, solo usa este comando la primera vez que ejecutes el script en la máquina o servidor. O solo cuando esta se haya reiniciado.

Agregando soporte para IPv6

La versión que se ha mostrado antes funciona bien cuando solo hay direcciones IP de la versión 4. Sin embargo, hay ocasiones en las cuales la lista negra de AbuseIPDB contiene direcciones IP de la versión 6.

Si bien el script no detecta el tipo de IP, esto no afecta el funcionamiento ya que simplemente esas direcciones no son agregadas y continua la ejecución.

¿Pero que pasa si también quiero bloquear esas direcciones?

Por fortuna, esto se puede solucionar fácilmente. Ya que tanto ipset como iptables tienen soluciones para estas direcciones. Junto con python3.

Para aceptar las direcciones IPv6, primero debemos de crear un set con la opción family definida en inet6. Esto, se vería así en la consola:

ipset create set_name iphash -exist family inet6

Con esto creamos el set para agregar direcciones de la versión 6 al cortafuegos. Traduciendo la instrucción a python, quedaría de la siguiente manera:

run([ipset_bin, 'create', set_name, 'iphash', '-exist', 'family', 'inet6'])

Pero aún queda un paso importante: ¿Cómo saber si una IP es IPv4 o IPv6? Eso lo debemos de saber antes de saber a cual lista se va a agregar la dirección.

Por fortuna, python3 tiene una librería llamada ipaddress que sirve para esos casos.

Usando esa librería, es fácil saber datos de una dirección IP. Solo hace falta hacer lo siguiente:

import ipaddress
ip_version = ipaddress.ip_address(remore_ip_address).version

Con esa librería, nosotros tenemos acceso a diversa información de una dirección. Esto en forma de un objeto.

Otro uso que le podemos dar a esta librería es para saber si una dirección es pública o no. Esto lo logramos usando el atributo is_global.

Agregando la detección IPv6 al script de lista negra de AbuseIPDB

Una vez que hemos resuelto este par de problemas, entonces nosotros podemos ponerlo todo junto en el script.

Con lo cual, nos debería quedar algo similar a esto:

Con este script, nosotros creamos los sets que necesitamos además de que se agregan de forma correcta a iptables.

Ojo, hay que recordar que un set de IPs de versión 6 requiere que se vincule usando el binario ip6tables en vez de iptables.

Igualmente, como está en el ejemplo, solo nos aseguramos de agregar al set todos los IPs que sean públicos. Esto usando el atributo is_global que da TRUE o FALSE dependiendo cada caso.

De esta forma evitamos que, por cualquier razón, se agreguen IPs privadas a la lista de elementos bloqueados del firewall.

Si bien AbuseIPDB.com tiene mucho cuidado en no agregar a su lista negra IPs que sean de carácter privado, siempre es bueno verificarlos.

Paso final: Crear un cron job para la lista negra

Hasta ahora todo bien. Ya tenemos un script que puede agregar al firewall direcciones IPv4 o IPv6, pero hasta este momento hay que correrlo manualmente.

Algo que ciertamente no es ideal en muchos de los casos. Lo mejor siempre va a ser que el mismo se ejecute de forma constante cada cierto tiempo.

Para eso, debemos de adaptarlo y agregarlo como parte de los trabajos del cron del sistema.

Pero para ello hay que tener en consideración algo: Los sets creados no son persistentes.

Esto significa que cada vez que se reinicie el equipo, estos van a desaparecer, por lo cual hay que volverlos a crear.

Este problema lo podemos solucionar de múltiples formas y una de ellas es el agregar un trabajo que se encargue de recuperar nuestra lista con cada inicio.

Con lo cual tendríamos dos scripts. Uno ejecutándose diario y otro solo al iniciar el servidor. Así podemos hacer algo tan sencillo como esto:

Puedes descargar estos archivos aquí:

Si has prestado atención, puedes ver que solamente se ha movido el código a una función (def) y que importamos en el primer script lo que necesitamos para llenar el set de ips.

De esta forma no necesitamos escribir de nueva cuenta el programa para ambos casos. Y así podemos agregarlo al cron usando el siguiente comando:

sudo crontab -e

Estando ya en el editor, deberemos de agregar las siguientes líneas:

0 3 * * * /full/path/to/importabuseipdbblacklist.py
@reboot python3 /full/path/to/loadabuseipdbonboot.py

Una vez hecho, ya estarán las tareas listas a ejecutarse cada vez que se reinicie el servidor, o una vez todos los días a las 3 de la mañana.

De esta forma, la lista negra siempre estará al día y cargada.

Nota: Ambos archivos, tanto importabuseipdbblacklist.py como loadabuseipdbonboot.py deben de estar en el mismo directorio.

Conclusiones sobre agregar la lista negra de AbuseIPDB

Agregar la lista negra de AbuseIPDB es relativamente sencillo. En especial si cuentas con las herramientas correctas.

Hay muchas formas de prevenir que clientes con mal comportamiento encuentren un servidor. Pero bloqueándolos antes de que tengan oportunidad es una de las mejores.

El usar herramientas como firewalld no siempre es lo mejor, por limitaciones que tiene la misma. A veces hay que hacer las cosas con comandos más cercanos al núcleo.

Finalmente, hay que recordar que esta solución no es realmente «persistente», ya que la lista desaparecerá cada vez que se reinicie el sistema.

Aunque se soluciona fácilmente, como lo es creando cron jobs, existen diversas formas de solucionarlo. Pero en cada caso, todo depende de las propias necesidades que tengamos.

Sin embargo, hay que preguntarse: ¿Qué tan seguido se reinicia un servidor? En mi experiencia, no mucho.

Pero quizá podamos dejar para otro día la implementación de un mecanismo persistente.

¡Únete a la conversación!

C

16 Comentarios

  1. Alejandro

    Hola, mi nombre es Alejandro.
    Al intentar ejecutar el script obtengo los siguientes errores

    ipset v6.30: The set with the given name does not exist
    ipset v6.30: Syntax error: cannot parse 2607:f298:5:114b::b54:d51: resolving to IPv4 address failed

    El fichero python fué cortado y pegado, solo le añadí la llave.

    Cuando termina de ejecutarse realizo iptables -L -n para ver si añadió algo y sigue igual que antes de ejecutar el comando.

    Estoy con raspbian 9.

    Saludos.

    Responder
    • Kadai Crosshansen

      ¡Saludos Alejandro!

      El primer error puede deberse a que, por alguna razón, ipset no crea el set correspondiente (líneas 20 y 21 borran y crean de nuevo la lista) o algo sucede al momento de agregar los elementos.

      Una forma de saber si el set existe es usando el comando: ipset list -t

      Este debe de mostrarte una lista completa con todos los sets existentes. Entre ellos debería de existir uno con información como esta:
      Name: abuseipdb-abusers
      Type: hash:ip
      Revision: 4
      Header: family inet hashsize 8192 maxelem 65536
      Size in memory: 283760
      References: 1

      Si existe, es posible que la regla no este activada. Para esto hay que correr manualmente: iptables -I INPUT -m set –match-set abuseipdb-abusers src -j DROP

      Así ya debería de darte una lista completa de los ips bloqueados con iptables -L -n (cuidado, puede ser extremadamente larga).

      En cuanto al error de «resolving to IPv4 address failed», simplemente el script no verifica si el IP es versión 4 o versión 6. Eso lo corregiré más adelante (ya lo había detectado, pero al ser direcciones IPv6 no me afectaban tanto… aún).

      Cuando tenga un poco más de tiempo procurare actualizar bien esta entrada con los detalles que haz comentado.

      ¡Muchas gracias por el comentario!

      Responder
      • Alejandro

        Hola de nuevo, gracias por tu rapida respuesta.

        Realizo ipset list -t

        name: abuseipdb_blacklist // difiere del nombre que pones mas arriba.

        Al realizar iptables para activar la regla, obtengo el siguiente mensaje.

        iptables v1.6.0: Set abuseipdb-abusers doesn’t exist.

        El fichero python lo ejecuto y no obtengo error pero sigue con lo mismo, no añade ninguna ip.

        ¿Es posible que el nombre diferente abuseipdb_blacklist o abuseipdb-abusers tenga algo que ver?

        Saludos y gracias.

        Responder
        • Kadai Crosshansen

          Después de correr el script, ¿Qué información obtienes al correr el siguiente comando?

          ipset list abuseipdb_blacklist

          Ya probé el script en mi entorno (por desgracia no tengo acceso a raspbian 9), pero no me muestra otro error más que el de IPv6, todo debería estar relativamente bien.

          Si después de correr el script y el comando que mostré (ojo, el nombre debe de coincidir con el que crea el script) hay una lista de IPs, entonces se esta cargando bien y está listo para agregarse al firewall.

          El script corre bien en Ubuntu 16.04 y ahí lo he vuelto a probar.

          Dime si continuas teniendo problemas, ya que pudiera deberse a otro problema especifico de raspbian.

          Responder
          • Alejandro

            Type: hash:ip
            Revision: 4
            Header: family inet hashsize 4096 maxelem 65536
            Size in memory: 165856
            References: 3
            Number of entries: 10000
            Members:
            XX.XX.XXX.XXX ……. Y un largo ecetera de ip.

            Te pongo tambien la primera linea de iptables -L -n

            DROP all — 0.0.0.0/0 0.0.0.0/0 match-set abuseipdb_blacklist src

            Esta regla esta 3 veces repetida. Pero no llega a añadir ninguna ip a iptables.

            Por lo que veo, descarga bien la lista de ip pero no es capaz de añadirla a Iptables.

            Ejecuto los procesos como root y el script python lo tengo en mi /home a la espera que funcione bien y llevarlo a un sitio mas adecuado.
            La verdad no entiendo python pero no creo que esto ultimo influya en el proceso.

            Saludos y Gracias de nuevo.

          • Kadai Crosshansen

            Oh, ya estaba funcionando en tu caso. El set de ips (creado por ipset) es un tipo de lista especial el cual se agrega por referencia a iptables. No vez una lista cuando usas iptables -L -n de los ips asociados al set, pero al estar vinculado mediante referencia es que ya está siendo aplicada.

            Si ya tienes varias instancias, puedes reiniciar tu raspberry. Eso debería de limpiar las tablas que existan en iptables y ipset y así puedes agregar de nueva cuenta la lista negra y solo una aparecerá.

            Pero por lo que comentas, ya estaba funcionando como debía.

  2. Alejandro

    Ok, muchas gracias, como me comentaste que viera la lista de ip con el comando iptables -L -n y el resultado fue exactamente el mismo que sin el script pense que no hacia nada.
    Ahora solo me falta añadirlo al cron para que lo ejecute todos los dias.

    Referente al segundo error, no volvio a darlo.

    Gracias por todo, espero no tener que molestarte de nuevo.

    Responder
    • Kadai Crosshansen

      Todo un placer ayudar!

      En cuando al error de las IPv6, tan pronto pueda estaré modificando el script para bloquearlas (como se debe).

      Y espero que este script te haya sido de utilidad!

      Responder
      • Alejandro

        Bueno, implemente el script en cron y tras unos días de observación, todo funciona bien, me dio de nuevo el error IPv6 confío en que lo arregles pronto.

        Y por supuesto que me fue de utilidad el script.
        Saludos.

        Responder
  3. Alejandro

    Buenas de nuevo.

    Intento realizar la modificacion para ipv6 y con el reinicio.
    Solo se usan los ficheros loadabuseipdbonboot.py y importabuseipdbblacklist.py

    Con ipset list -t veo que crea los dos set de ordenes, pero vacias de ip al reiniciar y cron ejecutar el fichero loadabuseipdbonboot.py obtengo estos errores. (desconozco si el error sera por la versión de python de mi equipo).

    Traceback (most recent call last):
    File «/usr/lib/python3/dist-packages/urllib3/connection.py», line 138, in _new_conn
    (self.host, self.port), self.timeout, **extra_kw)
    File «/usr/lib/python3/dist-packages/urllib3/util/connection.py», line 75, in create_connection
    for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
    File «/usr/lib/python3.5/socket.py», line 733, in getaddrinfo
    for res in _socket.getaddrinfo(host, port, family, type, proto, flags):
    socket.gaierror: [Errno -3] Temporary failure in name resolution

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File «/usr/lib/python3/dist-packages/urllib3/connectionpool.py», line 594, in urlopen
    chunked=chunked)
    File «/usr/lib/python3/dist-packages/urllib3/connectionpool.py», line 350, in _make_request
    self._validate_conn(conn)
    File «/usr/lib/python3/dist-packages/urllib3/connectionpool.py», line 837, in _validate_conn
    conn.connect()
    File «/usr/lib/python3/dist-packages/urllib3/connection.py», line 281, in connect
    conn = self._new_conn()
    File «/usr/lib/python3/dist-packages/urllib3/connection.py», line 147, in _new_conn
    self, «Failed to establish a new connection: %s» % e)
    requests.packages.urllib3.exceptions.NewConnectionError: : Failed to establish a new connection: [Errno -3] Temporary failure in name resolution

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File «/usr/lib/python3/dist-packages/requests/adapters.py», line 423, in send
    timeout=timeout
    File «/usr/lib/python3/dist-packages/urllib3/connectionpool.py», line 643, in urlopen
    _stacktrace=sys.exc_info()[2])
    File «/usr/lib/python3/dist-packages/urllib3/util/retry.py», line 363, in increment
    raise MaxRetryError(_pool, url, error or ResponseError(cause))
    requests.packages.urllib3.exceptions.MaxRetryError: HTTPSConnectionPool(host=’api.abuseipdb.com’, port=443): Max retries exceeded with url: /api/v2/blacklist (Caused by NewConnectionError(‘: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution’,))

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File «/home/alex/loadabuseipdbonboot.py», line 23, in
    import_abuseipdb_blacklist()
    File «/home/alex/importabuseipdbblacklist.py», line 46, in import_abuseipdb_blacklist
    abuseipdb_response = requests.get(abuseipdb_url, headers=request_headers)
    File «/usr/lib/python3/dist-packages/requests/api.py», line 70, in get
    return request(‘get’, url, params=params, **kwargs)
    File «/usr/lib/python3/dist-packages/requests/api.py», line 56, in request
    return session.request(method=method, url=url, **kwargs)
    File «/usr/lib/python3/dist-packages/requests/sessions.py», line 488, in request
    resp = self.send(prep, **send_kwargs)
    File «/usr/lib/python3/dist-packages/requests/sessions.py», line 609, in send
    r = adapter.send(request, **kwargs)
    File «/usr/lib/python3/dist-packages/requests/adapters.py», line 487, in send
    raise ConnectionError(e, request=request)
    requests.exceptions.ConnectionError: HTTPSConnectionPool(host=’api.abuseipdb.com’, port=443): Max retries exceeded with url: /api/v2/blacklist (Caused by NewConnectionError(‘: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution’,))

    Esta noche vere cuando cron ejecute el otro fichero importabuseipdbblacklist.py si obtengo algun error.

    Saludos.

    Responder
  4. Alejandro

    Kadai Crosshansen

    Olvida el post anterior, como buen despistado que soy, se me olvido ejecutar el comando ip6tables -I INPUT xxxx etc.

    A raiz de eso, al reiniciar salen todos esos errores.

    Si ejecutamos desde el terminal el comando al ejecutar el script no da error y añade (en mi caso) una entrada que es la ip6.

    Saludos.

    Responder
    • Kadai Crosshansen

      Buenas noches y disculpa por la demora. Que versión de Python 3 tienes? (usa python3 –version) El código fue probado en python 3.5.2 y el script necesita al menos python versión 3.3.

      Continuas teniendo el error, parece ser que había un problema con tu equipo al momento de tratar de conectarte al servicio de abuseipdb.com o hasta de resolución de nombres por parte de urllib3. Al parecer es un problema DNS.

      Por ejemplo, da muchas pistas una línea como esta:
      requests.exceptions.ConnectionError: HTTPSConnectionPool(host=’api.abuseipdb.com’, port=443): Max retries exceeded with url: /api/v2/blacklist (Caused by NewConnectionError(‘: Failed to establish a new connection: [Errno -3] Temporary failure in name resolution’,))

      Edit:
      Me acorde que usas una raspberry, por lo cual posiblemente el error se deba a que la Raspberry no tiene acceso a internet al momento de ejecutarse al iniciar el equipo. Ver: https://www.raspberrypi.org/forums/viewtopic.php?t=237691

      Ahí hay varias soluciones, por ejemplo al script con el @reboot en el cron le puedes poner lo siguiente:

      import time
      # Sleep for 30 seconds
      time.sleep(30)

      O configurar a la raspberry para que espere a la red (wait for network en raspi-config) y así no arroje el error.

      Responder
  5. Alejandro

    Hola, sin problema por la demora.

    Como te comento ya funciona correctamente.
    Efectivamente hubo problemas de conexión a internet en esas horas, cuando mas tarde ejecute la orden ip6tables para activar el set y despues correr el script ya no teníamos esos problemas.
    La pequeña duda que tengo yo ahora, es si es obligatorio ejecutar ip6tables e iptables para activar el set, pienso que no, viendo el script aunque no controlo python.

    Y sobre los consejos para el @reboot no suelo apagar el ordenador, aunque me va a costar poco implementarlos.

    Gracias por todo. Saludos.

    Responder
    • Kadai Crosshansen

      Buenas noches.

      ip6tables e iptables son solo necesarios después de crear el set (con ipset) o después de reiniciar. Esto para «vincular» el set a las reglas que usa el firewall. en sí, solo se trata de una utilidad que habla directamente con el kernel de Linux. Puedes aprender un poco más aquí: https://en.wikipedia.org/wiki/Iptables

      Responder
  6. RobbieTheK

    For Fedora, can this script be edited to populate /etc/hosts.deny (TCP Wrapper) or just use the ipset command? iptables doesn’t play nice with firewalld and the upcoming nftables replacement.

    Responder
    • Kadai Crosshansen

      Well, I have not tried writing it directly to a file, but it is possible that populating the /etc/hosts.deny and reloading the daemon may do the trick. Just I have never tested it on a Fedora install.

      In my case, this solution works (using the command) and being sure to dump the iptables before restart or using a database to keep it properly synced. Anyway, thanks for the comment!

      Responder

Enviar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Pin It on Pinterest

Share This