blog:brute-force_pourquoi_fail2ban_denyhosts_sshguard_and_co_sont_des_gadgets_inutiles..._ou_presque

Brute-force : pourquoi fail2ban, DenyHosts, sshguard and co sont des gadgets inutiles... ou presque

Mise à jour : quelques jours après la publication de ce billet, on apprend que des personnalités du spectacle ont vu leurs comptes cloud ainsi piratés et que leurs photos intimes circulent sur le Web (voir par exemple la nouvelle sur /. et les commentaires et recommandations d'Apple) — 2014/09/05.

Attaques par force brute (ou brute-force), tout le monde connaît : essayer de se connecter à un service, une application, whatever en énumérant tous les mots de passe possibles. Pour s'en prémunir, il existe pas mal d'outils dont fail2ban, DenyHosts et sshguard mais ce ne sont que les plus connus.

Lors d'une tentative de connexion qui échoue (au hasard mot de passe invalide ou utilisateur inexistant), une trace est laissée dans un journal mentionnant le plus souvent l'identifiant et l'adresse IP source (a minima).

Ces outils surveillent les journaux, cherchent ces messages et ajoutent des règles (on y reviendra) pour empêcher de nouvelles connexions… De nouvelles connexions, oui mais depuis cette adresse IP. Seuls les scripts kiddies sont impactés par ces mesures, les « vrais » badguys disposant eux de botnets et donc d'adresses IP sources nombreuses. Bref, ces outils ne protègent donc pas de grand-chose…

Tant pis, il est de bon ton de filtrer tout de même, filtrons. Pour finir sur leur inutilité relative, de tous les outils que je connais, tous ont un défaut rédhibitoire :

  • soit ils manipulent iptables ou directement Netfilter ie. nécessitent Linux,
  • soit ils compilent en dur les messages à chercher (sic !)
  • au pire ils proposent de se rabattre sur TCP Wrapper mais seul les daemons utilisant cette bibliothèque libwrap seront « protégés » ; typiquement inutile pour un serveur (et donc une application) Web…

En me replongeant dans la documentation de mon daemon syslog(3) favori — je veux dire syslog-ng — il s'avère que j'ai déjà bien plus qu'il ne m'en faut sous la main, pas besoin de lancer un n-ième daemon :

  • il propose le filtrage par expressions régulières,
  • il sait lire un fichier texte (ie. un autre fichier journal d'une application n'envoyant pas ses messages à syslog(3) mais directement dans ce fichier),
  • il sait écrire dans un tube nommé,
  • il sait envoyer des messages à un programme…

Commençons par un exemple reprenant les deux premiers points :

syslog-ng.conf
...
source s_jabber { file ("/var/log/ejabberd/ejabberd.log" default-facility(local7) default-priority(info) follow_freq(1) program_override(ejabberd) flags(no-parse)); };
source s_local {
   unix-dgram("/var/run/log" flags(syslog-protocol));
   file("/dev/klog" program_override("/netbsd"));
   internal();
};
...
filter f_auth { facility(authpriv,auth) and level(info..emerg); };
destination d_authlog { file("/var/log/authlog" perm(600) owner(root) group(wheel)); };
log { source(s_local); filter(f_auth); destination(d_authlog); };
 
filter f_ssh_bruteforce {
   filter(f_auth) and message(".*Failed password for invalid user [^\ ]* from ")
      or filter(f_auth) and message("Invalid user [^\ ]* from ")
      or filter(f_auth) and message("Too many authentication failures for ")
      or filter(f_auth) and message("User [^\ ]* from [0-9a-f\.:]* not allowed because not listed in AllowUsers");
};
destination d_bruteforce { file("/var/log/suspicious.log" owner(root) group(wheel) perm(0640)); };
log { source(s_local); filter(f_ssh_bruteforce); destination(d_bruteforce); };
filter f_ejabberd_auth { message ("Failed authentication for [^\ ]@kabs.homeunix.org from IP "); };
log { source(s_jabber); filter(f_ejabberd_auth); destination(d_bruteforce); };
filter f_owncloud { program(ownCloud) and message("{core} Login failed: user '[\^ ]*' , wrong password, IP:"); };
log { source(s_local); filter(f_owncloud); destination(d_bruteforce); };

À ce stade, j'ai un journal /var/log/suspicious.log agrégeant les erreurs d'OpenSSH et d'ejabberd. Sur ce modèle, je peux maintenant ajouter des daemons, services, applications… Il reste à parser ce fichier pour ajouter une ACL au mécanisme de protection de mon choix et donc adresser le dernier point évoqué ci-dessus.

Ce mécanisme de mon choix n'est autre qu'une table dans pf(4) mais cela importe peu, je veux juste lancer une commande avec un argument (au moins) : l'adresse à bloquer. On pourrait raffiner et donner d'autres arguments, par exemple pour choisir la table dans pf(4) (SSH ou Nginx ?) ou pour générer un message à syslog(3) signalant que telle adresse a été ajoutée… Notre imagination est notre seule limite !

Pour une machine IPv4-only et avec un tube nommé, un fifo, ça ressemble donc à :

syslogng2pf.sh
#!/bin/ksh
#
umask=077
fifo=${1:-/var/spool/sockets/syslog2pf}
 
# XXX trap "rm -f ${fifo}" INT TERM QUIT
if [ ! -f ${fifo} ]; then
   mkfifo ${fifo}
fi
 
while [ -e ${fifo} ]; do
   while read suspicious <${fifo}; do
      src_addr=$( echo ${suspicious} |\
         sed -e "s/.*Failed password for invalid user [^\ ]* from \([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\).*/\1/" \
            -e "s/.*User [^\ ]* from \([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\) not allowed because not listed in AllowUsers/\1/" \
            -e "s/.*Failed authentication for [^\ ]@kabs.homeunix.org from IP \([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\).*/\1/" \
            -e "s/.*{core} Login failed: user '[^\ ]' , wrong password, IP:\([0-9]*\.[0-9]*\.[0-9]*\.[0-9]*\).*/\1/" \
            -e "/^[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*$/!d" \
            -e "/^192\.0\.2/d" )
      pfctl -t bruteforce -T add ${src_addr}
      pfctl -t bruteforce -T show | grep -q {src_addr}
      if [[ $? -eq 0 ]]; then
         logger -i -p authpriv.info "$0: ${src_addr} added to bruteforce table"
         echo "   ${src_addr}" >> /etc/bruteforce.pf
      else
         logger -i -p authpriv.crit "$0: error while adding ${src_addr} to <bruteforce> pf(4) table"
      fi
   done
done

Ce script attend en argument le chemin vers le fichier et lit depuis ce dernier, non sans l'avoir créé ; il fait le ménage dans les messages pour ne garder que l'adresse IPv4 source (IPv6 est laissé en exercice au lecteur ; hint outre [0-9], une adresse IPv6 contient [a-f] et :) ; à la fin, il supprime tout ce qui n'est pas une adresse IPv4 valide ; enfin, il supprime les adresses de mon réseau (ici le /24 d'exemple, cf. 5737). Il ajoute l'adresse à la table avec pfctl(8) et vérifie qu'elle est bien présente dans la table ; enfin, il journalise.

