PROJET AUTOBLOG


Le blog de Seboss666

Site original : Le blog de Seboss666

⇐ retour index

DroidCam : c’est cool, mais ça pourrait être bien mieux

samedi 13 juin 2020 à 11:01

Avec le confinement, il se trouve que je n’ai pas pu voir ma sœur depuis le nouvel an. Idem pour ma mère, qui de son côté s’est flingué le dos. Pour organiser une visioconférence de « consolation », j’ai redécouvert à quel point les webcam integrées dans les laptop sont affreuses. Et si on tentait le smartphone en guise de remplaçant ?

Il n’y a qu’à voir les rageux fans d’Apple gueuler sur la qualité médiocre (c’est même pire que ça : c’est moins bon qu’avant) de la cam intégrée aux derniers macbook pro qui coutent deux smic pour comprendre le problème : même chez Apple qui fait rarement dans le compromis, un composant pourtant peu onéreux par rapport au reste de la machine se retrouve être juste honteux. Apple, qui vante les qualités de son application Facetime, qui utilise une source vidéo affreuse, ça fait tâche dans le tableau.

Mais il ne faut pas croire que le problème est spécifique à la marque à la pomme, sans vouloir aller jusqu’à des webcam 4k60fps intégrées (on a pas souvent les réseaux pour envoyer une telle image, sans parler de la décoder à l’autre bout de la « ligne »), la différence flagrante de la qualité des webcams comparées aux évolutions constantes année après année des capteurs de smartphone me rend perplexe, à croire qu’on ne peut pas capitaliser sur ces évolutions. Je suis de près l’apparition des modèles de laptop qui sortent avec une plateforme Ryzen 4000 (partie CPU de Ryzen 3000 et GPU Vega en 7nm), et le constat est le même à chaque fois : quelque soit la cible de l’appareil, la webcam est au maximum en 720p 30fps, ce qui est désormais limite en 2020. Je demande pas la lune, mais avoir au moins du fullHD avec en bonus, sur les hauts de gamme, du 60FPS, me parait loin d’être impossible ni trop cher en 2020, non ?

Arrive quand même l’horreur : la webcam du PC de ma mère. Un modèle 17″ Asus à base de Core i3 4000m d’il y a donc six ans, fourni avec Windows 8.1, dont la webcam est sobrement intitulée « USB Camera ». Le mieux que j’ai pu apprendre est qu’elle est fabriquée par Realtek, mais je n’ai rien trouvé d’autre comme référence ni pilote, car c’est celui de Microsoft qui est utilisé, et qui date de… 2006. Vous la sentez la merde ?

Voilà, et encore, c’est une image fixe, en 640×480 donc, la résolution que j’utilisais pour mon écran en 2002. Et je vous épargne la photo le soir avec 4 fois moins de lumière. Dans la pratique en vidéo si on arrive à obtenir 10 images par seconde c’est tout le bout du monde. Alors imaginez quand un Skype vous étire ça sur l’intégralité de votre écran 16 pouces (celui de ma sœur), ça donne quelque chose de tellement immonde que je renonce à vous l’afficher, pour préserver votre santé mentale et visuelle.

Le smartphone en remplacement ?

J’avais déjà eu cette idée dans le passé, sans avoir été au bout de la démarche en cherchant et en testant, mais avec l’idée de la comparaison naturelle entre la qualité d’image des smartphones même d’entrée de gamme (en gros, pour le prix d’une Logitech StreamCam, on a un smartphone 4G complet avec un écran de 6″…),  cette expérimentation a naturellement refait surface. Entre le débit d’image, la résolution, la gestion de la lumière bien plus optimale, les possibilités éventuelles sur le zoom et j’en passe, bref, c’est une aventure qui se tente.

J’ai donc eu l’occasion de faire le test sur deux ordiphones différents : mon Huawei P20 Lite dont il est prévu que je vous donne des nouvelles pour ses deux ans, et le Huawei Y5 version 2019. Mais tout smartphone Android un peu récent fera l’affaire, pour peu qu’il ait un peu de patate quand même, parce qu’on a vu la différence entre les deux déjà, on va le voir.

DroidCam, l’application de référence

Malgré son nom, DroidCam est techniquement disponible sur Android et iOS, et les applications PC Windows ET Linux sont également de la partie. Oui oui, aussi pour Linux, mais j’ai eu l’occasion de faire le test d’abord sur Windows, comme ça pas de jaloux.

Pour rappel, l’application fonctionne en deux parties : la partie installée sur mobile capture l’image du téléphone et crée un « serveur vidéo », qu’on peut ensuite soit consulter directement dans un navigateur (on a que l’image), soit contacter via l’appli client sur PC qui va créer une webcam virtuelle pour y transmettre le flux récupéré sur le téléphone. On peut aussi capturer le son du smartphone, mais pour avoir tenté de faire un comparatif, la qualité du micro ambiant du smartphone n’est pas forcément plus agréable que le micro intégré du laptop, et on est toujours à des années lumière des micro-casques donc… Deux modes de fonctionnement sont possibles, le premier simple via le wifi/réseau local, le deuxième via USB qui demande un peu plus de manipulations que je n’ai pas testé.

Premier constat : les devs sont bons, les applications aussi bien mobiles que PC sont simples et très fonctionnelles. Par contre, leur site web c’est de la merde en barre : aucun menu de navigation, ni de moteur de recherche, on doit sauter de page en page pour tenter de trouver nos informations (j’en ai surtout eu besoin pour la partie Linux, j’y reviendrai). On se croirait revenu sur un de mes premiers essais de site web en 1998 en cours d’informatique en seconde.

Deuxième constat : l’application mobile existe en deux versions, une « gratuite » et une payante. Je mets gratuite entre guillemets parce qu’on sait que désormais, l’affichage de publicité n’a rien d’innocent et désintéressé dans l’univers Google et du Web en général. Pire, et là c’est vraiment gonflé de leur part, la résolution du flux vidéo du smartphone est limitée, dans la version gratuite, à 640×480. On garde les autres avantages comme la gestion de la lumière et la fluidité relative à la puissance du smartphone, mais merde, on est plus en 2002. La version payante propose en plus une tétrachiée d’options supplémentaires qui permettent de contrôler plusieurs éléments du smartphone directement depuis l’appli, comme le zoom, l’autofocus, la balance des blancs, l’allumage du flash, etc, mais ils auraient au moins pu amener le 720p par défaut quoi !

