Wordpress : étude d'un site web victime de piratage

"ADurant le mois de février 2017, j'ai été sollicité pour analyser et "nettoyer" un site web piraté. Le site web en question fonctionnait sous Wordpress. Je partage mon expérience ici, en espérant que cela aide certaines personnes plus tard. J'ai choisi de ne pas mentionner le nom du site ou de son webmestre, si des commentaires venaient à divulguer ces informations, je me permettrai de les éditer ou de les refuser.

Mais avant de continuer, une mise en garde : en cas de piratage avéré de votre site, l'option la plus sûre reste de tout effacer, de restaurer des sauvegardes et de mettre à jour votre CMS ainsi que ses plugins ! Malheureusement, tout le monde ne fait pas de sauvegarde, et se retrouve parfois, selon l'hébergeur, avec un site web hors ligne le temps que le "nettoyage" soit fait.

Ah, et au passage : les adresses IP et noms de domaines ont été anonymisés. Si jamais il y a un oubli, faites-le moi savoir, et je corrigerai au plus vite !

Première étape : l'inventaire

J'ai commencé par faire un rapide inventaire de ce que je pouvais récupérer : le webmestre du site m'a aimablement fourni les accès à son hébergement et à son site, de sorte que je puisse effectuer sans difficulté toutes les actions dont j'aurais besoin. Je récupère donc les éléments suivants :

  • logs d'accès sur environ 16 jours ;
  • copie complète des fichiers sur l'hébergement ;
  • export de la base de données.

Première recherche dans les logs

Etudions donc ces logs, et voyons ce qui peut en ressortir. Je regarde d'abord le nombre de lignes de chaque fichier (la rotation semble se faire de manière quotidienne) :

13:19 nils@shell2:~/tmp/exemple/access_logs$ wc -l *.log
   29771 exemple.fr-03-02-2017.log
   11377 exemple.fr-04-02-2017.log
   12504 exemple.fr-05-02-2017.log
   12279 exemple.fr-06-02-2017.log
    9700 exemple.fr-07-02-2017.log
    6182 exemple.fr-08-02-2017.log
   11819 exemple.fr-09-02-2017.log
   11918 exemple.fr-10-02-2017.log
   19616 exemple.fr-11-02-2017.log
   15377 exemple.fr-12-02-2017.log
   11232 exemple.fr-13-02-2017.log
    8253 exemple.fr-14-02-2017.log
    6791 exemple.fr-15-02-2017.log
   13711 exemple.fr-16-02-2017.log
   23480 exemple.fr-17-02-2017.log
   16602 exemple.fr-18-02-2017.log
  220612 total

Trois jours deviennent intéressant, le premier avec 29771 requêtes, un autre 19616, et enfin un dernier à 23480. Je vais donc rechercher dans ces logs, des requêtes étranges, en particulier beaucoup de requêtes POST vers une ou plusieurs pages précises, qui ne semblent pas faire partie du site. Je suis content d'avoir retenu quelque chose de mon expérience précédente.

Pour aller voir si quelque chose ressort des requêtes POST, je réutilise les one-liners awk dont j'avais parlé ici et . Voyons donc ce qui effectue le plus de requêtes POST dans le premier log :

nils@shell2:~/tmp/exemple/access_logs$ grep POST exemple.fr-03-02-2017.log | grep -v "POST /wp-cron.php" | awk '{frequencies[$1]++;} END {for (ip in frequencies) printf "%d\\t%s" , frequencies[ip] , ip;}' | sort -rn | head -5
98      10.105.31.167
46      10.169.249.134
13      10.0.164.52
11      10.186.33.40
7       10.43.0.21

En effectuant des résolutions DNS inverses et des whois des adresses IP, je retrouve entre autres le FAI du webmestre, ainsi que le cluster de l'hébergeur. Mais je trouve aussi une adresse IP allemande, une autre tchétchène, et une russe. Surprenant pour un blog francophone, n'est-ce pas ? Bon, avant d'être accusé de racisme, allons voir ce que ces adresses IP ont fait comme requêtes. Extrait :

