Aller au contenu

Exploiter une race condition TOCTOU sur fichier privilégié

Exploiter une race condition TOCTOU sur fichier privilégié

Section intitulée « Exploiter une race condition TOCTOU sur fichier privilégié »

Une vulnérabilité TOCTOU (Time Of Check To Time Of Use, délai entre vérification et utilisation) apparaît lorsqu’un programme vérifie un état puis agit plus tard en supposant que cet état n’a pas changé. Sur une opération privilégiée, cette hypothèse transforme un banal chemin de fichier en frontière de privilège percée.

→ Lire l’explication complète de la marmotte

Le schéma vulnérable tient en deux opérations distinctes :

  1. vérifier qu’un chemin désigne un fichier autorisé ;
  2. ouvrir ce même chemin avec des privilèges élevés.

Entre les deux, un processus contrôlant le répertoire ou le fichier peut remplacer l’objet vérifié par un lien symbolique, renommer un composant parent, monter un système de fichiers à un emplacement attendu ou modifier des attributs. Le programme n’agit alors plus sur l’objet contrôlé, mais sur celui choisi par l’attaquant.

La primitive fautive n’est pas lstat() ou access() prise isolément. C’est la décision d’autorisation attachée à un nom, puis l’action attachée plus tard au même nom. Un chemin est une résolution dynamique dans un espace de noms. Il ne constitue pas une identité stable. Croire l’inverse revient à confier une serrure à une pancarte.

Le cas classique concerne un binaire exécuté avec un identifiant utilisateur (UID, User Identifier) plus privilégié que son appelant. Il vérifie que /tmp/export/rapport.txt appartient à l’appelant, puis l’ouvre pour écriture avec ses privilèges. Si l’appelant peut remplacer rapport.txt par un lien vers un fichier sensible avant l’ouverture, le binaire privilégié suit le lien et écrit là où il ne devait jamais écrire.

La même famille de défauts existe hors système de fichiers : vérification d’un jeton puis consommation séparée, validation d’un état de base de données avant une transition, contrôle d’une politique IAM (Identity and Access Management, gestion des identités et des accès) avant l’appel à une ressource mutable. Le fichier reste toutefois le terrain de démonstration le plus net, car le noyau arbitre la résolution et les permissions.

Pour cartographier les autres mécanismes de cette catégorie, consulter les failles documentées et les choix de conception côté code dans la section développement.

La démonstration suivante ne requiert ni privilège administrateur ni binaire setuid. Elle force une pause artificielle après la vérification afin de rendre la course déterministe. En production, l’attaquant répète la substitution jusqu’à gagner la course, souvent avec plusieurs processus et des répertoires sur lesquels il possède les droits d’écriture.

Prérequis : Linux ou un système compatible POSIX (Portable Operating System Interface), un compilateur gcc, les outils mkdir, mv et ln. Exécuter dans un répertoire temporaire dédié. Le programme ci-dessous écrase uniquement le fichier passé en argument.

/* toctou_vulnerable.c : démonstration volontairement vulnérable */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char **argv) {
struct stat st;
int fd;
if (argc != 2) return 2;
/* Vérification sur le chemin : l'objet est régulier maintenant. */
if (lstat(argv[1], &st) != 0 || !S_ISREG(st.st_mode)) {
fputs("Refus : fichier non régulier\n", stderr);
return 1;
}
/* Pause pédagogique : elle agrandit la fenêtre TOCTOU. */
sleep(2);
/* Usage ultérieur du même chemin : un lien est maintenant suivi. */
fd = open(argv[1], O_WRONLY | O_TRUNC);
if (fd < 0) {
perror("open");
return 1;
}
write(fd, "ecrit par le programme\n", 23);
close(fd);
return 0;
}

Compiler puis déclencher le remplacement pendant la pause :

Fenêtre de terminal
mkdir -p /tmp/toctou-demo && cd /tmp/toctou-demo
gcc -Wall -Wextra -O2 -o toctou_vulnerable toctou_vulnerable.c
printf 'fichier valide\n' > cible
printf 'fichier de remplacement\n' > autre
./toctou_vulnerable cible &
pid=$!
sleep 1
mv cible cible.verifie
ln -s autre cible
wait "$pid"
cat autre