Usage sous Windows, avec le laptop de ma mère

Comme je l’ai évoqué, l’application est simple, efficace, s’installe sans pourrir votre PC. On aimerait voir des freeware comme ça plus respectueux de nos machines en 2020 ! L’interface est simple, on lance, on saisit l’adresse IP du téléphone (et le port si on s’est amusé à le changer), et une seconde après on a l’image qui doit s’afficher, sauf si on a laissé le téléphone sur la table évidemment 😀 Mais il suffit dès lors de cadrer l’image qu’on souhaite partager. En fonction des téléphones et de l’usage final, on peut poser le « capteur » contre l’écran , ça masquera une petite partie de celui-ci mais si on n’a rien à manipuler en paralèlle, ça peut le faire.

Ensuite sous Skype, on sélectionne la webcam « DroidCam » et on constate le résultat :

On a la même résolution, mais on est à des années lumière en termes de qualité et de fluidité. Ma sœur me l’a fait remarquer quand on s’est amusé à faire un comparatif entre les deux 😀 En clair, solution validée, même pour ma maman quand elle se retrouvera autonome pour ça.

Usage sous Linux, laptop pro et perso (c’est pareil)

Pareil parce que les deux sont sous Manjaro, la seule différence c’est que sur le PC du boulot je suis resté en noyau 4.19, alors que sur mon perso c’est du noyau 5.4. Oui, que du LTS. Je vous expliquerai probablement pourquoi sur le laptop du boulot j’ai du revenir au 4.19 en 2020, c’est marrant aussi. Enfin dans les deux cas, j’ai pu faire fonctionner le système sans trop de problèmes, mais évidemment, ça n’a pas été aussi fluide que sous Windows. L’installation aucun souci, via AUR c’est d’une simplicité absolue. Mais la suite…

D’abord, le module noyau v4l2loopback-dc qui permet de créer la webcam virtuelle n’est pas chargé par défaut, il faut manuellement « modprober » ce qui se fait en mode administrateur. On a vu plus souple, le point positif c’est que le module est fourni via DKMS il est donc mis à jour proprement en cas d’update noyau. Et c’est là en fait que j’ai découvert la limitation de la résolution, le module crée par défaut une webcam en 640×480, même si le flux est en résolution supérieure. Il faut aller modifier, toujours en mode administrateur, le fichier de configuration du module pour changer les paramètres (pensez à créer le fichier chez vous s’il n’existe pas déjà) :

#cat /etc/modprobe.d/v4l2loopback-dc.conf
options v4l2loopback_dc width=960 height=720

Ensuite décharger le module (modprobe -r), et le recharger, parce qu’évidemment les paramètres ne sont pas pris à chaud. Et attention, si on met une sortie qui n’est pas dans le même format que la source, l’image est déformée. Dans mon cas j’ai quand même poussé jusqu’à 720p, ça étire un peu le flux d’origine mais comme je garde le ratio, c’est moins dégueulasse. On constate quand même une légère augmentation du bruit visible en faible luminosité.

Ensuite, la gestion du son est encore à part et vu les manipulations de la documentation j’ai abandonné direct. On parle de module Alsa, qui reste semble-t-il toujours aux commandes alors qu’on manipule pulseaudio au quotidien, donc j’ai du mal à comprendre comment ça fonctionne. 2020, et la gestion du son sous Linux a encore 20 ans de retard sur Windows, et on se demande pourquoi Linux ne s’impose toujours pas…

Enfin, une fois la partie « bas niveau » en place, on peut lancer l’application qui a exactement la même tête que sous Windows, donc adresse IP à saisir, et ensuite, on sélectionne la caméra dans Skype/Teams. La bonne surprise cependant, c’est que là où dans l’application Windows on me dit que les contrôles sont réservés à la version DroidCamX (donc payante), sous Linux je peux contrôler le zoom, l’autofocus et le flash  😀 Sur le laptop boulot on a donc une webcam avec la même résolution, mais l’ouverture est plus large, l’image plus fine et lumineuse et la fluidité un peu meilleure. Sur le laptop perso, la webcam fait déjà du 720p avec une gestion relativement propre de la lumière, la fluidité n’est pas parfaite mais ça peut suffire pour des petites visio-conférences. Y’a quand même du bruit donc faut pas être exigeant sur la qualité de l’image.

L’alternative qui n’a pas fonctionné pour moi

J’ai cherché à voir si une option permettant une meilleure qualité sans devoir balancer sa carte bancaire à Google existait. Il semblerait, elle s’appelle Iriun Webcam, dont le fonctionnement est siimilaire à savoir appli mobile+appli desktop. Il permet d’exploiter la 4K si le téléphone le permet, mais de mon côté, j’ai rencontré beaucoup de problèmes. Déjà, l’installation avec le paquet AUR déconne parce que le md5 de l’archive téléchargée par le script d’installation n’est pas le même. Les développeurs n’ont pas spécialement envie de penser qu’on est sur PC et qu’on a besoin de contrôles de versions, donc j’ai tenté l’installation à la main (on clone le dépot AUR, on modifie le PKGBUILD, et makepkg -si dans la foulée). Mais une fois v4l2loobpack manuellement chargé au niveau du noyau (module officiel pour faire une webcam virtuelle) et le logiciel lancé, ben écran noir sur la capture, pourtant le smartphone indique bien que la connexion est établie. Aussi, le flux semble coupé très très régulièrement, du coup, au bout d’un quart d’heure de manipulations sans résultat, j’ai lâché l’affaire.

Je ne l’ai pas testé non plus sous Windows, si vous avez expérimenté avec hésitez pas à partager, à ce moment de l’histoire j’ai décidé que j’avais autre chose à faire que de débugger des applis qui ne me donnent déjà pas beaucoup d’infos sur leur fonctionnement.

Ça manque cruellement d’open-source