En adaptant le morceau de fichier de configuration ci-dessus, ça tombe en marche.

Utiliser un fifo est surtout utile si la lecture peut être plus lente que l'écriture… est-ce vraiment utile ?

Remarque 1 : la simplicité d'Unix frappe encore : la lecture fonctionne que le fichier soit un fichier simple ou un fifo.

Remarque 2 : le lecteur avisé notera que Syslog-NG peut lancer une commande, il suffit alors de modifier le script pour lire stdin.

Remarque 3 : pour tester, remplacer le tube par /tmp/plop, lancer le script dans un terminal et envoyer des messages (echo “blabla 1.2.3.4” » /tmp/plop) depuis un autre. On voit bien apparaître les lignes sur le premier terminal :

pfctl -t bruteforce -T add 1.2.3.4
pfctl -t bruteforce -T add 1.2.3.5
pfctl -t bruteforce -T add 1.2.3.6

Remarque 4 : j'ai volontairement ajouté en commentaire la commande « stupide » trap : quand le processus meurt, le fichier est supprimé… son contenu est alors perdu, c'est donc tout sauf une bonne idée.

Voilà, il ne reste plus qu'à « empiler » : faire transiter les messages de nouvelles applications, daemons, programmes, whatever par syslog(3), filtrer dans syslog-ng.conf puis dans le script.

NB : l'avantage des programmes que je fustigeais au début réside dans la mémorisation de la date d'ajout pour pouvoir programmer une date de fin. Une tâche en crontab(5) fait presque aussi bien.

  • blog/brute-force_pourquoi_fail2ban_denyhosts_sshguard_and_co_sont_des_gadgets_inutiles..._ou_presque.txt
  • Dernière modification : 2014/09/04 23:17
  • de pc