Wordpress : étude d'un site web victime de piratage
Durant 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 là. 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.