Car oui, tous les éléments testés ici ne sont pas open-source. Et c’est un vrai problème des deux côtés, aussi bien au niveau du smartphone où l’on dépend du store fermé de Google ou d’Apple, que du côté du PC où l’on ne peut pas forcément s’assurer que l’application n’en fait pas un peu trop, pour un logiciel qui a accès à la webcam. Sur Linux, comme ça dépend d’un module noyau (modifié dans le cas de DroidCam), c’est un peu tendu pour s’assurer du suivi du bon support lors des montées de version (même si globalement l’infrastructure v4l2 est assez stable).

Il y a bien eu une application appelée SmartCam, dont les fichiers sont toujours disponibles, mais elle n’a pas été mise à jour depuis 2013 voire plus vieux encore pour certains, et autant dire que ça commence à faire vraiment trop dans le monde de l’informatique.

En attendant, et vu la qualité du résultat même avec une résolution décevante avec la version gratuite, vous avez une solution de rechange pour les webcam intégrées aux laptop, ou pour votre PC de bureau. Y’a juste un truc que j’ai pas vraiment abordé : le fait qu’il faut poser le téléphone dans une position pas trop désavantageuse pour votre visage 😀

PS : c’est évident pour certains, mais je rappelle que si vous êtes moche comme moi à la base, une bonne webcam ou un bon smartphone n’y changera rien. On verra juste mieux à quel point vous êtes moche 😀

Un peu d’amour pour le blog (et la VM en général)

mardi 2 juin 2020 à 18:30

Vous connaissez l’adage qui dit que les cordonniers sont les plus mal chaussés ? Dans mon cas c’était incroyablement vrai, ma procrastination sur la maintenance de tout ça étant incroyablement puissante. J’ai quand même fini par me mettre à la tâche, et j’ai décidé de détailler pour que l’on comprenne bien le problème afin d’éviter de le reproduire.

Le champ de bataille

Nous sommes donc sur une VM bloquée en Debian 8, avec les installations notables suivantes :

Et surtout, d’anciennes traces d’installation d’ISPConfig, qu’on se traîne encore comme un boulet parfois sans savoir pourquoi on a des soucis. On le voit, je ne suis pas vraiment mon propre conseil d’essayer de limiter les sources externes de paquets, mais avec le temps et l’âge d’une Debian, proposer des technos récentes (PHP7+ et HTTP2 entre autres), c’est compliqué. Pareil pour les montées de versions qui sont compliquées par les opérations passées et l’activité des scripts.

Installation de la VM, allégorie

De cette installation, j’en ai tiré notamment un conflit à l’installation du module Redis sur PHP 7.3 à cause du nom du package qui bute avec les fichiers de celui de php 7.0 de Dotdeb. J’avais pas fait attention quand j’avais installé PHP 7.3 en ce mois de Janvier, et j’avais du coup perdu deux mois de visites sur Matomo (c’est lui qui demandait PHP 7.3 pour ses mises à jour), parce que le plugin Queued Tracking qui stocke les visites dans Redis avant enregistrement définitif renvoyait une erreur 500 parce que les commandes Redis n’existent pas. Et comme l’interface fonctionnait au moment où j’ai basculé, j’ai pas percuté. Oui, je suis un boulet.

Travail préparatoire

L’intervention a donc d’abord été l’occasion d’un gros gros inventaire et surtout d’un nettoyage de scripts inutiles, de vhosts inexistants et non fonctionnels, de pools php qui vont avec ou orphelins (vhosts nettoyés mais php toujours là), bref, les confs PHP et Nginx sont beaucoup plus claires sur ce qui est réellement en activité sur le serveur.

J’en ai également profité pour procéder à un déménagement de certains vhosts annexes sur PHP 7.3, comme mon FreshRSS qui me remercie grandement. Au passage, au fur et à mesure je rend agnostique le chemin du socket, ça permet de changer facilement de version sans avoir à bidouiller plusieurs fichiers. Là, je déplace le fichier de conf du pool d’une version de PHP à l’autre, je redémarre celui qui ne sert plus, je redémarre celui qui servira, je teste. Le retour arrière est à peu près aussi rapide.

Ça a aussi permis de virer quelques packages, et j’ai été surpris, parce qu’en fait, PHP 5.6 n’était plus utilisé ni démarré, il a donc fini à la poubelle. Autre découverte, apache2 est également toujours installé !? Il a rejoint PHP 5.6. J’ai potentiellement viré un ou deux autres trucs découverts par hasard, mais pas forcément en lien direct avec le bordel du jour.

On attaque la butte

Ce coup-ci, on rendre dans le dur. Je commence par réactiver les dépôts Sury pour tenter de mettre à jour les paquets PHP 7.0 de Dotdeb vers PHP 7.0 Sury. L’opération a pris du temps, mais s’est bien terminée, modulo quelques paquets restants. En effet, ils n’ont pas le même nom chez Sury. Je les supprime donc (paquets liés au module redis) et je réinstalle la version Sury. Et ça s’est super bien passé !

Il restait quelques éléments à supprimer pour être définitivement tranquille. Le méta-paquet « php » va installer la dernière révision de PHP en date, soit PHP 7.4. Et désormais, je n’ai plus de conflit à l’installation de paquets PHP 7.x. Au passage, il y a des différences entre PHP 7.0 et php 7.3, mais dans certains cas ils sont juste inutiles donc pas la peine de les installer. D’autres n’existent plus comme mcrypt, on va voir juste après la conséquence.

Ce que le nettoyage a permis

J’ai pu réinstaller le QueuedTracking de Matomo, qui permet un gain de temps substantiel dans le chargement de la page puisque la visite est enregistrée dans Redis avant d’être traitée et enregistrée dans la base de données en arrière-plan par Matomo. Moins d’attente dans le chargement d’une page de votre côté, et comme de toute façon je suis pas à la seconde pour la traçabilité, ça peut prendre le temps que ça veut pour enregistrer (ou pas, en fonction des détails envoyés comme le DNT).