10.0.164.52 www.exemple.fr - [03/Feb/2017:14:56:17 +0100] "POST /wp-content/mybkl.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; MAARJS; rv:11.0) like Gecko"
10.0.164.52 www.exemple.fr - [03/Feb/2017:16:25:19 +0100] "POST /wp-content/mbsrd.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Mobile/13F69"
10.0.164.52 www.exemple.fr - [03/Feb/2017:17:41:16 +0100] "POST /wp-content/vipmpnjen.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36"
10.0.164.52 www.exemple.fr - [03/Feb/2017:18:06:57 +0100] "POST /wp-content/bvcomjjaf.php HTTP/1.1" 200 11 "-" "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; Tablet PC 2.0)"
10.0.164.52 www.exemple.fr - [03/Feb/2017:18:10:18 +0100] "POST /wp-content/mmgi.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 5.1; rv:50.0) Gecko/20100101 Firefox/50.0"
10.0.164.52 www.exemple.fr - [03/Feb/2017:18:24:07 +0100] "POST /wp-content/gruk.php HTTP/1.1" 404 56215 "-" "Mozilla/5.0 (iPad; CPU OS 9_2_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13D15 Safari/601.1"
10.0.164.52 www.exemple.fr - [03/Feb/2017:18:24:11 +0100] "POST /wp-content/fkjzhl.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Linux; Android 6.0.1; SM-N920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.124 Mobile Safari/537.36"
10.0.164.52 www.exemple.fr - [03/Feb/2017:18:32:00 +0100] "POST /wp-content/ssauz.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"
10.0.164.52 www.exemple.fr - [03/Feb/2017:20:19:02 +0100] "POST /wp-content/zpamh.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36"
10.0.164.52 www.exemple.fr - [03/Feb/2017:20:47:29 +0100] "POST /wp-content/bvcomjjaf.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"
10.0.164.52 www.exemple.fr - [03/Feb/2017:20:48:09 +0100] "POST /wp-content/ssauz.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (iPad; CPU OS 10_0_1 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) Version/10.0 Mobile/14A403 Safari/602.1"
10.0.164.52 www.exemple.fr - [03/Feb/2017:20:50:33 +0100] "POST /wp-content/vgmq.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"
10.0.164.52 www.exemple.fr - [03/Feb/2017:21:09:03 +0100] "POST /wp-content/pwemtiqeb.php HTTP/1.1" 200 11 "-" "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; Touch; ASU2JS; rv:11.0) like Gecko"

Je récupère alors une archive propre de Wordpress depuis le site officiel, par acquis de conscience, mais personnellement, fkjzhl.php ou pwemtiqeb.php ça me semble louche comme nom de fichier. Comme prévu, ces fichiers n'ont absolument rien d'officiel, et les plugins Wordpress ne s'installent pas dans /wp-content/.

A ce moment-là, ma conclusion est la suivante : l'intrus (en supposant qu'il soit seul) a déposé une multitude de fichiers un peu partout dans l'arborescence afin de rendre plus difficile un éventuel nettoyage. De plus, en accédant à plusieurs fichiers, depuis plusieurs adresses IP différentes, cela noie les requêtes dans la masse et rend là aussi, la détection plus difficile.

Même joueur joue encore

Passons au deuxième fichier, en utilisant la même méthode :

nils@shell2:~/tmp/exemple/access_logs$ grep POST exemple.fr-17-02-2017.log | grep -v "POST /wp-cron.php" | awk '{frequencies[$1]++;} END {for (ip in frequencies) printf "%d\\t%s" , frequencies[ip] , ip;}' | sort -rn | head -5
10143   10.9.129.250
240     10.28.47.221
234     10.135.219.59
229     10.213.224.115
199     10.123.209.172

Là aussi, la géolocalisation est assez variée : Allemagne, Hong-Kong, Pologne, Russie, Lituanie.Jetons alors un œil aux requêtes POST les plus visitées :

grep POST exemple.fr-17-02-2017.log | awk '{frequencies[$7]++;} END {for (field in frequencies) printf "%s\\t%d" , field , frequencies[field];}' | sort -nr -k 2,2 | grep -v "/wp-cron.php"
/hostdata4.php  11299
/wp-includes/js/tinymce/dir.php 1211
/xmlrpc.php     240
/wp-content/from.php    75
/wp-content/common.php  39
/wp-login.php   8
/wp-content/db_model.php        7
/wp-content/rss_feeder.class.php        4
/wp-includes/customize/db2.php  3
/wp-admin/network/plugin-editor.php     3
/wp-includes/js/tinymce/f53585.php      3
/wp-includes/Requests/Exception/HTTP/431.php    3
/wp-content/tongue_lib.php      2
/palaute.php    2
/addfavorites.php       2
/wp-content/uploads/2015/07/lib.php     2
/ranking.php    2
/confirmorder.php       2
/wp-content/press_lib.php       2
/wp-json/wp/v2/posts/5529       1
/wp-content/adodb.class.php?test_url=true       1
/e28441e709.php?test_url=true   1
/index.php/wp-json/wp/v2/posts/5529     1
/wp-content/index.php   1
/wp-content/powerful.inc.php    1
/wp-content/991e700dbd.html     1
/       1
/xmlrpc.php?for=jetpack&token=anonymized      1

