Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Log in
Soumettre
OutilsExploitsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
ZigRaceExploit — POC CVE-2021-25741 en Zig | Kitploit
Outils/GitHubGitHub/glutenfree69/zigraceexploit
Sécurité des ConteneursAnalyse des VulnérabilitésExploitationSécurité CloudApprentissage et ÉducationExploitation de Binaires
GitHubglutenfree69/zigraceexploit

ZigRaceExploit

POC CVE-2021-25741 en Zig

Voir le dépôt
11il y a 6 moisPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

CVE-2021-25741 — TOCTOU Symlink Race Exploit in Zig

Exploit éducatif de la race condition TOCTOU (Time Of Check, Time Of Use) qui est à l'origine de CVE-2021-25741, une vulnérabilité dans le kubelet Kubernetes.

Le projet démontre en local (sans cluster K8s) comment un swap concurrent de symlink peut contourner une vérification de chemin entre lstat() et open(), et prouve que openat2() avec les flags RESOLVE_* est la solution.

Table des matières

  • Le problème
  • Comment fonctionne l'exploit
  • Architecture du code
  • Prérequis
  • Build
  • Utilisation
    • Test en local avec Docker
    • Comparaison openat vs openat2
    • Jouer avec le délai TOCTOU
    • Test sur un vrai node Kubernetes
  • Résultats attendus
  • Explication du code
  • Syscalls Linux utilisés
  • Debug avec strace
  • Références

Le problème

Résolution de chemins sous Linux

Quand le kernel résout un chemin comme /a/b/c/file, il le fait composant par composant. À chaque étape, si le composant est un symlink, le kernel le suit automatiquement. Cette résolution n'est pas atomique — le filesystem peut changer entre deux étapes.

TOCTOU dans le kubelet

Le kubelet Kubernetes faisait exactement ce pattern vulnérable :

1. CHECK : lstat(subPath) → "c'est un répertoire, c'est safe"
     ↕ FENÊTRE DE RACE — un process dans le container swap le répertoire pour un symlink
2. USE   : mount(subPath) → suit le symlink, monte le filesystem host

Entre la vérification et le mount, un process malveillant dans le container pouvait remplacer le subPath par un symlink vers / du host, obtenant ainsi un accès complet au filesystem host.

Le fix : openat2(2)

Le syscall openat2 (kernel 5.6+) résout le chemin et ouvre le fichier de manière atomique, avec des contraintes :

FlagEffet
RESOLVE_NO_SYMLINKSRefuse de suivre tout symlink → retourne ELOOP
RESOLVE_BENEATHRefuse de sortir du répertoire de base → retourne EXDEV
RESOLVE_IN_ROOTTraite le dirfd comme la racine du filesystem
RESOLVE_NO_XDEVRefuse de traverser des points de montage

Avec openat2, il n'y a pas de fenêtre TOCTOU : si un symlink apparaît pendant la résolution, le syscall échoue immédiatement.


Comment fonctionne l'exploit

Deux threads coopèrent pour exploiter la fenêtre TOCTOU :

workdir/
├── legit_dir/
│   └── secret.txt  → contient "LEGIT"
├── symlink_target/
│   └── secret.txt  → contient "PWNED"
└── target/          → swappé entre vrai dir et symlink

Thread Racer

Tourne en boucle ultra-rapide et swap atomiquement target/ entre deux états via renameat2(RENAME_EXCHANGE) :

  • État A : target/ est un vrai répertoire (contient secret.txt = "LEGIT")
  • État B : target/ est un symlink vers symlink_target/ (contient secret.txt = "PWNED")

Un seul syscall par swap = fenêtre de race maximale.

Thread Victim (simule le kubelet)