J’ai aussi profité pour virer le dépot Dotdeb pour m’assurer que je ne risquais pas d’installer quelque chose qui provient de chez eux. Il ne reste que celui dédié à Nginx qui n’a de toute façon pas été mis à jour depuis un moment (oui je sais…), mais comme il ne contient pas de paquets PHP c’est pas grave. J’ai d’ailleurs fait le ménage sur deux/trois autres dépôts qui ne servent plus et qui pour certains étaient déjà désactivés. Moins de dépôts activés, veut dire mises à jour et installations qui prennent moins de temps.

Une seule source de PHP, plus de conflit (je crois que je l’ai déjà dit), veut dire aussi passage à de futures versions facilité, ou presque. En effet, en fonction de l’âge des scripts/applications installées, ça peut se vautrer dans les grandes largeurs avec une version qui supprime une ou plusieurs fonctions qui servent à ceux-ci. et dans mon cas, mon WordPress va encore avoir besoin d’un peu d’amour pour tenter une grosse montée de version avant de pouvoir bénéficier d’une montée de version de PHP (et quand je vois le gain sur FreshRSS, ça fait plus qu’envie). J’ai quand même tenté l’upgrade vers PHP 7.2, pour l’instant je ne constate pas d’erreur, malgré l’âge de certains éléments (plugins pas maintenus entre autres). Il faudra quand même que je termine cette satanée migration vers WordPress 5 pour m’assurer de ne plus avoir de problème et pouvoir continuer les migrations.

moi à la fin de l’après-midi

J’étais chaud du coup j’ai procédé un peu pareil sur le VPS qui porte l’installation de LibreNMS. ce VPS avait déjà subi une sale montée de version de Debian à l’arrache, OVH avait eu la très mauvaise idée de juste laisser « stable » dans la configuration des dépots, mais comme je ne fais pas de dist-upgrade en auto, ça s’était fini en simili Debian 9 avec des dépendances et le noyau de Debian 8. Là encore, j’avais le PHP 5.6 de Debian 8 encore installé sur Debian 9, alors qu’il ne servait plus à rien.

Le nécessaire déménagement

Je me suis concentré aujourd’hui sur les problèmes liés à PHP et au blog (tiens au passage, le récent problème lié à Broken Link Checker a été compliqué à régler, mais c’est fait). Mais on l’a vu, il reste des horreurs comme le nginx custom, et la VM, avec son âge, a plusieurs autres tares, dont ces fameux relents d’ISPConfig. Faire la montée de version en place ne m’intéresse pas et causerait certainement plus de problème qu’elle n’en résoudrait, il est temps de mettre l’installation à la retraite pour repartir sur du neuf, un peu comme un gros ménage de printemps ne remplace pas un déménagement dans l’efficacité à virer les choses accumulées depuis des années.

Et ça fait partie des choses que je préparais pour le futur proche, mais c’était dans l’optique d’un certain budget allouable qui ne sera pas possible cette année (merci SARS-Cov-2). J’ai donc des plans à revoir pour quand même avancer sur le sujet sans me faire trop mal au porte-monnaie, en attendant, je suis content du résultat des travaux.

PS : j’avais prévenu sur Twitter que « ça va couper chérie » le temps que je bosse dessus. On m’a demandé pourquoi j’ai pas bossé sur une seconde VM à côté et basculé une fois terminé. Déjà parce qu’il y a pas mal de choses sur cette VM, et pas seulement le blog : autres sites, serveurs de discussion vocales, etc. Ensuite, la configuration réseau n’a jamais été pensée pour permettre ce genre de bascule simple, la machine est directement exposée avec son IP publique, sans pare-feu ni reverse-proxy devant, pas de Docker, pas de pare-feu autre que celui du noyal. Donc j’ai bossé « en live », ce qui n’était pas plus mal pour permettre de découvrir l’étendue des dégâts potentiels. Mais certains aspects pourraient changer dans la future infra.

Analyse poussée d’un code malveillant PHP, deuxième round

vendredi 8 mai 2020 à 18:40

Pour ceux que la sécurité informatique intéresse un peu, j’avais déjà fait une sorte de pas à pas d’analyse d’un code PHP masqué pour tenter d’en comprendre le fonctionnement. J’ai pu mettre la main récemment sur un nouveau morceau de choix, car mon analyse m’a poussé beaucoup plus loin. Let’s go ?

Le code qu’on avait analysé en substance n’était là que pour permettre de déposer d’autres fichiers. Basique, mais toujours pratique à garder sous la main. Ici, je suis tombé sur un morceau beaucoup plus intéressant, qui m’a pris deux bonnes heures pour noter tout ce que je vous partage aujourd’hui. Je vais volontairement masquer certains éléments comme l’URL finale, histoire de pas trop diriger les gens vers des éléments qui peuvent potentiellement endommager leur appareil ou en tout cas ses données.

Petit contexte rapide aussi, le ministère de la Culture remonte un problème avec un lien vers un site (celui de mon client) qui redirigeait vers des sites louches. Alors que normalement je ne touche plus que très rarement à ce genre de situation dans mon quotidien, le client avec qui je bosse en parallèle sur une refonte de sa plateforme m’a contacté directement. Comme je suis quelqu’un de faible qui aime ce genre de situation, j’interviens évidemment 😀

L’enquête de surface

J’attaque par une fouille rapide de l’arborescence, en suivant la logique du code : un bout de JavaScript injecté en bas de page via le fichier index.php, qui remonte à plus de deux mois, et j’identifie un code PHP injecté dans une dépendance qui remonte à plus de deux ans (un oubli de nettoyage d’une précédente intervention, erf).

J’ai pris le réflexe de faire une copie de toutes mes trouvailles pour analyse à froid avant de faire le ménage rapidement, et rassurer le ministère de la Culture que c’était bon, qu’ils pouvaient remettre le lien en ligne. C’est plusieurs jours plus tard que j’ai attaqué la véritable enquête.

Le JavaScript en substance procédait à du Black Hat SEO, il était embarqué dans une div parlant de Louis Vuitton en anglais (sur un site français), div qui était accompagnée par le code JavaScript obfusqué :

On remplace le deuxième eval() par un document.write() et le tour est joué :

La div était donc masquée, mais sa présence dans le code source suffit à faire son effet sur les robots d’indexation. Et c’est donc le PHP qui m’a donné du fil à retordre, on va voir pourquoi.

