Skip to content
KitploitKITPLOIT
ИнструментыБлог
Log in
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

ЛентыКонтактыКонфиденциальность© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
ZigRaceExploit — CVE-2021-25741 POC на Zig | Kitploit
Инструменты/GitHubGitHub/glutenfree69/zigraceexploit
Безопасность контейнеровАнализ уязвимостейЭксплуатацияБезопасность облачных средОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubglutenfree69/zigraceexploit

ZigRaceExploit

CVE-2021-25741 POC на Zig

Репозиторий
126 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2021-25741 — TOCTOU гонка символьных ссылок в Zig

Образовательный эксплойт состояния гонки TOCTOU (Time Of Check, Time Of Use), которое лежит в основе CVE-2021-25741, уязвимости в kubelet Kubernetes.

Проект демонстрирует локально (без кластера K8s), как конкурентная подмена симлинка может обойти проверку пути между lstat() и open(), и доказывает, что openat2() с флагами RESOLVE_* является решением.

Содержание

  • Проблема
  • Как работает эксплойт
  • Архитектура кода
  • Предварительные требования
  • Сборка
  • Использование
    • Локальное тестирование с Docker
    • Сравнение openat vs openat2
    • Настройка задержки TOCTOU
    • Тестирование на реальном узле Kubernetes
  • Ожидаемые результаты
  • Пояснение кода
  • Используемые системные вызовы Linux
  • Отладка с strace
  • Ссылки

Проблема

Разрешение путей в Linux

Когда ядро разрешает путь, например /a/b/c/file, оно делает это компонент за компонентом. На каждом шаге, если компонент является симлинком, ядро автоматически следует за ним. Это разрешение не атомарно — файловая система может измениться между двумя шагами.

TOCTOU в kubelet

kubelet Kubernetes в точности реализовывал этот уязвимый шаблон:

1. ПРОВЕРКА: lstat(subPath) → "это каталог, всё безопасно"
     ↕ ОКНО ГОНКИ — процесс в контейнере заменяет каталог на симлинк
2. ИСПОЛЬЗОВАНИЕ: mount(subPath) → следует за симлинком, монтирует файловую систему хоста

Между проверкой и монтированием вредоносный процесс в контейнере мог заменить subPath на симлинк, ведущий на / хоста, получив полный доступ к файловой системе хоста.

Исправление: openat2(2)

Системный вызов openat2 (ядро 5.6+) разрешает путь и открывает файл атомарно, с ограничениями:

ФлагЭффект
RESOLVE_NO_SYMLINKSОтказывается следовать любым симлинкам → возвращает ELOOP
RESOLVE_BENEATHЗапрещает выход за пределы базового каталога → возвращает EXDEV
RESOLVE_IN_ROOTОбрабатывает dirfd как корень файловой системы
RESOLVE_NO_XDEVЗапрещает пересечение точек монтирования

С openat2 нет окна TOCTOU: если симлинк появляется во время разрешения, системный вызов немедленно завершается ошибкой.


Как работает эксплойт

Два потока взаимодействуют для эксплуатации окна TOCTOU:

workdir/
├── legit_dir/
│   └── secret.txt  → содержит "LEGIT"
├── symlink_target/
│   └── secret.txt  → содержит "PWNED"
└── target/          → переключается между реальным каталогом и симлинком

Поток Racer

Бесконечно быстро переключает target/ между двумя состояниями с помощью renameat2(RENAME_EXCHANGE):

  • Состояние 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" → race win (симлинк был использован)
  6. Если содержимое == "LEGIT" → race loss (это был реальный каталог)

В защищённом режиме (--use-openat2) шаг 3 использует openat2 с RESOLVE_NO_SYMLINKS | RESOLVE_BENEATH. Если присутствует симлинк, ядро возвращает ELOOP вместо того, чтобы следовать за ним.


Архитектура кода

src/
├── main.zig       # Точка входа: разбор CLI, создание разделяемого состояния, запуск
│                  # потоков, измерение времени, вывод результатов
│
├── racer.zig      # Поток Racer: подготовка начального состояния (target → симлинк),
│                  # затем цикл renameat2(RENAME_EXCHANGE) для непрерывного
│                  # переключения target/ и legit_dir/
│
├── victim.zig     # Поток Victim: цикл N итераций fstatat → задержка →
│                  # 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)
│                  # - openat2() через linux.syscall4(.openat2, ...)
│                  # - rename_exchange() через linux.renameat2()
│                  # - Вспомогательные: 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, ...) с вручную определённой структурой open_how из заголовков UAPI ядра.


Предварительные требования

ИнструментВерсияЗачем
Zig0.15.xКомпилятор + кросс-компиляция
DockerлюбаяДля запуска Linux-бинарника на Mac
colimaлюбаяСреда выполнения Docker на macOS (или Docker Desktop)

Бинарник компилируется в статически (musl libc) и работает на любом Linux без зависимостей.

Минимальное ядро:

  • 3.15+ для renameat2(RENAME_EXCHANGE)
  • 5.6+ для openat2 с RESOLVE_* (требуется только для --use-openat2)

Docker Desktop и colima на Mac используют ядро 6.x — всё поддерживается.


Сборка

Кросс-компиляция 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 (если macOS)
colima start

# Запуск эксплойта (по умолчанию уязвимый режим)
docker run --rm -v $(pwd)/zig-out/bin:/app alpine /app/race-exploit --iterations 10000

Сравнение openat vs openat2

# Уязвимый режим (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

Или с помощью скрипта, запускающего оба режима:

./scripts/run_in_docker.sh --iterations 10000

Настройка задержки TOCTOU

Параметр --delay-us добавляет задержку между lstat (проверка) и openat (использование). Чем больше задержка, тем шире окно TOCTOU, и тем выше win rate:

# Без задержки — 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
Скачать инструмент