I have been Powned

Sans être un paranoïaque de la sécurité, je fais plutôt partie des gens qui entretiennent un hygiène informatique sérieuse. Et c'est bienvenu car c'est quand même mon métier.

En plus de 20 ans de pratique quotidienne je n'ai jamais découvert de compromission sur mes machines. Ça ne veut pas dire que je n'ai jamais fait de bourde : un mot de passe tapé « par réflexe » mais dans la mauvaise fenêtre, une petite fuite d'info dans des moteurs de recherches, etc.

Ceci étant, comme on le verra, ça n'empêche pas d'accumuler certaines mauvaises pratiques, en partie parce que la façon « moderne » de faire l'impose pour être efficace au quotidien.

Mais la compromission de sa station de travail, routinière pour les usagers de Windows, a un caractère exceptionnel pour un utilisateur de GNU/Linux de longue date comme moi. Et comme on va le voir, ça ressemble aussi à une mise à nu.

Note: Je partage mon expérience y compris dans le but d'améliorer ma réaction. N'hésitez pas à me contacter si je donne l'impression de passer à coté d'un gros truc.

Si ya un doute, ya pas de doute

Au détour d'un travail sur un projet de l'April je vois un subliminal mais inquiétant:

[1]+  Fini                    /home/francois/April/drupal2spip_lal/venv/bin/python3 /home/francois/.uds/_err.log > /dev/null 2>&1  (wd : ~)
(maintenant, wd : ~/April/spamotron)

J'ouvre ce fichier et très vite le doute s'installe :