Le contenu de autre devient ecrit par le programme, alors que lstat() avait validé un fichier régulier nommé cible. La vérification était exacte à l’instant où elle a eu lieu. Elle était simplement périmée au moment de l’écriture.

Le délai de deux secondes ne représente pas une attaque réelle, il rend visible le défaut. Retirer ce délai ne corrige rien : la fenêtre devient plus petite, pas inexistante. Réduire une vulnérabilité probabiliste est une optimisation cosmétique. Les slides adorent ce genre de pansement.

La contre-mesure à retenir est nette : autoriser et utiliser le même objet déjà ouvert, jamais réévaluer un chemin mutable. Sous Linux, partir d’un descripteur de répertoire de confiance, puis ouvrir le fichier relatif avec des contraintes de résolution imposées par le noyau.

openat2() est l’option préférée lorsque le noyau est en version 5.6 ou supérieure. Son paramètre resolve permet de refuser les liens symboliques et d’interdire la sortie du répertoire de départ. Une bibliothèque C peut nécessiter un appel système direct si son en-tête ne fournit pas encore l’interface. Ce détail de portabilité est moins glamour qu’un mot-dièse DevSecOps, mais il décide si la protection existe.

/* Linux 5.6+ : ouvrir sans suivre de lien ni quitter base_fd. */
struct open_how how = {
.flags = O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC,
.mode = 0600,
.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS
};
int fd = syscall(SYS_openat2, base_fd, "rapport.txt", &how, sizeof(how));
if (fd < 0) {
/* Journaliser errno et refuser l'opération, sans repli non sécurisé. */
return -1;
}
/* Vérifier l'objet réellement ouvert, pas son nom historique. */
struct stat st;
if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode) || st.st_nlink != 1) {
close(fd);
return -1;
}

base_fd doit désigner un répertoire ouvert auparavant dans une zone non inscriptible par l’attaquant. Utiliser un chemin absolu réputé sûr ne suffit pas : chaque composant du chemin peut être renommé ou remplacé selon les droits disponibles.

Sur les plateformes sans openat2(), combiner openat() avec un descripteur de répertoire, O_NOFOLLOW, O_CLOEXEC, puis fstat(). Cette solution reste moins expressive : O_NOFOLLOW protège le dernier composant, pas les répertoires intermédiaires. Si le modèle de menace inclut un contrôle des parents, il faut revoir l’architecture, pas empiler des drapeaux jusqu’à obtenir un sapin de Noël.

Pour créer un fichier nouveau, O_CREAT | O_EXCL fournit une création atomique et échoue si le nom existe déjà. Pour modifier un fichier existant, ouvrir par descripteur puis vérifier type, propriétaire, groupe, nombre de liens et éventuellement numéro d’inœud avant l’opération métier. Le contrôle doit suivre le risque : st_nlink != 1 mérite souvent un refus dans un répertoire de dépôt, car les liens physiques contournent certaines hypothèses de propriété.

Les chemins venant d’une requête HTTP, d’une archive extraite, d’un dépôt Git, d’un volume conteneur ou d’un répertoire temporaire sont hostiles par défaut. Un identifiant de fichier encodé dans une base de données n’est pas automatiquement moins dangereux : si cet identifiant est résolu dans un espace de noms modifiable, la course revient par la porte de service.

Les répertoires temporaires partagés sont particulièrement toxiques. Un bit collant limite la suppression de fichiers appartenant à autrui, mais n’empêche ni la création de liens ni les courses sur des noms appartenant à l’attaquant. /tmp n’est pas une zone de confiance avec un bonnet rigolo, même lorsqu’un programme y dépose des fichiers à permissions restrictives.

mkstemp() crée un fichier temporaire de façon atomique et renvoie un descripteur déjà ouvert. C’est le bon choix pour un fichier de travail privé. Le nom retourné ne doit pas être refermé puis rouvert : cette rechute réintroduit la course. Conserver le descripteur, appliquer fchmod() et fsync() si la durabilité est requise, puis renommer atomiquement dans un répertoire contrôlé.