« -Scalpel ? -Scalpel. »

Voyons donc le petit saligaud :

C’est pas beau hein ? Bon le fait est que la première étape va être beaucoup plus facile à analyser parce qu’il n’y aura pas cinquante étapes de traduction des fonctions, ici j’épure et je remplace l’eval par un echo. Voici le résultat :

Pas très beau à lire, mais les barbouillis ne concernent que les noms des variables, ça va être cool. On a donc plusieurs éléments ici qu’on va détailler bloc par bloc.

La première ligne est une arme nucléaire ou presque : elle traite le contenu d’une requête POST, si elle trouve une clé ‘pf’ dont le contenu mouliné avec md5 correspond à ce qui est demandé, ça exécute le code PHP encodé en base64 qui doit se trouver dans une autre clé ‘cookies_p’. Vous avez là un petit moteur d’exécution de code distant, ce qu’on appelle dans le métier une RCE. En pratique ça fait partie des pires failles de sécurité qu’on remonte dans les programmes, ici, l’attaquant s’est donc laissé une porte grande ouverte. Dans la pratique il est pour l’instant limité à l’utilisateur PHP, mais rien ne l’empêche de tenter des escalades de privilèges, c’est même possible de procéder à des extractions de données sans même plus de difficultés. Vous l’aurez compris, il est désormais compliqué de pouvoir continuer de faire confiance au serveur. Mais c’est pas tout.

À partir de la ligne 9, on a un code que je n’avais encore jamais eu l’occasion de voir et qui m’intrigue beaucoup. Il s’avère que le code tente d’identifier si la requête a été faite par un humain, un robot d’indexation de moteur de recherche, ou un humain mais en local (ce qui peut être le site lui-même). En gros, si c’est un humain, on va parser tout le contenu de la variable $_SERVER pour en faire un flux qu’on va envoyer via une requête POST sur une URL distante. Je vous laisse avec la documentation de PHP pour voir tout ce que peut contenir cette variable $_SERVER, c’est plutôt détaillé et complet.

Cette URL est toujours en ligne, le whois du domaine ne dit rien mais l’adresse IP est hébergée en Ukraine. Le script appelé sans paramètre répond vide mais un code 200, il semble donc toujours d’actualité. J’ai donc décidé de tenter de rejouer le code pour voir son comportement.

J’ai pris quelques minutes pour tenter de crafter moi-même ce contenu de $_SERVER. Pour ce faire, j’ai écrit un micro code PHP :

<?php

var_dump($_SERVER);

J’ai ensuite démarré un micro serveur Apache avec module PHP embarqué via une image docker random trouvée sur le hub, flemmardise totale mais ça suffit pour l’exercice :

docker run --rm -p 8000:80 --name "apache-php" -v "${PWD}/index.php":/var/www/html/index.php newdeveloper/apache-php

Et j’ouvre ce fichier index.php via mon navigateur sur l’adresse 127.0.0.1 et sur le port 8000. Voilà le résultat :

N’étant pas un expert en regex de porc et feignant comme pas deux pour faire un parsing, j’ai fait le gros sale à base de copier/coller dans un fichier pour ensuite modifier ce contenu pour en faire une vraie affectation de tableau en PHP pour la variable $_SERVER. J’ai ensuite fait une copie du code en question, pour voir ce que le file_get_contents() récupérait, mais une fois encore on fait un echo à la place du eval.

Oh ben tiens dis donc, le code PHP récupéré et exécuté injecte un header Location avec une URL bizarre, ce qui a pour effet de rediriger le visiteur vers cette URL. Je m’attarde pas sur le base64, il fait la même chose mais via du code HTML si pour une raison ou pour une autre le code PHP a déjà envoyé ses headers avant l’exécution de cette portion (ce qui se traduit par une erreur fatale en PHP…).

A ce moment-là, je vous conseille de vous préparer avant de faire les cons. Soit vous tentez l’URL via curl, mais il ne vous retournera que le html brut. Soit vous tentez dans un navigateur, et là vous avez intérêt à être blindé, à jour, voir à désactiver dans un premier temps l’exécution de Javascript et l’analyser/le charger manuellement si vous le sentez.

Pourquoi ? Oh, trois fois rien. Après deux/trois redirections, le navigateur s’est bloqué sur une page qui affiche un bête « Loading… », et n’a rien fait de plus chez moi. Mais en analysant la réponse, c’est assez impressionnant :

Oui, ce n’est que du code JavaScript. Je suis pas expert, et c’est à mon avis fait exprès qu’il soit aussi tordu, mais malgré tout, en le parcourant en diagonale, on finit par comprendre qu’on vient de charger un outil soit de minage de cryptomonnaie, soit plus plausible un CryptoLocker, ce qui est déjà beaucoup plus pénible si ça passe les protections du navigateur. Et ce que vous voyez à droite est la taille du code, vu de « très haut ». C’est donc très long, et très dense. J’ai pas insisté sur la lecture, j’ai envie de garder un semblant de santé mentale.

Verdict : on évite le code zombie

Le reste de l’analyse n’a pas permis d’identifier de truc caché, y compris des fichiers « images » qui sont en fait des scripts PHP. En posant la question j’apprends que le code concerné a été réalisé il y a plus de dix ans par des stagiaires, et qu’il n’a été ni corrigé, ni mis à jour depuis cette date. J’avais déjà eu un indice avec le fait que le code était « full français » : nom des variables et des fonctions, commentaires, noms des fichiers et des dossiers, tout y était. L’autre indice était l’organisation même du code qui ressemblait bizarrement à mes tous premiers projets persos, ceux avant le peu glorieux Collect pour ceux qui voudraient rigoler de mon niveau en développement PHP.

Il tourne de plus sur une machine dont la gestion des mises à jour et des montées de version d’environnements d’exécutions est inexistante : OS en fin de vie, PHP non supporté, la totale. N’importe qui vous le dira : la sécurité informatique, c’est une chaîne, aussi forte que le maillon le plus faible. Mettez vos serveurs et vos logiciels à jour, mais laissez un code zombie comme celui-là mal conçu et c’est grand ouvert pour se faire infecter et faire joujou avec toutes sortes de saloperies, et tous les autres efforts fournis n’auront servi à rien. Je sais, c’est plus facile à dire qu’à faire, et j’en suis le premier exemple, actuellement il ne faut pas trop regarder sous le capot du blog, l’installation tient plus du bidonville que du Palace. le chantier est au programme, mais sans date précise pour le moment.

