Un serveur des fois, c'est comme un gosse. Et quand vous lui demandez le soir comment s'est passée la journée, il se peut qu'il vous dise qu'une petite brute a essayé de lui voler son goûter à la récré. Mais la différence majeure c'est que sur un serveur vous le verrez dans les logs, et vous aurez même l'adresse de la petite brute. Pourquoi ne pas alors essayer de l'empêcher de recommencer ?
(bon, comparaison à deux sous en guise d'intro : check)
En lisant les rapports logwatch générés quotidienne depuis mon aggrégateur RSS auto-hébergé, cela fait déjà plusieurs mois que j'ai pu constater que mon port 22 était régulièrement approché par des IP peu recommandables, qui tentaient évidemment d'en profiter pour s'introduire dans le système.
Mais j'y songe, comment lis-je mes rapports logwatch depuis mon aggrégateur RSS vous demanderez-vous peut-être ?
logwatch-rss
J'ai réalisé il y a quelques temps que je consultais plus facilement mon aggrégateur que ma boîte mail, surtout celle du serveur (qui n'est qu'une boîte "système", je n'ai pas encore franchi le pas de l'email auto-hébergé). Je me suis logiquement dit qu'il serait donc plus pratique de "recevoir" via un flux RSS le rapport système généré par logwatch que je consulte tous les jours.
Le principe serait de modifier le job CRON qui exécute la génération afin qu'il écrive le résultat dans un fichier texte dans un dossier dédié en plus de le renvoyer sur la sortie standard (de manière à conserver la réception par mail en parallèle). Un petit script PHP dans le dossier "logwatch-rss" situé dans la racine de mon serveur web va ensuite construire un flux Atom à partir des fichiers texte, permettant leur lecture depuis n'importe quel aggrégateur.
Pour moi qui place les fichiers rapports dans le dossier /var/logwatch-archives, le job CRON devient donc ceci :
20 2 * * * root /usr/sbin/logwatch | tee /var/logwatch-archives/logwatch_$(date +%Y-%m-%d).txt
Le script PHP correspondant est disponible sur mon GitHub. La configuration demandée est minimale : il suffit de préciser le chemin vers le dossier contenant les fichiers textes. Le reste est automatique. Dans mon cas, je peux ensuite vérifier le flux en accédant à la page http://usul/logwatch-rss/. C'est aussi cette adresse que j'ai renseignée dans mon aggrégateur.
Le résultat est le suivant dans mon rsslounge :
Mon flux logwatch-rss sur rsslounge
(ici on peut voir "localhost" dans l'adresse du flux car évidemment le serveur interrogé est la machine elle-même)
Désormais, lous les matins en consultant mes flux, il me suffit d'ouvrir celui provenant de logwatch-rss pour lire tranquillement le rapport généré.
Ceci fait, revenons au sujet principal.
Le retour de la petite brute
La méthodologie expliquée ici s'inspire fortement de l'article de daemonkeeper.net.
Mon serveur, étant accessible depuis le grand Nain Ternet est - comme toutes les machines dans ce cas - la cible de bots tentant de pénétrer les systèmes un peu trop ouverts dans le but d'en dérober des données ou plus souvent d'en faire des membres forcés d'un botnet. En ce qui me concerne je ne crains pas vraiment les attaques en brute-force car mes mots de passes sont suffisamment complexes, et surtout sont couplés à une politique d'exclusion assez sévère : au bout de 3 essais échoués, l'IP est placée en liste noire pour une heure. Je ne dis pas que ce serait suffisant pour résister à une attaque d'envergure provenant d'IP multiples, mais ici ma modeste machine ne représente un challenge intéressant pour aucun pirate sain d'esprit (à raison), je suis donc plutôt tranquille.
Il reste cependant toujours frustrant de constater ces attaques sans pouvoir rien faire. D'autant que grâce à logwatch et au reverse DNS, j'ai remarqué qu'une majorité d'IP provenait de Chine, d'Europe de l'Est/Russie et des États-Unis, trois régions du globe dont je n'attends aucune connexion légitime.
Serait-il possible via des règles iptables adaptées d'exclure des plages IP ? Et si oui, où trouver les plages IP correspondant aux pays que je souhaite bloquer ?
Après une rapide recherche, je réalise que je ne suis évidemment pas le seul à m'être posé cette question.
Problème : il n'y a pas une plage d'IP par pays mais de (très) nombreuses, et s'il faut les ajouter sous forme de règles individuelles dans iptables on n'est pas sorti de l'auberge. D'autant que j'apprends qu'iptables n'est pas du tout adapté à ce genre d'utilisation et un nombre important de règles entraîne de forts ralentissement dans le traitement des paquets. L'utilisation d'ipset est suggérée et recommandée. Je me suis donc renseigné sur son utilisation.
Sur Debian Wheezy, il suffit d'installer le paquet éponyme :
# apt-get install ipset
Le module noyau correspondant est déjà présent, il n'y a rien à faire. Pour Squeeze (et Ubuntu 10.04 chez moi avec Sihaya) il est possible de le compiler et de l'installer séparément, ce qui prend 5 minutes avec module-assistant (les deux commandes permettant de ce faire sont disponibles ici). Attention cependant dans la suite de cet article, vous devrez adapter la syntaxe des commandes ipset car elle a changé entre la version fournie avec Squeeze et Wheezy (man ipset is your friend).
Je reste toujours admiratif de l'imagination et du talent graphique déployés par les concepteurs de ces logiciels si complexes et puissants pour réaliser leurs logos
IPDeny : au pays des listes
Tout d'abord il faut récupérer les plages IP des pays que l'on souhaite ajouter aux règles. IPDeny fournit des archives de fichiers "zone" au format texte répondant exactement à ces critères (attention, l'archive all-zones.tar.gz proposée est en fait vide). Après réflexion, plutôt que de constituer une liste noire, j'ai préféré créer une liste blanche de pays autorisés en incluant dans un premier temps tous les pays d'Europe de l'Ouest (où je pourrais potentiellement voyager à moyen terme, il ne s'agirait pas que je me bloque moi-même !).
J'ai téléchargé l'intégralité des fichiers zones disponibles ainsi que le très pratique script aggregate-cidr-addresses.pl (apt-get install perl pour pouvoir l'exécuter évidemment). Une fois tous les fichiers dans un dossier, il permet en effet de fusionner en un fichier unique toutes les lignes de plusieurs zones, tout en fusionnant aussi les plages IP qu'ils contiennent !
Dans mon cas, j'ai copié les fichiers des pays d'Europe de l'Ouest dans un dossier "ipset_whitelist" avec le script.
# ls -1
ad.zone
aggregate-cidr-addresses.pl
be.zone
ca.zone
ch.zone
de.zone
dk.zone
es.zone
fi.zone
fr.zone
gb.zone
gf.zone
gr.zone
ie.zone
is.zone
it.zone
local.zone
mc.zone
nl.zone
no.zone
pf.zone
se.zone
Puis j'ai lancé la fusion :
$ chmod +x aggregate-cidr-addresses.pl
$ ./aggregate-cidr-addresses.pl *.zone > whitelist.zone
Le fichier local.zone (qui n'est pas fourni par IPDeny) contient seulement la zone "locale" et les adresses privées qui font évidemment partie de la liste blanche :
$ cat local.zone
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
J'obtiens un fichier whitelist.zone de 260 ko environ (contre 480 ko pour la somme des poids des fichiers d'origine).
Création de la collection avec ipset
Avant d'ajouter ces plages à ipset, il faut initialiser la collection correspondante et lui donner un nom (j'ai choisi "geowhitelist") :
# ipset create geowhitelist hash:net
On peut vérifier à présent qu'elle existe et qu'elle est vide :
# ipset list geowhitelist
Name: test
Type: hash:net
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 16760
References: 0
Members:
À présent, pour traiter l'ajout des plages dans ipset et dans le cas où j'aurai cette action à refaire plus tard, j'ai créé un petit script qui prend deux paramètres en entrée : l'identifiant de la collection ipset (IPSET_SET_NAME), et le fichier zone source contenant les plages d'IP (INPUT_FILE). Le script exécute ensuite cette commande pour chaque ligne du fichier : ipset add $IPSET_SET_NAME <plage IP>.
# ./create_ipset_rule.sh geowhitelist whitelist.zone
Updating set "geowhitelist" using "whitelist.zone"
................................................................................................................................................................................................................................................................................................................................................................................................................................................................................... [...]
Done.
Set updated successfully (now approx. 16825 lines).
Voici le code source du script en question :
#!/bin/bash
if [ $(whoami) != "root" ]; then
echo Must be run as root.
exit 1
fi
if [ -z $1 ]; then
echo Missing set name
exit 1
fi
IPSET_SET_NAME=$1
if [ -z $2 ]; then
echo Missing filename
exit 1
fi
INPUT_FILE=$2
echo Updating set "$IPSET_SET_NAME" using "$INPUT_FILE"
for cidr in $(cat $INPUT_FILE); do
ipset add $IPSET_SET_NAME $cidr
return=$?
if [ $return != 0 ]; then
echo
echo ipset returned $return
exit 2
fi
echo -n .
done
echo
echo Done.
linesCount=$(ipset list $IPSET_SET_NAME | wc -l)
echo "Set updated successfully (now approx. $linesCount lines)."
Le problème avec ipset est que ces collections sont éphémères et ne sont pas restaurées après un reboot. Dans mon cas ce n'est pas critique, mais autant faire les choses bien. Je dumpe donc la collection dans un fichier, lui-même dans un dossier que je crée préalablement :
# mkdir /etc/ipset
# ipset save geowhitelist > /etc/ipset/geowhitelist.set
Puis j'installe le script suivant dans /etc/network/if-pre-up.d/restore-ipset (source originale disponible ici)
#!/bin/sh
# Nanawel 2014-06-02 - Restores ipset rules when eth0 interface comes up
# http://blog.laimbock.com/2013/09/22/how-to-block-countries-with-ipdeny-ip-country-blocks-ipset-and-iptables-on-el6/
set -e
if [ "$IFACE" != "eth0" ]; then
exit 0
fi
# Only run from ifup.
if [ "$MODE" != "start" ]; then
exit 0
fi
# load ipset sets from /etc/ipset
# ipset set naming syntax is <setname>.set
find /etc/ipset -maxdepth 1 -type f -iname "*.set" | while read SET; do
/usr/sbin/ipset restore -! < $SET
if [ $? -eq 0 ]; then
logger -t ipset "success: restore of $SET"
else
logger -t ipset "fail : restore of $SET"
fi
done
exit 0
Note : n'oubliez pas d'adapter le nom de l'interface réseau à votre cas (chez moi c'est eth2 mais j'ai préféré mettre le plus classique eth0 ci-dessus).
Un petit
# chmod +x /etc/network/if-pre-up.d/restore-ipset
pour rendre le script exécutable, et on est bon ! À partir de maintenant, toute collection ipset précédemment enregistrée dans le dossier /etc/ipset avec l'extension ".set" sera chargée lors de l'activation de l'interface réseau eth0.
Un résumé de l'objectif de ce montage
Configuration de iptables
Nous avons donc une règle ipset qui est capable de distinguer les IP provenant de certains pays. Mais ipset seul ne fait rien, il faut le coupler avec iptables pour pouvoir manipuler le trafic correspondant.
Ma règle sera simple : je vais simplement demander à iptables d'ignorer (drop) toutes les connexions TCP à destination des ports 22, 80 et 443 (soit respectivement SSH, HTTP et HTTPS) dont l'adresse IP source n'appartient pas à la collection "geowhitelist". Pour cela je me place dans la table "filter" et dans la chaîne "INPUT". J'avais déjà utilisé iptables il y a 3 ans pour mettre en place des adresses IP "virtuelles" grâce à NETMAP, mais c'est beaucoup plus simple ici.
# iptables -t filter -A INPUT -p tcp -m tcp -m multiport -m set -j DROP --dports 22,80,443 ! --match-set geowhitelist src
Je détaille pour les deux du fond qui dorment :
-t filter | dans la table "filter" |
---|---|
-A INPUT | ajout (A) d'une règle sur la chaîne des paquets destinés à la machine (INPUT) |
-p tcp | si le protocole utilisé est TCP |
-m tcp | on charge le module "tcp" (pour la ligne précédente) |
-m multiport | on charge le module "multiport" (car le port sera en fait une liste de valeurs séparées par des virgules) |
-m set | on charge le module "set" (correspondant à ipset) |
-j DROP | on ignore le paquet |
--dports 22,80,443 | dont le port de destination est parmi 22, 80 et 443 (nécessite "-p tcp" au-dessus) |
! --match-set geowhitelist src | et dont l'adresse source (src) ne correspond pas (!) à une plage définie par la collection "geowhitelist" (--match-set) |
Pretty straightforward, isn't it Watson?
Pour vérifier, nous pouvons à présent lister les règles courantes de la table "filter" :
# iptables -t filter -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
DROP tcp -- anywhere anywhere tcp multiport dports ssh,http,https ! match-set geowhitelist src
Chain FORWARD (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
iptables nous remplace en plus gentiment les numéros des ports par leur équivalent en termes de services, plus lisible (voir /etc/services).
Ce résultat est à interpréter selon les règles déjà en place évidemment. Dans le cas où vous avez des règles de filtrage par port, il est nécessaire d'ajouter la règle AVANT celles pouvant exclure ou accepter les paquets correspondants. On utilisera alors à la place la commande d'insertion proposée par iptables ("iptables -I" au lieu de "iptables -A").
Ça y est, la règle est en place, les paquets correspondant aux critères que nous venons de définir sont désormais ignorés par Netfilter. Afin de tester l'efficacité du système, essayez de vous connecter à SSH depuis le réseau local, ou au pire de la machine elle-même : normalement vous n'aurez aucun problème puisque vous êtes dans la "geowhitelist" (rappelez-vous, en plus des pays nous avons aussi ajouté les plages d'adresses privées non-routables sur Internet).
Pour tester à présent que les connexions de la "zone noire" sont bien filtrées, et si vous n'avez pas de machine à disposition depuis un de ces pays, vous pouvez utiliser l'utilitaire nmap en ligne fourni par pentest-tools.com, dont l'adresse IP est apparemment localisée en Roumanie, donc hors de notre "geowhitelist".
Le test des ports via pentest-tools.com
Les ports sont bien marqués comme filtrés, preuve que la machine qui a scanné les ports n'a pas trouvé le service tournant réellement derrière.
(Note : ici le port 22 est marqué "open" mais nous verrons cela dans la suite, considérez qu'il est "filtered" lui-aussi)
Mise en place de Kippo avec la geowhitelist
Après la mise en place du filtrage précédent, j'ai pu constater l'efficacité de la technique : les tentatives d'accès frauduleux sur mon serveur ont en effet diminué d'au moins 90%.
C'était bien.
Mais après réflexion, je me suis demandé s'il n'était pas possible de se servir de cette capacité à discerner "le bon grain de l'ivraie" pour s'amuser un peu et observer ce que les attaquants tentent de faire une fois qu'un serveur leur ouvre leurs portes.
Fidèle (petit) lecteur de MISC depuis quelque années et m'étant déjà renseigné sur le sujet, je savais qu'il existait des applications adaptées pour tromper de manière sécurisée les attaquants du réseau : les honeypots, ou "pots de miel" dans la langue de Shakespeare (s'il était né dans le Royaume de France au lieu de cette île froide et humide au nord de l'Europe).
Le terme définit bien sa fonction : attirer les nuisibles en leur offrant un met de choix, de manière à ce qu'ils ne s'intéressent pas à ce qui est vraiment important. Le maître du honeypot a également tout loisir d'observer en toute sécurité les actions de ces nuisibles sans que ceux-ci ne s'en doutent ou ne puissent l'en empêcher. Cela permet de mieux comprendre les techniques d'attaque et de mieux s'en prémunir sur les infrastructures réelles.
L'art du honeypot se décline en plusieurs pratiques. Certaines consistent à monter des machines (virtuelles le plus souvent) possédant une faille de sécurité connue dans un environnement réseau contrôlé. Il peut s'agir d'une faille du noyau, d'un applicatif réseau (Apache, PHP, Tomcat, etc.) ou d'une application web (injection SQL, XSS). Il peut également s'agir de laisser une configuration par défaut un peu trop permissive. Les actions effectuées peuvent alors être générée à partir de captures réseau (Wireshark) et des logs de la machine elle-même, à laquelle on aura ajouté éventuellement des outils adaptés en ce sens.
D'autres pratiques consistent à utiliser un logiciel qui va imiter le fonctionnement d'un applicatif réseau qui va lui-même enregistrer les actions des attaquants. Cet applicatif ressemblera pour l'attaquant au logiciel réel qu'il imite, mais sans la possibilité pour celui-ci de le détourner réellement. Le but est de faire tomber l'attaquant dans le miel, pas de le laisser repartir avec le pot !
Ce qui est le plus divertissant et le plus facile à monter est un honeypot SSH. Divertissant, car il est possible de visionner - en direct ou en différé - les sessions des utilisateurs, c'est-à-dire les commandes tapées et leur retours tel que les voit ceux-ci. Facile, car il existe plusieurs logiciels "prêts à l'emploi" dont il suffit d'extraire l'archive et de lancer le binaire pour qu'il soient opérationnels. Les plus connus sont Kippo et Kojoney. J'ai opté pour Kippo pour la simplicité et le nombre de retours disponibles sur la Toile.
La version "officielle" est disponible sur GitHub mais après un test de quelques jours j'ai décidé d'utiliser à la place celle issue du fork par Michel Oosterhof car celle-ci ajoute notamment le "support" du protocole SFTP. En effet, la plupart des intrusions ne laissaient aucune trace après le login car l'attaquant tentait d'utiliser le SFTP, qui échouait. Il abandonnait donc immédiatement la connexion.
N'oubliez pas d'installer les dépendances requises avant de poursuivre.
Préparons le terrain et installons notre honeypot. Il nous faudra un utilisateur dédié et sans privilèges (disons que c'est quand même préférable !).
# useradd -d /home/kippo -m -s /bin/false kippo
# su -l kippo -s /bin/bash
$ cd
$ git clone https://github.com/micheloosterhof/kippo.git
$ cd kippo
Puis il faut initialiser la configuration à partir du modèle fourni :
$ cp kippo.cfg.dist kippo.cfg
Il est ensuite possible de modifier cette configuration, notamment le port utilisé (par défaut 2222) et le faux nom de la machine.
Dans la version de Michel Oosterhof, un utilisateur "root" est configuré avec le mot de passe "*" qui demande à Kippo d'accepter tous les mots de passe fournis. Cela étant, afin de conserver un comportement "réaliste" et éviter de trop éveiller les soupçons, il est préférable de n'en choisir qu'un parmi la liste des plus basiques (root, 123456, admin, etc.).
Puis pour lancer le service en tâche de fond :
$ ./start.sh
On peut ensuite se déconnecter et le honeypot continuera de tourner, attendant patiemment les connexions sur le port choisi. Attention toutefois, rien ne le redémarre automatiquement en cas de reboot. Vous pouvez alors soit créer un script de démarrage (init.d / rc.d / systemd) ou ajouter un job CRON appelant le script start.sh avec l'utilisateur kippo (@reboot).
Il faut à présent ajouter la règle iptables qui va rediriger les requêtes provenant de pays blacklistés vers le honeypot, tout en laissant évidemment les autres accéder au vrai service OpenSSH qui tourne sur le port 22.
L'objectif du montage avec le honeypot
Voici ce que ça donne (ma machine a l'adresse IP 192.168.1.100 sur mon LAN) :
# iptables -t nat -A PREROUTING -p tcp -m tcp -m set -d 192.168.1.100 --dport 22 -j DNAT --to-destination 192.168.1.100:2222 ! --match-set geowhitelist src
Toujours pour les deux du fond :
-t nat | dans la table "nat" |
---|---|
-A PREROUTING | ajout (A) d'une règle sur la chaîne exécutée avant de router les paquets (PREROUTING) |
-p tcp | si le protocole utilisé est TCP |
-m tcp | on charge le module "tcp" (pour la ligne précédente) |
-m set | on charge le module "set" (correspondant à ipset) |
-d 192.168.1.100 | si la destination du paquet est la machine elle-même |
--dport 22 | si le port de destination est 22 |
-j DNAT | on réalise une translation de la destination |
--to-destination 192.168.1.100:2222 | vers l'IP de la machine (pas de changement) et le port 2222 |
! --match-set geowhitelist src | pour tous les paquets dont l'adresse source (src) ne correspond pas (!) à une plage définie par la collection "geowhitelist" (--match-set) |
Comme la chaîne PREROUTING - de la table "nat" - est exécutée avant la chaîne INPUT - de la table "filter" (voir ci-dessous) - on n'a même pas à modifier la règle ajoutée dans la section précédente. Il n'y aura simplement plus de paquets qui arriveront dans INPUT à destination du port 22 à partir d'adresses IP en dehors de notre geowhitelist (par contre il restera toujours les paquets destinés aux ports 80 et 443 que l'on veut continuer de filtrer).
Source : knowplace.org
Le résultat est visible en listant les règles en place sur la table "nat".
# iptables -t nat -L
Chain PREROUTING (policy ACCEPT)
target prot opt source destination
DNAT tcp -- anywhere usul tcp dpt:ssh ! match-set geowhitelist src to:192.168.1.100:65022
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain POSTROUTING (policy ACCEPT)
target prot opt source destination
C'est fini ! Il ne reste plus qu'à patienter et consulter régulièrement les logs pour repérer les tentatives d'accès.
L'analyse des logs de Kippo
On trouve tout d'abord les traces complètes dans le fichier log/kippo.log : connexions, commandes envoyées, informations de débogage.
Ensuite, pour chaque accès réussi, un fichier log horodaté est également créé dans log/tty. Celui-ci conserve la trace complète de l'interaction dans un format qu'il est possible de "réexécuter" afin de le visionner. Pour cela on utilisera le script utils/playlog.py comme suivant (depuis le répertoire racine de Kippo) :
$ util/playlog.py log/tty/20140610-125839-2844.log
Le terminal déroulera alors le scénario de la session telle qu'elle a été enregistrée par le honeypot directement dans le terminal. Il s'agit évidemment d'un visionnage qui n'interagit aucunement avec le terminal courant ni la machine réelle, simplement une série de lignes de texte , un peu comme celles qu'on peut trouver sur asciinema.
Les fichiers téléchargés par les attaquants sont quant à eux placés dans le dossier dl/ en racine de Kippo pour une inspection ultérieure. Pour avoir moi-même procédé à une analyse de quelques-uns avec VirusTotal, il s'agit évidemment la plupart du temps de binaires de backdoors pour Linux, en 32 ou 64 bit. Pour la sécurité, Kippo change automatiquement leurs droits en 600, ce qui évite de les exécuter par erreur ! Attention : ClamAV ne les détecte que rarement à ce jour, l'analyse régulière du dossier avec cet antivirus ne remontera donc que rarement une alerte.
Il est possible aisément d'adapter ou compléter votre honeypot en modifiant les fichiers dans le dossier textcmds/ ou en modifiant les fichiers du faux système de fichiers en recréant son image (fs.pickle). Les informations sont disponibles sur le GitHub du projet et un peu partout sur la Toile.
Une dernier conseil : si comme moi vous vous retrouvez avec plusieurs dizaines de tentatives d'accès réussies chaque jour, il est préférable de mettre en place un logrotate sur le fichier log/kippo.log, et de supprimer automatiquement les fichiers téléchargés dans dl/ régulièrement. Il serait dommage que votre honeypot soit à l'origine d'un crash de votre machine réelle à cause du manque d'espace ! Vous pouvez aussi mettre en place un quota pour cet utilisateur, ou monter son dossier home sur un RAMdisk de taille fixe... le choix est large.
Amusez-vous bien :)