Pas mal de fichiers me semblent bizarres, et on peut s'amuser à aller les recherche dans l'archive "saine" de Wordpress. Spoiler Alert : il n'y sont pas.

Prenons un peu de hauteur

Avant de passer à autre chose, j'ai décidé de regarder les requêtes POST les plus visitées sur la totalité des fichiers (en retirant un peu plus de requêtes "classiques") :

nils@shell2:~/tmp/exemple/access_logs$ for i in $(find . -type f -print | sort); do cat $i >> ../exemple.fr-global.log; done
nils@shell2:~/tmp/exemple/access_logs$ grep POST ../exemple.fr-global.log | grep -v "/wp-cron.php\\|/wp-login.php\\|/xmlrpc.php" | awk '{frequencies[$7]++;} END {for (field in frequencies) printf "%s\\t%d" , field , frequencies[field];}' | sort -nr -k 2,2 | head -50
/hostdata4.php  24322
/wp-includes/js/tinymce/dir.php 10057
/_index.php     1373
/wp-admin/admin-ajax.php        762
/wp-content/izodltnu.php        753
/wp-content/vgmq.php    705
/wp-content/zpamh.php   699
/wp-content/fkjzhl.php  654
/wp-content/ulimzaggf.php       645
/wp-content/jbrv.php    642
/wp-content/omfxde.php  629
/wp-content/ohvvdgk.php 625
/wp-content/pwemtiqeb.php       614
/wp-content/mmgi.php    613
/wp-content/nrzekbal.php        609
/wp-content/bvcomjjaf.php       605
/wp-content/yuflla.php  601
/wp-content/mybkl.php   598
/wp-content/mbsrd.php   594
/wp-content/vipmpnjen.php       578
/wp-content/ssauz.php   542
/wp-content/common.php  378
/wp-content/gruk.php    313
/wp-content/db_model.php        133
/wp-content/nwjtirmy.php        96
/wp-content/nrkg/wzvzrtnqh.php  95
/wp-content/iari.php    89
/wp-content/from.php    77
/wp-admin/admin.php?page=stats&noheader&chart=flot-stats-data   72
/wp-admin/admin-ajax.php?action=wp_ewwwio_async_optimize_media&nonce=c8aa5f3464 24
/wp-content/plugins/thank-me-later/lib/start37.php      23
/wp-admin/admin-ajax.php?action=wordfence_testAjax      23
/wp-content/uploads/2015/07/lib.php     19
/wp-includes/Requests/Exception/HTTP/431.php    19
/wp-includes/customize/db2.php  17
/wp-includes/js/tinymce/f53585.php      13
/wp-admin/meta/output.php       12
/post.php       11
/wp-admin/post.php      9
/wp-admin/network/plugin-editor.php     9
/wp-admin/admin-ajax.php?action=wp_ewwwio_async_optimize_media&nonce=50967874b9 8
/       7
/wp-comments-post.php?for=jetpack       6
/wp-content/egatl/wnxavysc.php  5
/3cdb6f452a.php?test_url=true   5
/wp-content/sad.func.php        4
/addfavorites.php       4
/palaute.php    4
/wp-content/rss_feeder.class.php        4
/wp-content/hook-filters.php    4

Afin d'éviter que ce blog tire en longueur juste pour des lignes de requête POST, je me suis limité au 50 premières lignes. Rien de plus que dans les autres recherche, on note comme avant que l'intrus a pris ses aises dans les répertoires /wp-content/ et /wp-includes/. L'intérêt de faire cette recherche est de faire remonter certaines requêtes qui seraient passées sous les radars si j'avais continué fichier par fichier.

Résultat des courses

J'ai donc repéré comme ça un certain nombre de fichiers qui n'ont rien à voir avec le contenu réel du blog, et que je vais pouvoir supprimer sans regret. Cela n'est hélas pas suffisant, car rien n'empêche un intrus d'insérer des fichiers qui ne sont presque pas accédés. Dans un prochain billet, j'espère donc comparer au niveau fichier l'export de ce blog avec une archive saine.

Vous avez aimé cet article ? Alors partagez-le sur les réseaux sociaux ! Si en plus vous avez des remarques, ou des propositions d'améliorations, n'hésitez pas : les commentaires sont là pour ça !

Crédit photo : Michael Gil - A Needle in a Hay Stack.