Il y a tout de même quelques techniques pour limiter les dégâts, la plupart déjà partagées dans ma série sur la sécurisation d’un serveur, mais au détour d’une question Twitter on m’a rappelé qu’ils dataient un peu, il n’est pas impossible dans un futur proche que je rafraîchisse un peu le contenu. Je ne sais pas encore quelle forme ça va prendre, on va déjà commencer par se relire hein, on sait comment fonctionne ma mémoire 😀

Programmation défensive en bash

lundi 27 avril 2020 à 18:30

Nouvelle traduction aujourd’hui, d’un article qui risque sans prévenir de disparaître. La programmation défensive consiste à structurer son code pour limiter au strict minimum les surfaces d’attaques. Le JDN a un article qui résume bien des concepts, mais ici, on va s’attarder sur les scripts Bash. En effet, comme avec PHP on peut vite faire de la merde, et il est préférable de suivre certaines pratiques pour que leur qualité ne soit pas trop catastrophique quand il seront réutilisés ailleurs. Je ne les suis pas toutes, mais j’ai trouvé utile de les partager avec vous.

Variables globales immutables

readonly PROGNAME=$(basename $0)
readonly PROGDIR=$(readlink -m $(dirname $0))
readonly ARGS="$@"

Tout est local

Toutes les variables devraient être locales.

change_owner_of_file() {
  local filename=$1
  local user=$2
  local group=$3

  chown $user:$group $filename
}


change_owner_of_files() {
  local user=$1; shift
  local group=$1; shift
  local files=$@
  local i

  for i in $files
  do
    chown $user:$group $i
  done
}

kfir@goofy ~ $ local a
bash: local: can only be used in a function

main()

main() {
  local files="/tmp/a /tmp/b"
  local i

  for i in $files
    do
      change_owner_of_file kfir users $i
    done
}
main

Tout est une fonction

main() {
local files=$(ls /tmp | grep pid | grep -v daemon)
}

temporary_files() {
  local dir=$1

  ls $dir \
    | grep pid \
    | grep -v daemon
}

main() {
  local files=$(temporary_files /tmp)
}

test_temporary_files() {
  local dir=/tmp

  touch $dir/a-pid1232.tmp
  touch $dir/a-pid1232-daemon.tmp

  returns "$dir/a-pid1232.tmp" temporary_files $dir

  touch $dir/b-pid1534.tmp

  returns "$dir/a-pid1232.tmp $dir/b-pid1534.tmp" temporary_files $dir
}

Comme vous le voyez, ce test ne concerne pas main().

Des fonctions de debug

bash -x my_prog.sh

temporary_files() {
  local dir=$1

  set -x
  ls $dir \
    | grep pid \
    | grep -v daemon
  set +x
}

temporary_files() {
  echo $FUNCNAME $@
  local dir=$1

  ls $dir \
    | grep pid \
    | grep -v daemon
}

Donc, en appelant la fonction :

temporary_files /tmp

affichera sur la sortie standard :

temporary_files /tmp

Clarté du code

Qu’est-ce que fait ce code ?

main() {
  local dir=/tmp

  [[ -z $dir ]] \
    && do_something...

  [[ -n $dir ]] \
    && do_something...

  [[ -f $dir ]] \
    && do_something...

  [[ -d $dir ]] \
    && do_something...
}
main

Laissez votre code parler :

is_empty() {
  local var=$1

  [[ -z $var ]]
}

is_not_empty() {
  local var=$1

  [[ -n $var ]]
}

is_file() {
  local file=$1

  [[ -f $file ]]
}

is_dir() {
  local dir=$1

  [[ -d $dir ]]
}

main() {
  local dir=/tmp

  is_empty $dir \
    && do_something...

  is_not_empty $dir \
    && do_something...

  is_file $dir \
    && do_something...

  is_dir $dir \
    && do_something...
}
main

Chaque ligne fait une seule chose

temporary_files() {
  local dir=$1

  ls $dir | grep pid | grep -v daemon
}

Peut être écrit plus clairement :

temporary_files() {
  local dir=$1

  ls $dir \
    | grep pid \
    | grep -v daemon
}

temporary_files() {
  local dir=$1

  ls $dir | \
    grep pid | \
    grep -v daemon
}

Bon exemple où l’on voit clairement la connexion entre les les lignes et les symboles :

print_dir_if_not_empty() {
  local dir=$1

  is_empty $dir \
    && echo "dir is empty" \
    || echo "dir=$dir"
}

Afficher l’usage

Ne faites pas ça :

echo "this prog does:..."
echo "flags:"
echo "-h print help"

Ça devrait être une fonction :

usage() {
  echo "this prog does:..."
  echo "flags:"
  echo "-h print help"
}

echo est répété à chaque ligne. A la place on a ‘Here Document’ :

usage() {
	cat <<- EOF
	usage: $PROGNAME options

	Program deletes files from filesystems to release space.
	It gets config file that define fileystem paths to work on, and whitelist rules to
	keep certain files.

	OPTIONS:
		-c --config configuration file containing the rules. use --help-config to see the syntax.
		-n --pretend do not really delete, just how what you are going to do.
		-t --test run unit test to check the program
		-v --verbose Verbose. You can specify more then one -v to have more verbose
		-x --debug debug
		-h --help show this help
		   --help-config configuration help

	Examples:
		Run all tests:
		$PROGNAME --test all

		Run specific test:
		$PROGNAME --test test_string.sh

		Run:
		$PROGNAME --config /path/to/config/$PROGNAME.conf

		Just show what you are going to do:
		$PROGNAME -vn -c /path/to/config/$PROGNAME.conf
	EOF
}

Attention à bien utiliser des tabulations ‘\t’ pour le début de chaque ligne. Dans vim vous pouvez utiliser cette astuce si votre tabulation consiste en 4 espaces :