Ces protections ne garantissent pas l’intégrité du contenu écrit après ouverture. Un processus disposant déjà d’un accès au même descripteur, un stockage réseau aux sémantiques faibles, une compromission du processus privilégié ou une logique d’autorisation défectueuse restent hors périmètre. L’atomicité de résolution ne remplace ni séparation des privilèges, ni validation métier, ni confinement.

Dans un conteneur, les montages, espaces de noms et volumes persistants compliquent encore l’analyse. Un chemin qui paraît interne peut référer à un volume partagé avec un autre composant. Le confinement doit donc associer droits Unix, restrictions de montage en lecture seule lorsque possible, utilisateur non privilégié et politique d’accès applicative. La sécurité des conteneurs n’apparaît pas par génération spontanée parce qu’un manifeste contient le mot securityContext.

Un service privilégié doit exposer le minimum de fonctions de fichiers. Éviter les interfaces génériques du type « écrire où ce chemin l’indique ». Préférer un identifiant métier mappé côté serveur vers un répertoire fixe et un nom contrôlé. La réduction de flexibilité est le compromis central : on perd la commodité d’adresser un chemin arbitraire, on gagne une frontière d’autorisation vérifiable. Pour une opération privilégiée, ce compromis n’est pas négociable.

Journaliser les refus de résolution avec l’identité appelante, le répertoire logique et le code d’erreur, sans consigner de secrets ni de chemins contenant des données personnelles. Une hausse de ELOOP, EXDEV, ENOENT ou d’échecs RESOLVE_BENEATH peut signaler une tentative de liens ou une dérive de déploiement. Ces alertes doivent alimenter la supervision sécurité, pas mourir dans un fichier local que personne ne lit depuis l’âge du cuivre.

La sauvegarde ne corrige pas la course, mais limite l’impact d’une écriture réussie. Les fichiers modifiés par un composant privilégié nécessitent versions immuables, tests de restauration et contrôle d’intégrité. Restaurer un fichier sensible sans comprendre quel chemin a été suivi remet seulement le même piège dans sa boîte, avec un ruban.

Sans opération atomique sur un descripteur stable, la fenêtre entre la vérification d’un chemin et son utilisation reste ouverte. Un processus non privilégié qui contrôle le répertoire peut, pendant cet intervalle, remplacer le fichier vérifié par un lien symbolique, renommer un composant parent ou monter un système de fichiers à l’emplacement attendu. Le binaire privilégié agit alors sur la cible choisie par l’attaquant, pas sur celle qu’il croyait avoir validée : il suit le lien et écrit dans un fichier sensible avec ses droits élevés. Un chemin n’est pas une identité stable mais une résolution dynamique, et l’attaquant répète la substitution jusqu’à gagner la course. La même logique frappe hors du système de fichiers, quand un jeton est vérifié puis consommé séparément, ou qu’une politique d’accès est contrôlée avant l’appel à une ressource mutable.

Un programme peut recevoir le droit de modifier un fichier parce qu’il possède plus de permissions que la personne qui le lance. Avant de modifier ce fichier, il vérifie souvent son nom, son type et son propriétaire. Le problème apparaît si le programme fait cette vérification, puis attend même très peu de temps avant d’ouvrir le fichier.

Pendant cet intervalle, une personne malveillante peut retirer le fichier vérifié et placer à sa place un lien qui désigne un autre fichier. Quand le programme ouvre ensuite le même nom, le système suit le lien et ouvre un fichier différent. Le programme garde ses permissions élevées, donc il peut écrire dans ce second fichier alors que la personne malveillante n’aurait pas eu ce droit directement.

La correction consiste à ouvrir le fichier une seule fois dans un emplacement contrôlé, puis à garder la référence ouverte fournie par le système. Les vérifications doivent porter sur ce fichier déjà ouvert, pas sur son nom. Le programme doit aussi refuser les liens et empêcher la recherche de fichier de sortir du répertoire prévu.

Cette protection ne répare pas une mauvaise règle d’autorisation, un programme compromis ou un espace de stockage partagé sans contrôle. Il faut limiter les droits du programme, surveiller les refus suspects et conserver des sauvegardes testées pour réparer les fichiers qui auraient été modifiés.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.