Expériences · Lessons Learned
Le faux constat qui n'en était pas
Ce rapport est anonymisé. Il décrit un incident réel sans nom d'hôte, sans adresse et sans données clients. Le sujet n'est pas la vulnérabilité, mais la chaîne de preuve : que vérifie-t-on lorsqu'il faut décider en quelques heures si un système a été compromis — et avec quelle facilité le résultat peut être mal lu ?
Situation initiale
En l'espace d'une journée, le projet PHP a publié deux vulnérabilités critiques exploitables sans authentification (CVE-2026-109618 et CVE-2026-109619, CVSS 9,2 chacune, désignées ensemble « PHP2Shell »). Sont concernées les installations PHP qui traitent des requêtes web. L'hôte en question exploitait un reverse proxy, une instance Nextcloud sur PHP-FPM et plusieurs sites statiques. La question de l'exploitant était simple :
Sommes-nous concernés ? Le correctif est-il actif ? Et les journaux montrent-ils une exploitation ?
Trois questions, trois problèmes de preuve très différents. C'est là que résidait la difficulté.
Étape 1 — prouver l'exposition au lieu de la supposer
Le premier réflexe (« nous utilisons PHP, donc nous sommes concernés ») est trop grossier. Ce qui compte, c'est la SAPI : seules les installations qui traitent des requêtes web sont exposées. La preuve a donc été concrète :
- Inventaire du paysage PHP : uniquement
fpmetcliinstallés — pas demod_php, pas dephp-cgi, pas d'Apache. - Points exposés : exactement une application PHP derrière le proxy (la Nextcloud) et un port d'administration limité au LAN ; à côté, un site Adminer orphelin dont le socket PHP n'existait plus depuis des semaines.
- Pas de « PHP ailleurs » : les sites statiques sans bloc FastCGI ne constituent pas une surface d'attaque PHP.
Étape 2 — preuve du correctif : paquet et exécution
Une mise à jour de sécurité installée n'est pas encore un correctif actif. Entre « le paquet est sur le disque » et « le processus en cours l'utilise » se trouve un redémarrage du maître FPM. La vérification a donc été double :
| Vérification | Résultat | Ce qu'elle prouve |
|---|---|---|
| Version du paquet | 8.5.4-0ubuntu1.5 (correctif Ubuntu, USN-8948-1/-2) | Le correctif est installé |
| Date d'installation | 2026-10-22 06:12 (mises à jour automatiques) | Quand le correctif est arrivé |
| Redémarrage FPM | 2026-10-22 06:13, maître actif depuis | Le correctif s'exécute |
| Intégrité des fichiers | vérification des paquets sans écart | Aucun binaire manipulé |
Ces quatre lignes constituent la véritable preuve du correctif. Tout le reste est interprétation de journaux.
Étape 3 — quatre couches de journaux, quatre questions différentes
Un seul journal ne peut pas prouver un incident. Chaque couche répond à une question différente — et possède son propre angle mort :
| Couche | Répond à | Angle mort |
|---|---|---|
| Journal d'accès du proxy | Quelles requêtes sont arrivées, avec quel statut et quelle taille de corps ? | Aucun contenu ; la rotation limite la rétrospective |
| Journal applicatif (Nextcloud) | Qu'a fait l'application en interne — erreurs, avertissements, connexions ? | Au niveau info, seulement des événements, pas d'accès |
| Journal FPM | Plantage de workers, épuisement mémoire, erreurs fatales | Niveau PHP uniquement, sans contexte HTTP |
| Journald / noyau | OOM kills, segfaults, core dumps | Aucune sémantique applicative |
En complément : Fail2ban (force brute SSH) et le gestionnaire de paquets (moment du correctif). Seule la combinaison porte une affirmation — et seulement si les angles morts sont nommés.
Le faux constat — « l'application n'écrit pas de journal »
La recherche du journal applicatif a parcouru les répertoires habituels (/srv, /var, /home, profondeur limitée) sans rien trouver. La conclusion s'imposait : il n'y a pas de journal. Elle était fausse.
Deux choses avaient été négligées :
- Le répertoire de données se trouvait hors de l'espace de recherche. L'instance ne stockait pas ses données dans le répertoire du programme, mais sur un volume dédié. La recherche était minutieuse — simplement pas là.
- Le journaliseur avait un repli. Si le fichier de journal configuré n'existait pas et que son répertoire n'était pas accessible en écriture, l'application écrivait dans un fichier de substitution dans le répertoire de données. C'est un comportement correct — mais il masque une mauvaise configuration.
Le défaut réel se trouvait une couche plus bas : la tâche de rotation lit le chemin du journal dans la configuration. Comme ce chemin n'existait pas, elle comparait la taille d'un fichier inexistant — et ne tournait jamais. Résultat : un seul fichier de 438 Mo au lieu d'une rotation horaire.
logfile = dir-programme/data"] --> L{"Fichier présent ?"} L -->|non| F["Le journaliseur utilise le repli :
dir-données/nextcloud.log"] L -->|oui| W["Le journaliseur écrit le fichier configuré"] C --> R["La tâche de rotation lit
le même chemin configuré"] R --> N{"Fichier là ?"} N -->|non| X["Aucune rotation
(le fichier croît sans limite)"] F --> X
La leçon est inconfortable, car elle concerne notre propre travail : « non trouvé » n'est pas un constat tant que l'espace de recherche n'est pas documenté. Un constat négatif exige le même soin qu'un constat positif — y compris l'indication des endroits où l'on n'a pas cherché.
Le deuxième faux constat — le pare-feu
Un port d'administration était « accessible uniquement dans le LAN ». La vérification n'a trouvé aucune autorisation spécifique à ce port et a noté : aucune autorisation, donc fermé de l'extérieur. C'était également trop court.
En réalité, une règle générale pour tout le sous-réseau LAN existait, autorisant tous les ports. Le port était donc ouvert dans le LAN — la protection venait de la topologie réseau, non de la règle de port. Le résultat (« non accessible depuis Internet ») était correct, la justification non. Et une justification erronée est le début de la décision erronée suivante.
Règle : on lit un pare-feu dans son ensemble — politique par défaut, autorisations générales, ordre. Un simple regard sur « manque-t-il une autorisation de port ? » ne suffit pas.
Ce que les journaux montraient réellement
Après correction du chemin du journal, l'évaluation était possible — et le tableau s'est révélé banal :
- 18 244 requêtes HTTP sur dix jours ; l'application PHP n'a répondu à aucune requête par un 5xx.
- Corps de requête le plus volumineux : 25 Ko. Aucun corps ≥ 1 Mo — le chemin typique d'un épuisement mémoire était absent du trafic observé.
- Journal applicatif : 14 518 entrées, 0 erreur fatale, une seule erreur (app store inaccessible), aucune connexion échouée, aucune installation d'application.
- Recherche d'indicateurs sur 789 528 lignes (fonctions typiques de webshell, chemins
/tmp/) : zéro occurrence pour chacune. Les occurrences de noms de shell connus se sont révélées être des séquences aléatoires dans des identifiants de requête. - FPM et noyau : aucune erreur fatale, aucun OOM kill, aucun segfault.
- Racine web : aucun nouveau fichier PHP depuis le début de la période, aucune modification inattendue.
Le seul résultat « chaud » était un scanner envoyant quatre requêtes d'environ 1 Mo au site Adminer orphelin — exactement le schéma d'une tentative d'exploitation. Les quatre se sont terminées en 502 car le socket PHP n'existait pas. L'attaque visait un backend mort. C'est aussi un résultat : le constat le plus dangereux a démontré l'efficacité d'un durcissement (involontaire).
La limite de la preuve — et pourquoi il faut sécuriser les preuves
Deux règles de rétention limitaient l'affirmation :
- Journaux du proxy : rotation 5 × 10 Mo — environ dix jours de rétrospective. La période était entièrement couverte, pas davantage.
- Journal applicatif : la rotation ne conserve qu'un seul fichier précédent. À la rotation suivante, la période évaluée aurait été écrasée.
C'est pourquoi la période a été sécurisée avant la rotation suivante sous forme d'archive propre : compressée, propriétaire root, droits 640 (le fichier contient des noms d'utilisateur et des adresses IP), avec une somme de contrôle documentée. Une preuve non reproductible vaut peu lors d'un audit — une somme de contrôle transforme un fichier en affirmation vérifiable.
(un seul fichier tourné)"] --> B["Extrait de période
2026-10-15 … 2026-10-25"] B --> C["Archive gzip
root:root 640"] C --> D["SHA-256 dans le rapport"] D --> E["Reproduction :
gunzip + wc -l"]
Ce qui a bien fonctionné
- Preuve du correctif tenue deux fois (version du paquet et exécution) — l'affirmation décisive ne dépendait pas des journaux.
- Preuves négatives au lieu d'hypothèses : aucun nouveau fichier PHP, aucun 5xx, aucun OOM kill, aucune erreur fatale.
- Attaque contre un backend mort reconnue — le constat 502 a évité une évaluation dramatisée.
- Décision motivée, non affirmée : une obligation de notification a été examinée et écartée avec motifs, non passée sous silence.
Ce que nous ferons autrement la prochaine fois
- Documenter l'espace de recherche avant la recherche. N'écrire « non trouvé » que si les chemins et profondeurs sont nommés.
- Associer une contre-épreuve à chaque affirmation de configuration. Chemin du journal : non seulement le lire, mais vérifier que le fichier existe, croît et tourne.
- Tester la rotation activement (dépasser la taille artificiellement, prouver la rotation dans le journal) — ne pas supposer qu'elle fonctionne.
- Lire le pare-feu dans son ensemble (politique par défaut, règles générales de sous-réseau, ordre).
- Démanteler immédiatement les sites orphelins. Un site PHP mort n'est pas un risque, mais une source de bruit permanente dans les journaux et un angle mort de l'évaluation.
- Sécuriser les preuves avant la rotation — avec propriétaire, droits et somme de contrôle.
- Corriger les faux constats, ne pas les lisser. Le rapport contient la correction avec date et cause. Un faux constat réparé en silence détruit plus la confiance que l'erreur elle-même.
Transférabilité
L'incident lui-même fut peu spectaculaire : corrigé, non exploité, aucune notification requise. Ce qui a de la valeur, c'est l'appareil de preuve : exposition via inventaire, correctif via paquet et exécution, exploitation via plusieurs couches de journaux, évaluation avec limites de preuve explicites, preuve avec somme de contrôle — et un rapport avec historique des modifications qui contient aussi notre propre erreur d'appréciation. Cette structure peut être versionnée dans Git et vérifiée des années plus tard.
Sources
- Projet PHP : avis de sécurité pour
CVE-2026-109618etCVE-2026-109619. - CERT-EU, Security Advisory 2026-018 (mitigations et état de l'exploitation).
- Ubuntu Security Notices USN-8948-1/-2 (correctif du paquet pour PHP 8.5).
- Déclaration du fabricant de l'équipement périmétrique : PHP n'y est pas utilisé.