:s/^    /\t/

Arguments de ligne de commande

Voici un exemple de complément à la fonction usage d’au-dessus. Ce code est tiré de l’article bash shell script to use getopts with gnu style long positional parameters sur le blog de Kirk :

cmdline() {
  # got this idea from here:
  # http://kirk.webfinish.com/2009/10/bash-shell-script-to-use-getopts-with-gnu-style-long-positional-parameters/
  local arg=
  for arg
  do
    local delim=""
    case "$arg" in
      #translate --gnu-long-options to -g (short options)
      --config) args="${args}-c ";;
      --pretend) args="${args}-n ";;
      --test) args="${args}-t ";;
      --help-config) usage_config && exit 0;;
      --help) args="${args}-h ";;
      --verbose) args="${args}-v ";;
      --debug) args="${args}-x ";;
      #pass through anything else
      *) [[ "${arg:0:1}" == "-" ]] || delim="\""
         args="${args}${delim}${arg}${delim} ";;
    esac
  done

  #Reset the positional parameters to the short options
  eval set -- $args

  while getopts "nvhxt:c:" OPTION
  do
    case $OPTION in
      v)
          readonly VERBOSE=1
          ;;
      h)
          usage
          exit 0
          ;;
      x)
          readonly DEBUG='-x'
          set -x
          ;;
      t)
          RUN_TESTS=$OPTARG
          verbose VINFO "Running tests"
          ;;
      c)
          readonly CONFIG_FILE=$OPTARG
          ;;
      n)
          readonly PRETEND=1
          ;;
    esac
  done

  if [[ $recursive_testing || -z $RUN_TESTS ]]; then
    [[ ! -f $CONFIG_FILE ]] \
      && eexit "You must provide --config file"
  fi
  return 0
}

On l’utilise ensuite de cette façon en utilisant la variable immutable ARGS qu’on a défini au début du script :

main() {
cmdline $ARGS
}
main

Tests unitaires

test_config_line_paths() {
  local s='partition cpm-all, 80-90,'

  returns "/a" "config_line_paths '$s /a, '"
  returns "/a /b/c" "config_line_paths '$s /a:/b/c, '"
  returns "/a /b /c" "config_line_paths '$s /a : /b : /c, '"
}

config_line_paths() {
  local partition_line="$@"

  echo $partition_line \
    | csv_column 3 \
    | delete_spaces \
    | column 1 \
    | colons_to_spaces
}

source /usr/bin/shunit2

Voici un autre exemple sur la commande df :

DF=df

mock_df_with_eols() {
    cat <<- EOF
    Filesystem           1K-blocks      Used Available Use% Mounted on
    /very/long/device/path
                         124628916  23063572 100299192  19% /
    EOF
}

test_disk_size() {
  returns 1000 "disk_size /dev/sda1"

  DF=mock_df_with_eols
  returns 124628916 "disk_size /very/long/device/path"
}

df_column() {
  local disk_device=$1
  local column=$2

  $DF $disk_device \
    | grep -v 'Use%' \
    | tr '\n' ' ' \
    | awk "{print \$$column}"
}

disk_size() {
  local disk_device=$1

  df_column $disk_device 2
}

Ici il y a une exception, pour les tests je déclare DF au niveau global et pas en lecture seule. C’est parce que shunit2 n’autorise pas les changements de fonctions au niveau global.


Les plus aguerris au développement logiciel trouveront peut-être évidents ces constructions de scripts, pour ma part j’avais déjà lu certains de ces conseils, notamment pour Python. C’est sympa de voir qu’on peut réutiliser les mêmes concepts pour Bash, par défaut c’est un langage particulièrement permissif, à l’image de PHP. Il n’en faut pas plus pour que les deux se trainent une réputation désastreuse.

Déployer un service Consul, mais surtout le sécuriser !

lundi 20 avril 2020 à 18:30

Maintenant que j’ai récupéré la main complète sur le destin de l’infrastructure publique dont fait partie le serveur qui héberge le blog, je prépare le futur, et ce futur passe par de l’infrastructure as code, notamment avec Terraform et Ansible. Pour le premier, un élément important du résultat est le fichier d’état de l’infrastructure, le fameux « tfstate ». Et pour le garder au chaud, j’ai voulu utiliser, plutôt qu’un fichier texte sur le pc, un service Consul. Mais il ne faut pas faire n’importe quoi avec.

Le parfait compagnon de Terraform

Consul fait partie d’une famille qu’on appelle service de stockage dit clé-valeur, comme Redis, même si ce dernier a une forte orientation « temporaire » car il sert souvent de mémoire cache. Il est développé par Hashicorp, la même boite qui s’occupe aussi de Terraform. Il est donc nativement inclus comme backend de stockage pour y conserver son précieux fichier tfstate.

Vous pourrez également y stocker quantité de données qui pourront être réutilisées par la suite. Un de nos clients chez Linkbynet l’utilise par exemple pour y enregistrer ses besoins en termes de projets et d’infrastructures liées (nom du projet, nombre de machines, dimensionnements, nommages des ressources), et on n’a plus qu’à lancer un pipeline qui va lire les valeurs et déployer l’infrastructure en fonction de ces données. Les usages sont donc multiples.

Évidemment, disposer d’un « serveur » consul n’a de sens que si on doit accéder à plusieurs machines à ces données, l’avoir en local est pratique pour faire des tests, mais superflu par rapport à un fichier plat local, notamment dans le cadre de Terraform.

Un déploiement sur Docker Swarm très rapide

Le service n’a pas besoin d’être répliqué vu l’usage basique, les volumes persistants ont été créés sur le NAS, et la stack est presque basique, je la partage quand même :

version: "3.2"
networks:
  consul:
    driver: overlay

volumes:
  consul_data:
    driver_opts:
      type: nfs
      o: "addr=192.168.1.200,rw,nfsvers=3,nolock,soft,exec"
      device: ":/volume1/Docker/consul/data"
  consul_config:
    driver_opts:
      type: nfs
      o: "addr=192.168.1.200,rw,nfsvers=3,nolock,soft,exec"
      device: ":/volume1/Docker/consul/config"