Reproduit le pattern vulnérable du kubelet :

  1. fstatat("target", AT_SYMLINK_NOFOLLOW) — vérifie que c'est un répertoire
  2. Pause optionnelle (simule la latence du kubelet entre check et use)
  3. openat(dirfd, "target/secret.txt", O_RDONLY) — ouvre le fichier
  4. read() — lit le contenu
  5. Si contenu == "PWNED" → race win (le symlink a été suivi)
  6. Si contenu == "LEGIT" → race loss (c'était bien le vrai répertoire)

En mode protégé (--use-openat2), l'étape 3 utilise openat2 avec RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH. Si un symlink est présent, le kernel retourne ELOOP au lieu de le suivre.


Architecture du code

src/
├── main.zig       # Point d'entrée : parse CLI, crée le shared state, spawn les
│                  # threads, mesure le temps, affiche les résultats
│
├── racer.zig      # Thread racer : prépare l'état initial (target → symlink),
│                  # puis boucle sur renameat2(RENAME_EXCHANGE) pour swapper
│                  # target/ et legit_dir/ en continu
│
├── victim.zig     # Thread victim : boucle N itérations de fstatat → delay →
│                  # openat/openat2 → read → compare "LEGIT" vs "PWNED"
│
├── setup.zig      # Crée l'arborescence de test : workdir/, legit_dir/,
│                  # symlink_target/, target/ avec les fichiers sentinel
│
├── syscalls.zig   # Constantes et wrappers pour les syscalls raw :
│                  # - open_how struct (kernel UAPI, 3×u64)
│                  # - RESOLVE_* flags
│                  # - RENAME_EXCHANGE (= 2)
│                  # - openat2() via linux.syscall4(.openat2, ...)
│                  # - rename_exchange() via linux.renameat2()
│                  # - Helpers : is_err(), to_errno(), to_fd()
│
└── stats.zig      # Compteurs atomiques lock-free (std.atomic.Value(u64))
                   # pour wins, losses, errors, eloop + affichage formaté

Tous les syscalls sont appelés via std.os.linux.* directement (pas de wrappers std.fs ou std.posix). Le seul syscall sans wrapper dans la stdlib Zig 0.15 est openat2, qu'on appelle via linux.syscall4(.openat2, ...) avec une struct open_how définie manuellement depuis les headers UAPI du kernel.


Prérequis

OutilVersionPourquoi
Zig0.15.xCompilateur + cross-compilation
DockerquelconquePour exécuter le binaire Linux sur Mac
colimaquelconqueRuntime Docker sur macOS (ou Docker Desktop)

Le binaire est compilé en statique (musl libc) et tourne sur n'importe quel Linux sans dépendances.

Kernel minimum :

  • 3.15+ pour renameat2(RENAME_EXCHANGE)
  • 5.6+ pour openat2 avec RESOLVE_* (nécessaire uniquement pour --use-openat2)

Docker Desktop et colima sur Mac utilisent un kernel 6.x — tout est supporté.


Build

Cross-compilation Mac → Linux

# ARM64 (Mac M1/M2/M3 → Docker colima / EC2 ARM)
zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe

# x86_64 (pour des nodes x86)
zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe

Le binaire est dans zig-out/bin/race-exploit.

$ file zig-out/bin/race-exploit
ELF 64-bit LSB executable, ARM aarch64, statically linked

Utilisation

Test en local avec Docker

# Démarrer le runtime Docker (si macOS)
colima start

# Lancer l'exploit (mode vulnérable par défaut)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine /app/race-exploit --iterations 10000

Comparaison openat vs openat2

# Mode vulnérable (openat) — la race fonctionne
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000

# Mode protégé (openat2) — la race est bloquée
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000 --use-openat2

Ou avec le script qui lance les deux :

./scripts/run_in_docker.sh --iterations 10000

Jouer avec le délai TOCTOU

Le paramètre --delay-us ajoute un délai entre le lstat (check) et le openat (use). Plus le délai est long, plus la fenêtre TOCTOU est large, et plus le win rate augmente :

# Sans délai — win rate ~25%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 50000

# 10µs — win rate ~35%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 50000 --delay-us 10

# 100µs — win rate ~50%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 50000 --delay-us 100
Télécharger l’outil