close icon
close icon

    Commandes unix pour la manipulation de lignes de texte


    Parfois, vous n'avez pas d'outil pour consulter les événements applicatifs. D'autre fois, on ne veut même pas vous y donner accès. Par contre, on aura bien voulu vous donner une archive bourrée de fichiers de logs sur un poste dédié. Mais que faire avec cet amas d'octets quand on a qu'un système unix entre les mains ? Regardons ensemble.

    Comme sujet de pratique, nous allons explorer les logs d'un honeypot pour trouver des traces d'attaques et voir d'où elles peuvent venir (géographiquement). Un honeypot dans le domaine de la sécurité est une machine destinée à être attaquée pour observer des comportements malveillants. Nous nous baserons sur les logs disponibles sur ce site : http://old.honeynet.org/scans/scan34/. Le plus souvent, dans un environnement où ce type de besoin est fréquent, vous serez équipés d'outils qui feront toutes ces étapes pour vous et agiront même selon les événements de sécurité. Mais il arrive que ces outils ne soient pas présents, ou que vous ayez besoin de sortir des sentier battus proposés par les-dits outils. Plus généralement ces commandes sont des connaissances de base en système unix. Elles vous serviront autant pour de l’administration du système que pour des tâches spécialisées comme l’audit d’une base de code par exemple.

    Ce que vous pourrez faire à l'issue de cet article :

    Avant propos

    Dans cet article certaines commandes nécessitent l'utilisation d'expressions régulières (aussi appelées regex). Je ne décrirai pas la façon de les utiliser. Mais sachez, si vous vous documentez sur le sujet, que les différents langages n'ont pas exactement les mêmes notations.

    À la fin de cet article vous trouverez une fiche rapide de référence sur toutes les commandes que nous aurons vues.

    Pour toutes les commandes que nous allons voir, je vous encourage à lancer un man (pour manuel) pour voir à quoi ressemble la doc de la commande en question. Chercher dans cette doc les éléments que je vais vous présenter vous donnera un réflexe qui vous servira pour toujours ! Enfin tant que vous serez dans un shell unix.

    Exemple pour consulter la doc de la commande echo :

    $ man echo
    
    NAME
        echo -- write arguments to the standard output
    
    SYNOPSIS
        echo [-n] [string ...]
    
    DESCRIPTION
    [...]
    

    Une fois dans la doc vous pouvez également faire des recherches avec la commande /. Par exemple si je cherche des informations sur les retours à la ligne, une fois dans la page de manuel de echo je tape la touche / suivi du mot clé que je cherche :

    /newline
    

    Commençons par ouvrir l'archive

    Après avoir téléchargé le fichier SotM34-anton.tar.gz, nous allons l'ouvrir. Selon l'extension, tar signifie que c'est une archive de type tar qui peut être extraite avec le programme du même nom, et gz signifie que l'archive est compressée (l'extension tgz est une contraction avec les mêmes significations). Nous l'ouvrons alors avec cette commande :

    $ tar xzf SotM34-anton.tar.gz
    

    Dans le détail cette commande est composée de :

    Après avoir exécuté cette commande, vous devriez voir apparaitre le dossier SotM34 à côté de l'archive. Celui-ci contient les logs qui nous intéressent.

    N'oubliez pas de faire un man tar pour retrouver les options présentées ici !

    Localiser des traces d'attaques

    Maintenant que nous avons nos logs, commençons par les traces les plus simples et évidentes à trouver : une attaque brute-force sur le login de la machine. Cherchons alors des événements répétés dans les logs secure de syslog qui contiennent les événements de login.

    Après un cd syslog, affichons chaque ligne des fichiers secure :

    $ cat secure*
    

    Un extrait du résultat:

    Mar 13 16:26:09 combo sshd[8714]: Accepted password for test from 59.120.2.133 port 57019 ssh2
    Mar 13 16:26:09 combo sshd[8716]: Accepted password for test from 59.120.2.133 port 57023 ssh2
    Mar 13 16:26:09 combo sshd[8721]: Accepted password for test from 59.120.2.133 port 57051 ssh2
    Mar 13 16:26:09 combo sshd[8720]: Accepted password for test from 59.120.2.133 port 57049 ssh2
    [...]
    

    Ce qui s'affiche est tout le contenu des fichiers. On peut voir plusieurs lignes différentes, mais chercher les traces d'attaque dans ce résultat est fastidieux. Voyons comment extraire cette information de ces lignes de texte.

    Filtrer les lignes qui indiquent l'utilisation d'un mauvais mot de passe

    Nous allons faire en sorte que seules les lignes avec des tentatives échouées de login pour mauvais mot de passe s'affichent. On voit dans les lignes affichées avec la commande précédente que certaines lignes ressemblent à ça :

    Feb  5 04:32:25 combo sshd[9703]: Failed password for nobody from 210.3.2.8 port 53044 ssh2
    

    La partie commune à toutes les tentatives échouées est donc : Failed password. Nous allons alors filtrer les lignes pour n'avoir que celles qui contiennent ces deux mots.

    $ grep 'Failed password' secure*
    

    Un extrait du résultat:

    secure:Mar 13 22:50:55 combo sshd[9356]: Failed password for root from 67.103.15.70 port 55639 ssh2
    secure:Mar 13 22:51:07 combo sshd[9358]: Failed password for root from 67.103.15.70 port 55895 ssh2
    secure:Mar 13 22:51:18 combo sshd[9360]: Failed password for root from 67.103.15.70 port 56110 ssh2
    [...]
    

    Mais comment savoir combien de tentatives échouées le serveur à subit ?

    Obtenir le nombre de lignes affichées

    Pour connaître le nombre de lignes, j'ai utilisé une autre commande : wc (word count). Cet utilitaire permet de compter les mots, lignes, nombre de caractères et bytes. Pour connaître le nombre de lignes dans le fichier secure.1 :

    $ wc -l secure.1
    

    Le résultat:

    234 secure.1
    

    Sauf que le résultat de grep n'est pas un fichier. Pour la plupart des commandes que je vous présente dans l'article, celles-ci peuvent être exécutées sur des fichiers, mais également sur l'entrée standard. L'entrée standard c'est le nom que l'on donne aux "champs" qui nous permettent de donner des informations à des programmes. Par exemple quand vous tapez une commande, vous la donnez à l'entrée standard. Essayons en tapant wc -l seul : le programme sera en attente d'informations. Vous pouvez alors taper tout ce que vous voulez et même revenir à la ligne. Pour sortir de l'entrée standard il faut notifier wc que nous avons terminé avec la combinaison de touches ctrl-D qui, sous un système unix, envoie le signal EOF pour End Of File. Vous verrez alors un résultat qui ressemble à celui que vous avez obtenu en donnant un fichier à wc

    $ wc -l
    Un exemple
    Avec
    Des lignes
            3
    

    Cependant, ce que donne grep en résultat vient sur la sortie standard (et non l'entrée). Pour envoyer la sortie standard d'un programme sur l'entrée standard d'un autre, Unix propose le pipe noté |. Si on chaîne les commandes grep et wc nous avons alors :

    $ grep 'Failed password' secure* | wc -l
        702
    

    Le caractère | sera très souvent utilisé tout le long de cet article car il permet de combiner autant de commandes que l'on veut. Et si vous tentiez d'utiliser la commande grep à nouveau retrouver les tentatives échouées ? Et cette fois-ci, tentez d'utiliser pipe, sans donner directement des fichiers à grep.

    Trouver les IP des attaquants

    Dans le résultat de la commande grep, nous pouvons voir plusieurs IP différentes. Certaines tentatives échouées pourraient être anecdotiques, mais d'autres bien plus nombreuses ressemblent plutôt à un comportement malveillant.

    Nous allons alors extraire les IPs de nos attaquants avec la commande grep à nouveau, mais cette fois-ci avec une option permettant de ne garder en sortie standard que la condition qu'on lui spécifie :

    $ grep 'Failed password' secure* | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
    

    Un extrait du résultat:

    67.103.15.70
    67.103.15.70
    67.103.15.70
    67.103.15.70
    67.103.15.70
    67.103.15.70
    67.103.15.70
    67.103.15.70
    128.59.112.2
    [...]
    

    Nous avons alors réussi à extraire les IPs !

    Compter les apparitions de chaque IP

    Toutes ces IPs c'est bien mais wc ne permet de compter qu'un nombre total de lignes. Il faut alors compter en distinguant les lignes qui se ressemblent ! On va alors utiliser la commande uniq qui permet de distinguer les lignes similaires :

    $ grep 'Failed password' secure* | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | uniq -c
    

    Un extrait du résultat:

    [...]
     9 202.110.184.100
     2 67.103.15.70
    17 210.125.27.175
    14 202.110.184.100
    

    Le résultat est déjà plus intéressant, mais on remarque que les mêmes IPs apparaissent plusieurs fois. uniq -c ne distingue que les lignes identiques consécutives, il faudrait trouver un moyen de regrouper toutes les lignes identiques. On va pour cela utiliser la commande sort juste avant que uniq prenne la main :

    $ grep 'Failed password' secure* | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort | uniq -c
    

    Un extrait du résultat:

    23 128.59.112.2
     1 148.228.20.76
     8 198.107.38.61
    [...]
    

    Et pour aller plus loin nous allons même retrier les lignes pour avoir les IPs les plus présentes vers la fin de la liste :

    $ grep 'Failed password' secure* | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort | uniq -c | sort
    

    Un extrait du résultat:

    1 148.228.20.76
    1 82.76.137.124
    2 202.69.66.162
    2 211.141.89.73
    [...]
    

    Une pause

    Notre ligne de commande commence à être assez longue. Nous allons donc stocker nos résultats.

    On pourrait stocker le résultat dans un fichier si on voulait s'en resservir plus tard :

    $ grep 'Failed password' secure* | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort | uniq -c | sort > bad_ips
    

    Pour se resservir du fichier, il suffit de se resservir de cat sur le fichier pour reprendre là où on en était.

    On pourrait également stocker le résultat dans une variable qui ne durera que le temps que notre terminal sera ouvert :

    $ bad_ips=$(grep 'Failed password' secure* | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | sort | uniq -c | sort)
    

    Pour se resservir de la variable nous pouvons utiliser

    $ echo "${bad_ips}"
    

    Pour la suite, j'utiliserai la variable, mais n'hésitez pas à utiliser la version fichier, le résultat sera le même !

    Sélectionner les 5 IPs les plus "agressives"

    Dans cette liste d'IPs, toutes ne sont pas nécessairement intéressantes. Ici on va se concentrer sur l'extraction des 5 IPs les plus représentées dans les tentatives échouées. Notre liste étant déjà triée par ordre croissant, il faudrait que nous ne puissions afficher que les 5 dernières lignes. Pour cela, nous allons utiliser la commande tail :

    $ echo "${bad_ips}" | tail -n 5
    

    Le résultat:

    32 81.56.82.49
    80 222.237.79.237
    80 62.193.229.186
    80 66.79.166.130
    93 202.110.184.100
    

    Nous avons donc les lignes, mais il nous faudrait uniquement les IPs, pas le nombre. Nous allons donc réécrire les lignes pour n'avoir plus que les IPs. awk est un programme qui permet d'effectuer des actions sur les lignes, un peu comme une fonction map. Lorsqu'il est exécuté :

    $ echo "${bad_ips}" | tail -n 5 | awk -F ' ' '{print $2}'
    

    Le résultat:

    81.56.82.49
    222.237.79.237
    62.193.229.186
    66.79.166.130
    202.110.184.100
    

    Voilà nos 5 IPs !

    Localiser les IPs

    Et si nous tentions d'obtenir la localisation de ces 5 IPs ? Un service gratuit https://hackertarget.com/geoip-ip-location-lookup/ permet d'appeler une API (max 100 appels par jour) pour obtenir la localisation d'une IP. Ce type de service est notamment utilisé en détection de fraude, par exemple par des partenaires de paiement pour des sites de billetterie, afin de désactiver l'accès d'une région à un service si le taux de fraude de cette provenance est trop important. L'url proposée s'utilise en ajoutant l'IP à la fin de l'URL, dans les query parameters : https://api.hackertarget.com/geoip/?q=1.1.1.1 . Nous utiliserons donc la commande curl qui permet de faire des appels http de cette façon : curl https://api.hackertarget.com/geoip/?q=1.1.1.1

    Jusque là nous ne faisions que transmettre la sortie standard vers l'entrée standard, mais dans ce cas nous ne pourrons pas simplement laisser la commande se débrouiller avec cette entrée car elle doit faire quelque chose de spécifique : ajouter le paramètre à la fin de l'URL. Deux méthodes sont dans ce cas possible. La première méthode implique de reformarter les lignes avec awk, qui est un outil que nous avons déjà vu. Nous allons donc en utiliser une seconde qui consiste à utiliser xargs pour arranger des paramètres pour une autre commande. Le nombre d'appels au service étant limité, je vous propose de stocker le résultat dans un fichier ou une variable au choix pour s'en resservir sans refaire les appels http.

    $ echo "${bad_ips}" | tail -n 5 | awk '{print $2}' | xargs -I{} curl -w "\n" https://api.hackertarget.com/geoip/?q={} > bad_ips_location
    

    Un extrait du résultat après un cat bad_ips_location:

    [...]
    IP Address: 62.193.229.186
    Country: France
    State: Provence-Alpes-Cote d'Azur
    City:
    Latitude: 43.2854
    Longitude: 5.3761
    [...]
    

    Et nous avons différentes informations comme le pays et la région d'où proviennent les IPs. Et voilà !

    Il faut cependant noter qu’une IP ne donne pas nécessairement la véritable adresse de l’attaquant. D’un part elle pourrait être celle d’un VPN utilisé pour cacher l’origine réelle. D’autre part cette IP pourrait être celle d’un zombie, c’est à dire un appareil piraté dans le but d’émettre des attaques (comme une webcam dont on aurait pas changé le mot de passe par défaut). Dans le cas d’un déni de service, ces appareils zombies peuvent attaquer par milliers pour rendre le ban d’IP inutile.

    Bonus track : voir les IPs sur une carte

    Et si on visualisait la localisation des IPs sur une carte ? Il faudrait pouvoir appeler une url vers une carte avec la latitude et la longitude en paramètre, sauf que dans notre cas, ces valeurs ne sont pas sur les mêmes lignes. Alors comment les capturer ? Plusieurs méthodes permettent d'y parvenir, mais nous allons principalement réutiliser les mêmes commandes pour explorer d'autres de leurs options.

    On va commencer par ne filtrer que les lignes indiquant la Latitude et la Longitude. Ces lignes étant consécutives, on va se servir de l'option de contexte -A de grep

    $ cat bad_ips_location | grep --no-group-separator -A1 Latitude
    

    Un extrait du résultat:

    [...]
    Latitude: 43.2854
    Longitude: 5.3761
    [...]
    

    Une fois ces lignes affichées, nous allons afficher sur la même ligne la latitude et la longitude. awk permet d'exécuter des commandes complexes sous forme de script shell. Nous allons stocker la latitude dans une variable quand on tombera sur cette ligne, et afficher le contenu de la variable et la ligne courante quand la ligne courante est la longitude.

    $ cat bad_ips_location | grep --no-group-separator -A1 Latitude | awk '{if (/Longitude/) print lat " " $0; lat=$0}'
    

    Les parties alors et sinon peuvent contenir plusieurs commandes, mais dans ce cas chaque partie doit être encadrée d'accolades, et les instructions à l'intérieur doivent être séparées par des ;.

    Le résultat:

    Latitude: 43.5995 Longitude: 1.4332
    Latitude: 37.5112 Longitude: 126.9741
    Latitude: 43.2854 Longitude: 5.3761
    Latitude: 37.3249 Longitude: -121.9153
    Latitude: 30.5801 Longitude: 114.2734
    

    On a donc désormais la latitude et la longitude sur une même ligne. Nous allons finir en utilisant une commande que nous avons déjà vu pour transformer les lignes et extraire les colonnes qui nous intéressent. Vous remarquerez que nous aurions pu utiliser presque exactement cette commande à la place de xargs pour faire notre première requête qui nous a donné les lieux.

    $ cat bad_ips_location | grep --no-group-separator -A1 Latitude | awk '{if (/Longitude/) print lat " " $0; lat=$0}' | awk '{print "https://www.openstreetmap.org/#map=14/" $2 "/" $4}'
    

    Un extrait du résultat:

    [...]
    https://www.openstreetmap.org/#map=14/37.5112/126.9741
    [...]
    

    Et voilà des liens qui nous afficherons l'emplacement des IPs sur une carte !

    Quickref

    Vous pouvez retrouver l'ensemble des commandes de ce tuto ici :

    Code snippet loading ... https://gitlab.com/snippets/1853884.js

    La plupart des exemples suivants consomment un fichier. Mais n'oubliez pas qu'il peut consommer la sortie standard d'une autre commande à travers |.