import base64,lzma;exec(lzma.decompress(base64.b85decode("{Wp48S^xk9=GL@E0stWa761SMbT8$j;4$nE7F_@lh
[...]

Je remplace le exec par un print et j'obtiens un source python assez bien écrit (surtout après le passage de black), qui ressemble à un service de télécommande.

Un ps fauxww me montre que le script s'exécute encore dans un shell. Par curiosité je regarde la liste des fichiers ouverts. Celle-ci se limite quasiment au minimum vital pour un script python. Mais une connexion est ouverte :

$ lsof -p 3755
COMMAND  PID     USER   FD   TYPE  DEVICE SIZE/OFF    NODE NAME
[...]
python3 3755 francois    4u  IPv4 7991113      0t0     TCP renard:38026->199.247.5.158.vultr.com:3500 (ESTABLISHED)

Bon, ok. Il apparait via le ~/.bashrc. Je le nettoie et vérifie qu'il ne revient pas. J'en profite pour vérifier mes crontab et files at. Il ne revient pas.

Pas de panique

« sur sa couverture on peut lire en larges lettres amicales la mention : PAS DE PANIQUE »

— Douglas Adams, The Hitchhiker's Guide to the Galaxy

Ok. Pas de panique. Pas d'urgence, on respire un coup et un prend un café.

Première chose : rapidement cerner l'origine dans le temps de la chose. En fouillant les backups je déduis le 3 août comme première valeur.

Deuxième chose : en parcourant le code je comprends que c'est un botnet. Le code ne comprend rien d'autre qu'une télécommande, mais le maitre du botnet a pu avoir accès à toute ma machine.

Troisième chose : convaincu que le bot a eu potentiellement accès à tous mes fichiers perso, je commence sans tarder à demander la révocation de mes accès :

  • à mes collègues de travail ;
  • à mes collègues associatifs.

Je fais retirer mes accès à pas moins de quatre parcs de machines complètement disjoints, chacun contenant des dizaines de serveurs, de kvm et de conteneurs. Mes machines perso attendront, elles ne sont plus à ça près.

Comprendre

Le chemin de l'exécutable python désigne clairement un venv pour un projet que j'ai mené pour l'April en juillet/août. Je recherche sans succès les traces de l'origine du trojan.

J'examine le code à la recherche d'éléments spécifiques à chercher dans Google. Rien d'évident. Je recherche « pip .uds _err.log ». Je tombe sur une page dans un dialecte asiatique datée du 6 aout : https://guanjia.qq.com/news/n1/2583.html

Je la fait traduire par Google. Malgré le résultat très approximatif de la traduction, ça raconte assez bien la chose. Je remarque les mêmes serveurs impliqués, je retrouve certains des extraits de code montrés.

Leur explication ? Une opération de phishing impliquant PyPI au 31 juillet et ciblant le paquet request.

Request. Là mon cerveau fait tilt.

Mais putain quel con !

La semaine dernière j'ai finalisé drupal2spip_lal et testé sa mise en prod sur une Debian nue. J'ai été amené à corriger des dépendances, notamment requests. Comme un con pendant le dev j'avais fait une fôte (écrivant request) et c'était passé inaperçu. Je ne l'avais remarqué qu'au déploiement à cause d'une erreur de pip.

En fait ce qui s'était passé est qu'un développeur attentionné et conscient que parfois les gens se trompent a publié un paquet nommé request dont le principe est le suivant :

import sys,requests

#Some people might mistake this module for the popular python requests module
#So let's add an alias of that

sys.modules[__name__] =  sys.modules['requests']

C'est gentil, inoffensif mais peu productif : je fais partie de ceux qui préfèrent avoir des erreurs et les corriger très tôt plutôt que des bricolages d'adaptation.

Par contre ce qui n'est pas du tout inoffensif a été le script d'installation. Je n'ai pas la copie sous la main mais visiblement il s'est chargé a minima de modifier mon ~/.bashrc et d'installer la première charge dans ~/.uds/_err.log.

Tout ça pour une bête typo suivie d'un make update dans un contexte de pratiques discutables.

Résultat

Ma station de travail -- c'est-à-dire la machine avec laquelle je passe plus de temps qu'avec n'importe qui d'autre sur terre -- est compromise. J'ai hébergé un trojan à mon insu pendant 22 jours. C'est un cataclysme assez élevé sur l'échelle des drames.

Il n'y a pas à mégoter sur l'étendue de la situation : j'ai offert un shell python à un inconnu qui en avait envie. Il a eu accès à potentiellement tout sur ma machine. Il va falloir réinstaller et révoquer tout ce qui est possible.

Heureusement l'essentiel de mes secrets sont protégés :

  • fichiers importants chiffrés GPG ;
  • clés GPG et SSH toutes protégées par une passphrase ;
  • trousseau de mot de passe Firefox protégé par une passphrase.

Mais tout n'est pas rose :

  • des scans administratifs en clair ;
  • les mots de passe courriels en presque clairs : mon MUA sait gérer une passphrase dans les versions modernes, mais j'avais raté l'info donc je ne l'ai mis en œuvre que ce jour ;
  • j'utilise des agents SSH et GPG qui donnent un potentiel accès total sans passer par mes passphrases. Idem pour l'accès root à ma machine. Je suppose même possible d'exfiltrer la totale en examinant la carte mémoire des agents.

Bon, voilà pour la théorie. En pratique je ne suis même pas sûr qu'il y ait eu fuite de données. Les seuls codes d'exfiltration que j'ai lu sont conçus uniquement pour Windows et n'ont aucun effet sur ma machine (cf la charge « C2 » présentée par guanjia.qq.com). Il est possible et probable que la machine n'ai servi à rien d'autre que relais d'exécution à un botnet.

Le rôle de pip

J'ai parlé de mauvaises pratiques ou de pratiques discutables. J'en vois essentiellement une : le recours excessif à pip à la place du gestionnaire de paquet de l'OS. Pendant longtemps j'ai été réfractaire et râleur à ce sujet (et là je fais une dédicace à mon ami Christian qui lui y est toujours réfractaire...).

Mais depuis 6 ans que je fais du développement web et du « devops », pip est devenu incontournable. Même des gens plutôt précautionneux m'ont poussé à en faire. De plus, c'est quasiment impossible de faire du django (mon environnement de prédilection) sans pip, tout comme c'est quasiment impossible de faire du front sans npm (auquel je fais beaucoup beaucoup beaucoup moins confiance que à pypi/pip).

Notons que dans ce cas précis ça n'a aucun intérêt précis d'installer requests via pip : ce paquet est tellement populaire que disponible partout dans une version convenable. C'est par un pédantisme mal placé que je l'ai introduit (avec typo) dans mes requirements pour être exhaustif. En l'occurence i) je n'en avais pas besoin en local car requests était déjà présent sur ma machine et ii) ce projet ne sera jamais installé que par moi et sur une seule machine. Mais en bout de chaîne ce paquet s'est installé par la suite par un effet de bord attendu.

Le rôle de pypi

Les mainteneurs de pypi ont visiblement détecté le problème et supprimé le paquet. C'est pour ça que j'ai vu l'erreur au déploiement. Ceci étant :

  • Je n'ai trouvé nulle part de trace de ça. Il serait intéressant que les mainteneurs de pypi aient un registre qui liste ce genre de menace.
  • D'ailleurs ça m'étonne tellement que je suis sûr que ça existe mais je le n'ai pas trouvé. Google non plus.
  • Je n'ai pas été averti du problème alors qui a été connu des mainteneurs. On pourrait imaginer que les mainteneurs, à la place de supprimer le paquet, forgent un paquet « virtuel » en version ultime, qui lors d'un upgrade informe la personne de la situation avec de gros messages en gras rouge.

Je vais dorénavant utiliser plus assidûment des outils comme safety. Mais ce n'est pas évident que ça m'aurait protégé d'une charge à l'installation comme celle qu'il y a eu là.

D'ailleurs il n'y a pas eu de CVE et ce problème est ignoré de safety.

Conclusion

Comme disait Frédéric, « ce qui ne me tue pas me rend plus fort ». Mais comme répond echarp, « ou te laisse blessé ! ».

Bien sûr on n'est jamais sûr de rien car j'ignore la nature de la charge potentiellement téléchargée et exécutée pendant ces 22 jours.

Une fois passé la bourde, j'ai de bonnes raisons de penser qu'il n'y a pas à paniquer. Mes secrets importants sont protégés par un secret qui n'est que dans mon esprit. Dans la durée ça ne vaut rien mais dans l'instant ça protège.

Les backups c'est le bien. Ça m'a permis de remonter le temps et de comprendre l'origine et la nature des soucis. Car bien évidemment l'état de mon dépôt git ne conserve pas les détails historiques (je ré-écrit souvent l'historique). Idem pour le venv qui est régulièrement changé (ça sert à ça).

Notons que sans Google je ne trouve ni analyse, ni traduction de l'analyse. Je n'aime pas les GAFAMs et passe beaucoup de temps à lutter contre eux, mais force est de constater que Google rend des services encore difficiles à remplacer.

J'en vois une autre de mauvaise pratique : le recourt permanent aux agents. Je n'utilise pas le transfert d'agent car en cas de compromission c'est un cataclysme. Voir par exemple cette analyse pour un cas concret. En revanche j'utilise l'agent SSH et l'agent GPG fourni par Debian avec la configuration fournie par Gnome. Ils sont raisonnablement utiles (peut on vraiment faire de l'orchestration sans agent SSH ?) et raisonnablement limités, mais puisque je les utilise quotidiennement, ils ont donc des fenêtres d'ouverture pendant lesquelles je ne peux que supposer qu'ils exposent mes accès.

Mieux limiter mon recours à pip et aux agents sera un réflexion à mener. A minima un agent PGP ne me sert pas à grand-chose et devrait disparaître.

Notons également que j'ai tendance à ne pas restreindre ni tracer les accès réseaux vers la sortie, mais en l'occurrence ça aurait pu m'aider à y voir clair post mortem. Ceci étant, c'est une telle reponsabilité à maintenir correctement que je pense ne pas commencer. Idem pour les systèmes types IDS (OSSEC, Wazuh, Samhain) : les restrictions et les alertes me semblent exploitables sur des systèmes stables de production, mais sur une station de travail dédiée au développement mais une autre paire de manche à mettre en œuvre efficacement.

À défaut de mieux je vais m'employer à mieux ségréger mes développements (coucou docker).

Notons que le chiffrement de mon disque dur n'est dans ce cas d'aucun secours.