services:
  consul:
    image: consul:1.7.2
    environment:
      - "CONSUL_BIND_INTERFACE=eth0"
      - "CONSUL_HTTP_ADDR=0.0.0.0"
    entrypoint:
      - consul
      - agent
      - -server
      - -bootstrap-expect=1
      - -config-format
      - json
      - -config-file
      - /consul/config/consul-acl.json
      - -data-dir=/consul/data
      - -bind={{ GetInterfaceIP "eth0" }}
      - -client=0.0.0.0
      - -ui             

    networks:
      - consul
    ports:
      - "8500:8500"
      - "8600:8600/udp"
    volumes:
      - type: volume
        source: consul_data
        target: /consul/data
      - type: volume
        source: consul_config
        target: /consul/config  
    deploy:
      replicas: 1
      restart_policy:
        condition: any

La partie importante pour un déploiement « docker prod ready », c’est l’entrypoint qui contient les commandes permettant de bien faire prendre en compte l’initialisation des ACL et l’activation du mode « bootstrap » qui sinon laisse le serveur en mode « dev », avec moults messages de debug pratique, mais surtout un mode « in-memory » qui va un peu à l’envers de ce qu’on cherche à faire à savoir disposer d’un stockage persistant.

Données précieuses, sécurité adaptée

En effet, par défaut, c’est grand ouvert. Quelque soit le mode de déploiement, vous lancez, deux secondes après vous pouvez enregistrer votre première clé. On va pas se mentir, c’est pas dingue. Si quelqu’un vient foutre le bordel, notamment sur votre fichier d’état, vous serez bon pour tout reprendre à la main. Pour un ou deux serveurs, c’est pas grave, quand vous déployez plusieurs dizaines de ressources différentes d’un coup, ça fait bien mal au cul. Il est donc nécessaire de mettre en place un certain niveau de protection.

Le transport

Tout d’abord, il faut garder à l’esprit que les communications se font en HTTP. C’est donc un minimum que de chiffrer le transport. Plusieurs options sont possibles en fonction du contexte.

Pour un serveur qui restera dans un réseau un peu privé et un accès semi-manuel (exécution d’outils sur son poste), on pourra utiliser un tunnel SSH avec transfert de port. On utilisera la forme suivante :

$ ssh -L 8500:<consul_ip>:8500 <consul_ip|consul_host>

Ensuite, si vous interrogez le port local 8500, la connexion sera renvoyée à travers la connexion SSH pour interroger le port 8500 en face.

Dans mon cas, comme j’ai déployé le consul dans mon cluster Docker swarm, j’ai voulu passer par le reverse-proxy plutôt que le SSH. J’ai donc déployé du HTTPS avec un sous-domaine dédié et du Let’s Encrypt. C’est plus pratique dans ce dernier cas car consul peut être accédé par des outils tiers, comme un runner gitlab, et aussi, je peux utiliser n’importe quel PC sans forcément avoir mes clés SSH sur moi. Notez que dans un cas comme dans l’autre (SSH ou TLS), on est généralement sur des hauts niveaux de protection donc ça va.

L’authentification

Bien, le transport est chiffré, mais dans le cas d’HTTPS par exemple, ça ne résout pas un problème fondamental : si on connaît l’adresse, on fait ce qu’on veut du serveur. Fort heureusement, il est possible de mettre plusieurs choses en place.

La première qui est presque systématique sur les services que j’expose sur mon cluster Swarm, je met en place une authentification HTTP basique, qui repose sur un couple user/password que je génère quand je déploie le vhost sur le reverse-proxy. Sans ce couple, vous prenez une erreur 401 de la part d’Nginx. Les règles classiques se posent alors, entre longueur de mots de passe et complexité. Dans le cas présent où c’est conçu d’abord pour être exploité via des outils, la saisie manuelle n’étant pas nécessaire tout le temps je conseille de ne pas lésiner sur la longueur, pas de problème à disposer de couples de 32 caractères chacun, le tout bien aléatoire avec une palette étendue (alphanumérique, majuscule/minuscules, ponctuation). On évite quand même les emoji dans les login ou mots de passe qu’on commence à voir apparaître, déjà c’est pénible à taper sans clavier adapté, et croyez-le ou non ça a tendance à bien faire bugger les programmes, malgré les promesses d’UTF-8.

Consul de son côté dispose d’un système d’ACL qui semble bien fourni, mais que j’ai pour l’instant bien du mal à mettre en place. La faute à une documentation bien moins fournie et claire que peut l’être celle de Terraform, ou mieux celle d’Ansible qui regorge d’exemple pour tous les modules supportés permettant de mieux comprendre leur fonctionnement. Là si l’activation du moteur ACL est assez facile, la mise en place des règles de contrôle d’accès, et la création du token, semblent plus compliqués, sans parler de tester manuellement derrière, avec curl entre autres. Mais ceci dit, les ACL vous permettront notamment de limiter les actions d’une clé à certaines portions du serveur consul, ce qui permet d’éviter d’aller polluer les voisins.

À retenir, vous pouvez tout à fait activer les deux méthodes en même temps, Terraform permet de renseigner les informations pour les deux types de protection, il suffit d’initialiser le backend avec les options suivantes :

$ terraform init -backend=true \
    -backend-config="address=${CONSUL_ADDRESS}" \
    -backend-config="scheme=${CONSUL_SCHEME}" \
    -backend-config="http_auth=${CONSUL_USERNAME}:${CONSUL_PASSWORD}" \
    -backend-config="access_token=${CONSUL_TOKEN}" \
    -backend-config="path=${CONSUL_PATH}" \
    -backend-config="lock=${CONSUL_LOCK}"

Un outil versatile

Ici vous n’avez entrevu qu’un usage de Consul, mais il a plusieurs autres qualités que je vous laisse découvrir avec le site officiel. Je lâche pas l’affaire sur les ACL, j’ai beau reconnaître l’avantage de ce système de gestion de permissions, j’ai toujours du mal avec les différentes implémentations, j’ai pleuré avec celles de Mumble par exemple, pareil pour Teamspeak 3, donc autant dire que ça va mettre un peu de temps. Mais je désespère pas 🙂